Merged with latest master

This commit is contained in:
Demetris Stavrou 2018-07-10 11:58:59 +03:00
commit 0ba293a566
75 changed files with 847 additions and 638 deletions

View File

@ -1,32 +1 @@
**Note: for support questions, please use the `cookiecutter-django` tag on stackoverflow**. This repository's issues are reserved for feature requests and bug reports. ## [Make sure to follow one of the issue templates we've got](https://github.com/pydanny/cookiecutter-django/issues/new/choose), otherwise the issue might be closed immeditely
* **I'm submitting a ... **
- [ ] bug report
- [ ] feature request
- [ ] support request => Please do not submit support request here, see note at the top of this template.
* **Do you want to request a *feature* or report a *bug*?**
* **What is the current behavior?**
* **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem**
* **What is the expected behavior?**
* **What is the motivation / use case for changing the behavior?**
* **Please tell us about your environment:**
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)

23
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug Report
about: Report a bug
---
## What happened?
## What should've happened instead?
## Steps to reproduce
[//]: # (Any or all of the following:)
[//]: # (* Host system configuration: OS, Docker & friends' versions etc.)
[//]: # (* Project generation options)
[//]: # (* Logs)

24
.github/ISSUE_TEMPLATE/feature.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: New Feature Proposal
about: Propose a new feature
---
## Description
[//]: # (What's it you're proposing? How should it be implemented?)
## Rationale
[//]: # (Why should this feature be implemented?)
## Use case(s) / visualization(s)
[//]: # ("Better to see something once than to hear about it a thousand times.")

24
.github/ISSUE_TEMPLATE/improvement.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: Improvement Suggestion
about: Let us know how we could improve
---
## Description
[//]: # (What's it you're proposing? How should it be implemented?)
## Rationale
[//]: # (Why should this feature be implemented?)
## Use case(s) / visualization(s)
[//]: # ("Better to see something once than to hear about it a thousand times.")

10
.github/ISSUE_TEMPLATE/paid-support.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Paid Support Request
about: Ask Core Team members to help you out
---
Provided your question goes beyound [regular support](https://github.com/pydanny/cookiecutter-django/issues/new?template=question.md), and/or the task at hand is of timely/high priority nature use the below information to reach out for contributors directly.
* Daniel Roy Greenfeld, Project Lead ([GitHub](https://github.com/pydanny), [Patreon](https://www.patreon.com/danielroygreenfeld)): expertise in Django and AWS ELB.
* Nikita Shupeyko, Core Developer ([GitHub](https://github.com/webyneter)): expertise in Python/Django, hands-on DevOps and frontend experience.

6
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,6 @@
---
name: Question
about: Please, ask your question on StackOverflow or Gitter
---
First, make sure to examine [the docs](https://cookiecutter-django.readthedocs.io/en/latest/). If that doesn't help post a question on [StackOverflow](https://stackoverflow.com/questions/tagged/cookiecutter-django) tagged with `cookiecutter-django`. Finally, feel free to join [Gitter](https://gitter.im/pydanny/cookiecutter-django) and ask around.

28
.github/ISSUE_TEMPLATE/regression.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: Regression Report
about: Let us know if something that'd been working has broke
---
## What happened before?
## What happens now?
## Last stable commit / Since when?
## Steps to reproduce
[//]: # (Any or all of the following:)
[//]: # (* Host system configuration: OS, Docker & friends' versions etc.)
[//]: # (* Project generation options)
[//]: # (* Logs)

27
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,27 @@
[//]: # (Thank you for helping us out: your efforts mean great deal to the project and the community as a whole!)
[//]: # (Before you proceed:)
[//]: # (1. Make sure to add yourself to `CONTRIBUTORS.rst` through this PR provided you're contributing here for the first time)
[//]: # (2. Don't forget to update the `docs/` presuming others would benefit from a concise description of whatever that you're proposing)
## Description
[//]: # (What's it you're proposing?)
## Rationale
[//]: # (Why does the project need that?)
## Use case(s) / visualization(s)
[//]: # ("Better to see something once than to hear about it a thousand times.")

1
.gitignore vendored
View File

@ -215,7 +215,6 @@ tags
[Ii]nclude [Ii]nclude
[Ll]ib [Ll]ib
[Ll]ib64 [Ll]ib64
[Ll]ocal
[Ss]cripts [Ss]cripts
pyvenv.cfg pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json

View File

@ -1,5 +1,3 @@
# Config file for automatic testing at travis-ci.org
sudo: required sudo: required
services: services:
@ -10,18 +8,18 @@ language: python
python: 3.6 python: 3.6
env: env:
- TOX_ENV=py36 - TOX_ENV=py36
before_install: before_install:
- docker-compose -v - docker-compose -v
- docker -v - docker -v
script: script:
- tox -e $TOX_ENV - tox -e $TOX_ENV
- sh tests/test_docker.sh - sh tests/test_docker.sh
install: install:
- pip install tox - pip install tox
notifications: notifications:
email: email:

View File

@ -87,6 +87,7 @@ Listed in alphabetical order.
David Díaz `@ddiazpinto`_ @DavidDiazPinto David Díaz `@ddiazpinto`_ @DavidDiazPinto
Davur Clementsen `@dsclementsen`_ @davur Davur Clementsen `@dsclementsen`_ @davur
Delio Castillo `@jangeador`_ @jangeador Delio Castillo `@jangeador`_ @jangeador
Denis Orehovsky `@apirobot`_
Dónal Adams `@epileptic-fish`_ Dónal Adams `@epileptic-fish`_
Dong Huynh `@trungdong`_ Dong Huynh `@trungdong`_
Emanuel Calso `@bloodpet`_ @bloodpet Emanuel Calso `@bloodpet`_ @bloodpet
@ -120,6 +121,7 @@ Listed in alphabetical order.
Lyla Fischer Lyla Fischer
Malik Sulaimanov `@flyudvik`_ @flyudvik Malik Sulaimanov `@flyudvik`_ @flyudvik
Martin Blech Martin Blech
Martin Saizar `@msaizar`_
Mathijs Hoogland `@MathijsHoogland`_ Mathijs Hoogland `@MathijsHoogland`_
Matt Braymer-Hayes `@mattayes`_ @mattayes Matt Braymer-Hayes `@mattayes`_ @mattayes
Matt Linares Matt Linares
@ -160,6 +162,7 @@ Listed in alphabetical order.
Will Farley `@goldhand`_ @g01dhand Will Farley `@goldhand`_ @g01dhand
William Archinal `@archinal`_ William Archinal `@archinal`_
Yaroslav Halchenko Yaroslav Halchenko
Denis Bobrov `@delneg`_
========================== ============================ ============== ========================== ============================ ==============
.. _@a7p: https://github.com/a7p .. _@a7p: https://github.com/a7p
@ -171,6 +174,7 @@ Listed in alphabetical order.
.. _@amjith: https://github.com/amjith .. _@amjith: https://github.com/amjith
.. _@andor-pierdelacabeza: https://github.com/andor-pierdelacabeza .. _@andor-pierdelacabeza: https://github.com/andor-pierdelacabeza
.. _@antoniablair: https://github.com/antoniablair .. _@antoniablair: https://github.com/antoniablair
.. _@apirobot: https://github.com/apirobot
.. _@archinal: https://github.com/archinal .. _@archinal: https://github.com/archinal
.. _@areski: https://github.com/areski .. _@areski: https://github.com/areski
.. _@arruda: https://github.com/arruda .. _@arruda: https://github.com/arruda
@ -218,6 +222,7 @@ Listed in alphabetical order.
.. _@kevgathuku: https://github.com/kevgathuku .. _@kevgathuku: https://github.com/kevgathuku
.. _@knitatoms: https://github.com/knitatoms .. _@knitatoms: https://github.com/knitatoms
.. _@krzysztofzuraw: https://github.com/krzysztofzuraw .. _@krzysztofzuraw: https://github.com/krzysztofzuraw
.. _@msaizar: https://github.com/msaizar
.. _@MathijsHoogland: https://github.com/MathijsHoogland .. _@MathijsHoogland: https://github.com/MathijsHoogland
.. _@mattayes: https://github.com/mattayes .. _@mattayes: https://github.com/mattayes
.. _@menzenski: https://github.com/menzenski .. _@menzenski: https://github.com/menzenski
@ -261,7 +266,7 @@ Listed in alphabetical order.
.. _@brentpayne: https://github.com/brentpayne .. _@brentpayne: https://github.com/brentpayne
.. _@afrowave: https://github.com/afrowave .. _@afrowave: https://github.com/afrowave
.. _@pchiquet: https://github.com/pchiquet .. _@pchiquet: https://github.com/pchiquet
.. _@delneg: https://github.com/delneg
Special Thanks Special Thanks
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -65,7 +65,7 @@ Optional Integrations
*These features can be enabled during initial project setup.* *These features can be enabled during initial project setup.*
* Serve static files from Amazon S3 or Whitenoise_ * Serve static files from Amazon S3 or Whitenoise_
* Configuration for Celery_ * Configuration for Celery_ and Flower_ (the latter in Docker setup only)
* Integration with MailHog_ for local email testing * Integration with MailHog_ for local email testing
* Integration with Sentry_ for error logging * Integration with Sentry_ for error logging
@ -78,6 +78,7 @@ Optional Integrations
.. _Mailgun: http://www.mailgun.com/ .. _Mailgun: http://www.mailgun.com/
.. _Whitenoise: https://whitenoise.readthedocs.io/ .. _Whitenoise: https://whitenoise.readthedocs.io/
.. _Celery: http://www.celeryproject.org/ .. _Celery: http://www.celeryproject.org/
.. _Flower: https://github.com/mher/flower
.. _Anymail: https://github.com/anymail/django-anymail .. _Anymail: https://github.com/anymail/django-anymail
.. _MailHog: https://github.com/mailhog/MailHog .. _MailHog: https://github.com/mailhog/MailHog
.. _Sentry: https://sentry.io/welcome/ .. _Sentry: https://sentry.io/welcome/
@ -98,7 +99,9 @@ Support this Project!
This project is run by volunteers. Please support them in their efforts to maintain and improve Cookiecutter Django: This project is run by volunteers. Please support them in their efforts to maintain and improve Cookiecutter Django:
* https://www.patreon.com/danielroygreenfeld: Project lead. Expertise in AWS ELB and Django. * Daniel Roy Greenfeld, Project Lead (`GitHub <https://github.com/pydanny>`_, `Patreon <https://www.patreon.com/danielroygreenfeld>`_): expertise in Django and AWS ELB.
* Nikita Shupeyko, Core Developer (`GitHub <https://github.com/webyneter>`_): expertise in Python/Django, hands-on DevOps and frontend experience.
Projects that provide financial support to the maintainers: Projects that provide financial support to the maintainers:
@ -158,13 +161,13 @@ Answer the prompts with your own desired options_. For example::
domain_name [example.com]: myreddit.com domain_name [example.com]: myreddit.com
version [0.1.0]: 0.0.1 version [0.1.0]: 0.0.1
timezone [UTC]: America/Los_Angeles timezone [UTC]: America/Los_Angeles
use_whitenoise [y]: n use_whitenoise [n]: n
use_celery [n]: y use_celery [n]: y
use_mailhog [n]: n use_mailhog [n]: n
use_sentry [y]: y use_sentry [n]: y
use_pycharm [n]: y use_pycharm [n]: y
windows [n]: n windows [n]: n
use_docker [y]: n use_docker [n]: n
use_heroku [n]: y use_heroku [n]: y
use_compressor [n]: y use_compressor [n]: y
Select postgresql_version: Select postgresql_version:
@ -279,6 +282,8 @@ experience better.
Articles Articles
--------- ---------
* `cookiecutter-django with Nginx, Route 53 and ELB`_ - Feb. 12, 2018
* `cookiecutter-django and Amazon RDS`_ - Feb. 7, 2018
* `Deploying Cookiecutter-Django with Docker-Compose`_ - Oct. 19, 2017 * `Deploying Cookiecutter-Django with Docker-Compose`_ - Oct. 19, 2017
* `Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`_ - May 19, 2017 * `Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`_ - May 19, 2017
* `Exploring with Cookiecutter`_ - Dec. 3, 2016 * `Exploring with Cookiecutter`_ - Dec. 3, 2016
@ -290,6 +295,8 @@ Articles
Have a blog or online publication? Write about your cookiecutter-django tips and tricks, then send us a pull request with the link. Have a blog or online publication? Write about your cookiecutter-django tips and tricks, then send us a pull request with the link.
.. _`cookiecutter-django with Nginx, Route 53 and ELB`: https://msaizar.com/blog/cookiecutter-django-nginx-route-53-and-elb/
.. _`cookiecutter-django and Amazon RDS`: https://msaizar.com/blog/cookiecutter-django-and-amazon-rds/
.. _`Deploying Cookiecutter-Django with Docker-Compose`: http://adamantine.me/2017/10/19/deploying-cookiecutter-django-with-docker-compose/ .. _`Deploying Cookiecutter-Django with Docker-Compose`: http://adamantine.me/2017/10/19/deploying-cookiecutter-django-with-docker-compose/
.. _`Exploring with Cookiecutter`: http://www.snowboardingcoder.com/django/2016/12/03/exploring-with-cookiecutter/ .. _`Exploring with Cookiecutter`: http://www.snowboardingcoder.com/django/2016/12/03/exploring-with-cookiecutter/
.. _`Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`: https://joshuahunter.com/posts/using-cookiecutter-to-jumpstart-a-django-project-on-windows-with-pycharm/ .. _`Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`: https://joshuahunter.com/posts/using-cookiecutter-to-jumpstart-a-django-project-on-windows-with-pycharm/

View File

@ -18,6 +18,7 @@
"use_pycharm": "n", "use_pycharm": "n",
"use_docker": "n", "use_docker": "n",
"postgresql_version": [ "postgresql_version": [
"10.4",
"10.3", "10.3",
"10.2", "10.2",
"10.1", "10.1",
@ -34,8 +35,8 @@
"use_compressor": "n", "use_compressor": "n",
"use_celery": "n", "use_celery": "n",
"use_mailhog": "n", "use_mailhog": "n",
"use_sentry": "y", "use_sentry": "n",
"use_whitenoise": "y", "use_whitenoise": "n",
"use_heroku": "n", "use_heroku": "n",
"use_travisci": "n", "use_travisci": "n",
"keep_local_envs_in_vcs": "y", "keep_local_envs_in_vcs": "y",

View File

@ -14,26 +14,41 @@ Run these commands to deploy the project to Heroku:
heroku pg:promote DATABASE_URL heroku pg:promote DATABASE_URL
heroku addons:create heroku-redis:hobby-dev heroku addons:create heroku-redis:hobby-dev
heroku addons:create mailgun
heroku config:set WEB_CONCURRENCY=4 # If using mailgun:
# Generating a 32 character-long random string without any of the visually similiar characters "IOl01": heroku addons:create mailgun:starter
heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A-HJ-NP-Za-km-z2-9' | head -c 32)/"
heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)"
heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production
heroku config:set DJANGO_ALLOWED_HOSTS='.herokuapp.com'
heroku config:set DJANGO_AWS_ACCESS_KEY_ID=YOUR_AWS_ID_HERE heroku addons:create sentry:f1
heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY_HERE
heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME=YOUR_AWS_S3_BUCKET_NAME_HERE
# This is to be set only if you're using Sentry:
heroku config:set DJANGO_SENTRY_DSN=YOUR_SENTRY_DSN
heroku config:set PYTHONHASHSEED=random heroku config:set PYTHONHASHSEED=random
heroku config:set WEB_CONCURRENCY=4
heroku config:set DJANGO_DEBUG=False
heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production
heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)"
# Generating a 32 character-long random string without any of the visually similiar characters "IOl01":
heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A-HJ-NP-Za-km-z2-9' | head -c 32)/"
# Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
heroku config:set DJANGO_ALLOWED_HOSTS=
# Assign with AWS_ACCESS_KEY_ID
heroku config:set DJANGO_AWS_ACCESS_KEY_ID=
# Assign with AWS_SECRET_ACCESS_KEY
heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY=
# Assign with AWS_STORAGE_BUCKET_NAME
heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME=
git push heroku master git push heroku master
heroku run python manage.py migrate heroku run python manage.py migrate
heroku run python manage.py check --deploy
heroku run python manage.py createsuperuser heroku run python manage.py createsuperuser
heroku run python manage.py collectstatic --no-input
heroku run python manage.py check --deploy
heroku open heroku open

View File

@ -21,10 +21,13 @@ Before you begin, check out the ``production.yml`` file in the root of this proj
* ``redis``: Redis instance for caching; * ``redis``: Redis instance for caching;
* ``caddy``: Caddy web server with HTTPS on by default. * ``caddy``: Caddy web server with HTTPS on by default.
Provided you have opted for Celery (via setting ``use_celery`` to ``y``) there are two more services: Provided you have opted for Celery (via setting ``use_celery`` to ``y``) there are three more services:
* ``celeryworker`` running a Celery worker process; * ``celeryworker`` running a Celery worker process;
* ``celerybeat`` running a Celery beat process. * ``celerybeat`` running a Celery beat process;
* ``flower`` running Flower_ (for more info, check out :ref:`CeleryFlower` instructions for local environment).
.. _`Flower`: https://github.com/mher/flower
Configuring the Stack Configuring the Stack
@ -32,7 +35,7 @@ Configuring the Stack
The majority of services above are configured through the use of environment variables. Just check out :ref:`envs` and you will know the drill. The majority of services above are configured through the use of environment variables. Just check out :ref:`envs` and you will know the drill.
To obtain logs and information about crashes in a production setup, make sure that you have access to an external Sentry instance (e.g. by creating an account with `sentry.io`_), and set the ``DJANGO_SENTRY_DSN`` variable. To obtain logs and information about crashes in a production setup, make sure that you have access to an external Sentry instance (e.g. by creating an account with `sentry.io`_), and set the ``SENTRY_DSN`` variable.
You will probably also need to setup the Mail backend, for example by adding a `Mailgun`_ API key and a `Mailgun`_ sender domain, otherwise, the account creation view will crash and result in a 500 error when the backend attempts to send an email to the account owner. You will probably also need to setup the Mail backend, for example by adding a `Mailgun`_ API key and a `Mailgun`_ sender domain, otherwise, the account creation view will crash and result in a 500 error when the backend attempts to send an email to the account owner.
@ -70,7 +73,7 @@ You can read more about this here at `Automatic HTTPS`_ in the Caddy docs.
(Optional) Postgres Data Volume Modifications (Optional) Postgres Data Volume Modifications
--------------------------------------------- ---------------------------------------------
Postgres is saving its database files to the ``postgres_data`` volume by default. Change that if you want something else and make sure to make backups since this is not done automatically. Postgres is saving its database files to the ``production_postgres_data`` volume by default. Change that if you want something else and make sure to make backups since this is not done automatically.
Building & Running Production Stack Building & Running Production Stack
@ -84,6 +87,10 @@ Once this is ready, you can run it with::
docker-compose -f production.yml up docker-compose -f production.yml up
To run the stack and detach the containers, run::
docker-compose -f production.yml up -d
To run a migration, open up a second terminal and run:: To run a migration, open up a second terminal and run::
docker-compose -f production.yml run --rm django python manage.py migrate docker-compose -f production.yml run --rm django python manage.py migrate

View File

@ -91,8 +91,8 @@ This is the excerpt from your project's ``local.yml``: ::
context: . context: .
dockerfile: ./compose/production/postgres/Dockerfile dockerfile: ./compose/production/postgres/Dockerfile
volumes: volumes:
- postgres_data_local:/var/lib/postgresql/data - local_postgres_data:/var/lib/postgresql/data
- postgres_backup_local:/backups - local_postgres_data_backups:/backups
env_file: env_file:
- ./.envs/.local/.postgres - ./.envs/.local/.postgres
@ -170,3 +170,20 @@ When developing locally you can go with MailHog_ for email testing provided ``us
#. open up ``http://127.0.0.1:8025``. #. open up ``http://127.0.0.1:8025``.
.. _Mailhog: https://github.com/mailhog/MailHog/ .. _Mailhog: https://github.com/mailhog/MailHog/
.. _`CeleryFlower`:
Celery Flower
~~~~~~~~~~~~~
`Flower`_ is a "real-time monitor and web admin for Celery distributed task queue".
Prerequisites:
* ``use_docker`` was set to ``y`` on project initialization;
* ``use_celery`` was set to ``y`` on project initialization.
By default, it's enabled both in local and production environments (``local.yml`` and ``production.yml`` Docker Compose configs, respectively) through a ``flower`` service. For added security, ``flower`` requires its clients to provide authentication credentials specified as the corresponding environments' ``.envs/.local/.django`` and ``.envs/.production/.django`` ``CELERY_FLOWER_USER`` and ``CELERY_FLOWER_PASSWORD`` environment variables. Check out ``localhost:5555`` and see for yourself.
.. _`Flower`: https://github.com/mher/flower

View File

@ -1,31 +1,31 @@
Project Generation Options Project Generation Options
========================== ==========================
project_name [My Awesome Project]: project_name:
Your project's human-readable name, capitals and spaces allowed. Your project's human-readable name, capitals and spaces allowed.
project_slug [my_awesome_project]: project_slug:
Your project's slug without dashes or spaces. Used to name your repo Your project's slug without dashes or spaces. Used to name your repo
and in other places where a Python-importable version of your project name and in other places where a Python-importable version of your project name
is needed. is needed.
description [Behold My Awesome Project!] description:
Describes your project and gets used in places like ``README.rst`` and such. Describes your project and gets used in places like ``README.rst`` and such.
author_name [Daniel Roy Greenfeld]: author_name:
This is you! The value goes into places like ``LICENSE`` and such. This is you! The value goes into places like ``LICENSE`` and such.
email [daniel-roy-greenfeld@example.com]: email:
The email address you want to identify yourself in the project. The email address you want to identify yourself in the project.
domain_name [example.com] domain_name:
The domain name you plan to use for your project once it goes live. The domain name you plan to use for your project once it goes live.
Note that it can be safely changed later on whenever you need to. Note that it can be safely changed later on whenever you need to.
version [0.1.0] version:
The version of the project at its inception. The version of the project at its inception.
open_source_license [1] open_source_license:
A software license for the project. The choices are: A software license for the project. The choices are:
1. MIT_ 1. MIT_
@ -34,19 +34,19 @@ open_source_license [1]
4. `Apache Software License 2.0`_ 4. `Apache Software License 2.0`_
5. Not open source 5. Not open source
timezone [UTC] timezone:
The value to be used for the ``TIME_ZONE`` setting of the project. The value to be used for the ``TIME_ZONE`` setting of the project.
windows [n] windows:
Indicates whether the project should be configured for development on Windows. Indicates whether the project should be configured for development on Windows.
use_pycharm [n] use_pycharm:
Indicates whether the project should be configured for development with PyCharm_. Indicates whether the project should be configured for development with PyCharm_.
use_docker [y] use_docker:
Indicates whether the project should be configured to use Docker_ and `Docker Compose`_. Indicates whether the project should be configured to use Docker_ and `Docker Compose`_.
postgresql_version [1] postgresql_version:
Select a PostgreSQL_ version to use. The choices are: Select a PostgreSQL_ version to use. The choices are:
1. 10.3 1. 10.3
@ -57,45 +57,45 @@ postgresql_version [1]
6. 9.4 6. 9.4
7. 9.3 7. 9.3
js_task_runner [1] js_task_runner:
Select a JavaScript task runner. The choices are: Select a JavaScript task runner. The choices are:
1. None 1. None
2. Gulp_ 2. Gulp_
custom_bootstrap_compilation [n] custom_bootstrap_compilation:
Indicates whether the project should support Bootstrap recompilation Indicates whether the project should support Bootstrap recompilation
via the selected JavaScript task runner's task. This can be useful via the selected JavaScript task runner's task. This can be useful
for real-time Bootstrap variable alteration. for real-time Bootstrap variable alteration.
use_compressor [n] use_compressor:
Indicates whether the project should be configured to use `Django Compressor`_. Indicates whether the project should be configured to use `Django Compressor`_.
use_celery [n] use_celery:
Indicates whether the project should be configured to use Celery_. Indicates whether the project should be configured to use Celery_.
use_mailhog [n] use_mailhog:
Indicates whether the project should be configured to use MailHog_. Indicates whether the project should be configured to use MailHog_.
use_sentry [n] use_sentry:
Indicates whether the project should be configured to use Sentry_. Indicates whether the project should be configured to use Sentry_.
use_whitenoise [y] use_whitenoise:
Indicates whether the project should be configured to use WhiteNoise_. Indicates whether the project should be configured to use WhiteNoise_.
use_heroku [n] use_heroku:
Indicates whether the project should be configured so as to be deployable Indicates whether the project should be configured so as to be deployable
to Heroku_. to Heroku_.
use_travisci [n] use_travisci:
Indicates whether the project should be configured to use `Travis CI`_. Indicates whether the project should be configured to use `Travis CI`_.
keep_local_envs_in_vcs [y] keep_local_envs_in_vcs:
Indicates whether the project's ``.envs/.local/`` should be kept in VCS Indicates whether the project's ``.envs/.local/`` should be kept in VCS
(comes in handy when working in teams where local environment reproducibility (comes in handy when working in teams where local environment reproducibility
is strongly encouraged). is strongly encouraged).
debug [n] debug:
Indicates whether the project should be configured for debugging. Indicates whether the project should be configured for debugging.
This option is relevant for Cookiecutter Django developers only. This option is relevant for Cookiecutter Django developers only.

View File

@ -44,7 +44,7 @@ Environment Variable Django Setting Development
DJANGO_AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID n/a raises error DJANGO_AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID n/a raises error
DJANGO_AWS_SECRET_ACCESS_KEY AWS_SECRET_ACCESS_KEY n/a raises error DJANGO_AWS_SECRET_ACCESS_KEY AWS_SECRET_ACCESS_KEY n/a raises error
DJANGO_AWS_STORAGE_BUCKET_NAME AWS_STORAGE_BUCKET_NAME n/a raises error DJANGO_AWS_STORAGE_BUCKET_NAME AWS_STORAGE_BUCKET_NAME n/a raises error
DJANGO_SENTRY_DSN SENTRY_DSN n/a raises error SENTRY_DSN SENTRY_DSN n/a raises error
DJANGO_SENTRY_CLIENT SENTRY_CLIENT n/a raven.contrib.django.raven_compat.DjangoClient DJANGO_SENTRY_CLIENT SENTRY_CLIENT n/a raven.contrib.django.raven_compat.DjangoClient
DJANGO_SENTRY_LOG_LEVEL SENTRY_LOG_LEVEL n/a logging.INFO DJANGO_SENTRY_LOG_LEVEL SENTRY_LOG_LEVEL n/a logging.INFO
MAILGUN_API_KEY MAILGUN_ACCESS_KEY n/a raises error MAILGUN_API_KEY MAILGUN_ACCESS_KEY n/a raises error

View File

@ -32,7 +32,10 @@ DEBUG_VALUE = "debug"
def remove_open_source_files(): def remove_open_source_files():
file_names = ["CONTRIBUTORS.txt"] file_names = [
"CONTRIBUTORS.txt",
"LICENSE",
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
@ -61,6 +64,10 @@ def remove_docker_files():
os.remove(file_name) os.remove(file_name)
def remove_utility_files():
shutil.rmtree("utility")
def remove_heroku_files(): def remove_heroku_files():
file_names = ["Procfile", "runtime.txt", "requirements.txt"] file_names = ["Procfile", "runtime.txt", "requirements.txt"]
for file_name in file_names: for file_name in file_names:
@ -162,8 +169,12 @@ def set_django_admin_url(file_path):
return django_admin_url return django_admin_url
def generate_random_user():
return generate_random_string(length=32, using_ascii_letters=True)
def generate_postgres_user(debug=False): def generate_postgres_user(debug=False):
return DEBUG_VALUE if debug else generate_random_string(length=32, using_ascii_letters=True) return DEBUG_VALUE if debug else generate_random_user()
def set_postgres_user(file_path, value): def set_postgres_user(file_path, value):
@ -187,25 +198,56 @@ def set_postgres_password(file_path, value=None):
return postgres_password return postgres_password
def set_celery_flower_user(file_path, value):
celery_flower_user = set_flag(
file_path,
"!!!SET CELERY_FLOWER_USER!!!",
value=value,
)
return celery_flower_user
def set_celery_flower_password(file_path, value=None):
celery_flower_password = set_flag(
file_path,
"!!!SET CELERY_FLOWER_PASSWORD!!!",
value=value,
length=64,
using_digits=True,
using_ascii_letters=True,
)
return celery_flower_password
def append_to_gitignore_file(s): def append_to_gitignore_file(s):
with open(".gitignore", "a") as gitignore_file: with open(".gitignore", "a") as gitignore_file:
gitignore_file.write(s) gitignore_file.write(s)
gitignore_file.write(os.linesep) gitignore_file.write(os.linesep)
def set_flags_in_envs(postgres_user, debug=False): def set_flags_in_envs(
local_postgres_envs_path = os.path.join(".envs", ".local", ".postgres") postgres_user,
set_postgres_user(local_postgres_envs_path, value=postgres_user) celery_flower_user,
set_postgres_password(local_postgres_envs_path, value=DEBUG_VALUE if debug else None) debug=False,
):
local_django_envs_path = os.path.join(".envs", ".local", ".django")
production_django_envs_path = os.path.join(".envs", ".production", ".django") production_django_envs_path = os.path.join(".envs", ".production", ".django")
local_postgres_envs_path = os.path.join(".envs", ".local", ".postgres")
production_postgres_envs_path = os.path.join(".envs", ".production", ".postgres")
set_django_secret_key(production_django_envs_path) set_django_secret_key(production_django_envs_path)
set_django_admin_url(production_django_envs_path) set_django_admin_url(production_django_envs_path)
production_postgres_envs_path = os.path.join(".envs", ".production", ".postgres") set_postgres_user(local_postgres_envs_path, value=postgres_user)
set_postgres_password(local_postgres_envs_path, value=DEBUG_VALUE if debug else None)
set_postgres_user(production_postgres_envs_path, value=postgres_user) set_postgres_user(production_postgres_envs_path, value=postgres_user)
set_postgres_password(production_postgres_envs_path, value=DEBUG_VALUE if debug else None) set_postgres_password(production_postgres_envs_path, value=DEBUG_VALUE if debug else None)
set_celery_flower_user(local_django_envs_path, value=celery_flower_user)
set_celery_flower_password(local_django_envs_path, value=DEBUG_VALUE if debug else None)
set_celery_flower_user(production_django_envs_path, value=celery_flower_user)
set_celery_flower_password(production_django_envs_path, value=DEBUG_VALUE if debug else None)
def set_flags_in_settings_files(): def set_flags_in_settings_files():
set_django_secret_key(os.path.join("config", "settings", "local.py")) set_django_secret_key(os.path.join("config", "settings", "local.py"))
@ -223,8 +265,13 @@ def remove_celery_compose_dirs():
def main(): def main():
postgres_user = generate_postgres_user(debug="{{ cookiecutter.debug }}".lower() == "y") debug = "{{ cookiecutter.debug }}".lower() == "y"
set_flags_in_envs(postgres_user, debug="{{ cookiecutter.debug }}".lower() == "y")
set_flags_in_envs(
DEBUG_VALUE if debug else generate_random_user(),
DEBUG_VALUE if debug else generate_random_user(),
debug=debug,
)
set_flags_in_settings_files() set_flags_in_settings_files()
if "{{ cookiecutter.open_source_license }}" == "Not open source": if "{{ cookiecutter.open_source_license }}" == "Not open source":
@ -235,7 +282,9 @@ def main():
if "{{ cookiecutter.use_pycharm }}".lower() == "n": if "{{ cookiecutter.use_pycharm }}".lower() == "n":
remove_pycharm_files() remove_pycharm_files()
if "{{ cookiecutter.use_docker }}".lower() == "n": if "{{ cookiecutter.use_docker }}".lower() == "y":
remove_utility_files()
else:
remove_docker_files() remove_docker_files()
if "{{ cookiecutter.use_heroku }}".lower() == "n": if "{{ cookiecutter.use_heroku }}".lower() == "n":

View File

@ -8,6 +8,6 @@ flake8==3.5.0
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
tox==3.0.0 tox==3.1.1
pytest==3.5.1 pytest==3.6.3
pytest-cookies==0.3.0 pytest-cookies==0.3.0

View File

@ -11,7 +11,7 @@ mkdir -p .cache/docker
cd .cache/docker cd .cache/docker
# create the project using the default settings in cookiecutter.json # create the project using the default settings in cookiecutter.json
cookiecutter ../../ --no-input --overwrite-if-exists use_docker=y js_task_runner=None cookiecutter ../../ --no-input --overwrite-if-exists use_docker=y
cd my_awesome_project cd my_awesome_project
# run the project's tests # run the project's tests

View File

@ -5,3 +5,11 @@ USE_DOCKER=yes
# Redis # Redis
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0
{% if cookiecutter.use_celery == 'y' %}
# Celery
# ------------------------------------------------------------------------------
# Flower
CELERY_FLOWER_USER=!!!SET CELERY_FLOWER_USER!!!
CELERY_FLOWER_PASSWORD=!!!SET CELERY_FLOWER_PASSWORD!!!
{% endif %}

View File

@ -37,9 +37,17 @@ WEB_CONCURRENCY=4
{% if cookiecutter.use_sentry == 'y' %} {% if cookiecutter.use_sentry == 'y' %}
# Sentry # Sentry
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DJANGO_SENTRY_DSN= SENTRY_DSN=
{% endif %} {% endif %}
# Redis # Redis
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0
{% if cookiecutter.use_celery == 'y' %}
# Celery
# ------------------------------------------------------------------------------
# Flower
CELERY_FLOWER_USER=!!!SET CELERY_FLOWER_USER!!!
CELERY_FLOWER_PASSWORD=!!!SET CELERY_FLOWER_PASSWORD!!!
{% endif %}

View File

@ -330,7 +330,6 @@ tags
[Ii]nclude [Ii]nclude
[Ll]ib [Ll]ib
[Ll]ib64 [Ll]ib64
[Ll]ocal
[Ss]cripts [Ss]cripts
pyvenv.cfg pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json
@ -341,3 +340,5 @@ pip-selfcheck.json
MailHog MailHog
{%- endif %} {%- endif %}
{{ cookiecutter.project_slug }}/media/ {{ cookiecutter.project_slug }}/media/
.pytest_cache/

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/{{ cookiecutter.project_slug }}.iml" filepath="$PROJECT_DIR$/.idea/{{ cookiecutter.project_slug }}.iml" />
</modules>
</component>
</project>

View File

@ -1,33 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: runserver" type="Python.DjangoServer" factoryName="Django server" singleton="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.local" />
</envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="launchJavascriptDebuger" value="false" />
<option name="port" value="8000" />
<option name="host" value="0.0.0.0" />
<option name="additionalOptions" value="" />
<option name="browserUrl" value="" />
<option name="runTestServer" value="false" />
<option name="runNoReload" value="false" />
<option name="useCustomRunCommand" value="false" />
<option name="customRunCommand" value="" />
<method />
</configuration>
</component>

View File

@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: tests - all" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="TARGET" value="." />
<option name="SETTINGS_FILE" value="" />
<option name="CUSTOM_SETTINGS" value="false" />
<option name="USE_OPTIONS" value="false" />
<option name="OPTIONS" value="" />
<method />
</configuration>
</component>

View File

@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: tests - class: TestUser" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="TARGET" value="{{ cookiecutter.project_slug }}.users.tests.test_models.TestUser" />
<option name="SETTINGS_FILE" value="" />
<option name="CUSTOM_SETTINGS" value="false" />
<option name="USE_OPTIONS" value="false" />
<option name="OPTIONS" value="" />
<method />
</configuration>
</component>

View File

@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: tests - file: test_models" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="TARGET" value="{{ cookiecutter.project_slug }}.users.tests.test_models" />
<option name="SETTINGS_FILE" value="" />
<option name="CUSTOM_SETTINGS" value="false" />
<option name="USE_OPTIONS" value="false" />
<option name="OPTIONS" value="" />
<method />
</configuration>
</component>

View File

@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: tests - module: users" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="TARGET" value="{{ cookiecutter.project_slug }}.users" />
<option name="SETTINGS_FILE" value="" />
<option name="CUSTOM_SETTINGS" value="false" />
<option name="USE_OPTIONS" value="false" />
<option name="OPTIONS" value="" />
<method />
</configuration>
</component>

View File

@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: tests - specific: test_get_absolute_url" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="TARGET" value="{{ cookiecutter.project_slug }}.users.tests.test_models.TestUser.test_get_absolute_url" />
<option name="SETTINGS_FILE" value="" />
<option name="CUSTOM_SETTINGS" value="false" />
<option name="USE_OPTIONS" value="false" />
<option name="OPTIONS" value="" />
<method />
</configuration>
</component>

View File

@ -1,5 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="merge_production_dotenvs_in_dotenv" type="PythonConfigurationType" factoryName="Python" singleton="true"> <configuration default="false" name="merge_production_dotenvs_in_dotenv" type="PythonConfigurationType" factoryName="Python" singleton="true">
<module name="{{ cookiecutter.project_slug }}" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
<envs> <envs>
@ -10,7 +11,6 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" />
<option name="SCRIPT_NAME" value="merge_production_dotenvs_in_dotenv.py" /> <option name="SCRIPT_NAME" value="merge_production_dotenvs_in_dotenv.py" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />

View File

@ -1,17 +1,17 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: migrate" type="Python.DjangoServer" factoryName="Django server" singleton="true"> <configuration default="false" name="migrate" type="Python.DjangoServer" factoryName="Django server" singleton="true">
<module name="{{ cookiecutter.project_slug }}" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
<envs> <envs>
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.local" /> <env name="DJANGO_SETTINGS_MODULE" value="config.settings.local" />
</envs> </envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" /> <option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<PathMappingSettings> <PathMappingSettings>
<option name="pathMappings"> <option name="pathMappings">
<list> <list>

View File

@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="pytest: ." type="tests" factoryName="py.test" singleton="true">
<module name="{{ cookiecutter.project_slug }}" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;.&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method />
</configuration>
</component>

View File

@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="pytest: users" type="tests" factoryName="py.test" singleton="true">
<module name="{{ cookiecutter.project_slug }}" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;./{{ cookiecutter.project_slug }}/users/&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method />
</configuration>
</component>

View File

@ -0,0 +1,33 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="runserver" type="Python.DjangoServer" factoryName="Django server" singleton="true">
<module name="{{ cookiecutter.project_slug }}" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.local" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<PathMappingSettings>
<option name="pathMappings">
<list>
<mapping local-root="$PROJECT_DIR$" remote-root="/app" />
</list>
</option>
</PathMappingSettings>
<option name="launchJavascriptDebuger" value="false" />
<option name="port" value="8000" />
<option name="host" value="0.0.0.0" />
<option name="additionalOptions" value="" />
<option name="browserUrl" value="" />
<option name="runTestServer" value="false" />
<option name="runNoReload" value="false" />
<option name="useCustomRunCommand" value="false" />
<option name="customRunCommand" value="" />
<method />
</configuration>
</component>

View File

@ -1,17 +1,17 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker: runserver_plus" type="Python.DjangoServer" factoryName="Django server" singleton="true"> <configuration default="false" name="runserver_plus" type="Python.DjangoServer" factoryName="Django server" singleton="true">
<module name="{{ cookiecutter.project_slug }}" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
<envs> <envs>
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.local" /> <env name="DJANGO_SETTINGS_MODULE" value="config.settings.local" />
</envs> </envs>
<option name="SDK_HOME" value="docker-compose://[$PROJECT_DIR$/local.yml]:django/python" /> <option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<PathMappingSettings> <PathMappingSettings>
<option name="pathMappings"> <option name="pathMappings">
<list> <list>

View File

@ -5,19 +5,32 @@
<configuration> <configuration>
<option name="rootFolder" value="$MODULE_DIR$" /> <option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="config/settings/local.py" /> <option name="settingsModule" value="config/settings/local.py" />
<option name="manageScript" value="manage.py" /> <option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" /> <option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
{% if cookiecutter.js_task_runner != 'None' %}
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/node_modules" /> <excludeFolder url="file://$MODULE_DIR$/node_modules" />
</content> </content>
{% else %}
<content url="file://$MODULE_DIR$" />
{% endif %}
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PackageRequirementsSettings"> <component name="PackageRequirementsSettings">
<option name="requirementsPath" value="$MODULE_DIR$/requirements/local.txt" /> <option name="requirementsPath" value="$MODULE_DIR$/requirements/local.txt" />
</component>
<component name="PyDocumentationSettings">
<option name="renderExternalDocumentation" value="true" />
</component>
<component name="ReSTService">
<option name="workdir" value="$MODULE_DIR$/docs" />
<option name="DOC_DIR" value="$MODULE_DIR$/docs" />
</component> </component>
<component name="TemplatesService"> <component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" /> <option name="TEMPLATE_CONFIGURATION" value="Django" />

View File

@ -9,7 +9,7 @@ RUN apk update \
# Pillow dependencies # Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \ && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies # CFFI dependencies
&& apk add libffi-dev openssl-dev py-cffi \ && apk add libffi-dev py-cffi \
# Translations dependencies # Translations dependencies
&& apk add gettext \ && apk add gettext \
# https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell # https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell
@ -19,22 +19,26 @@ RUN apk update \
COPY ./requirements /requirements COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt RUN pip install -r /requirements/local.txt
COPY ./compose/production/django/entrypoint.sh /entrypoint.sh COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r//' /entrypoint.sh RUN sed -i 's/\r//' /entrypoint
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint
COPY ./compose/local/django/start.sh /start.sh COPY ./compose/local/django/start /start
RUN sed -i 's/\r//' /start.sh RUN sed -i 's/\r//' /start
RUN chmod +x /start.sh RUN chmod +x /start
{% if cookiecutter.use_celery == "y" %} {% if cookiecutter.use_celery == "y" %}
COPY ./compose/local/django/celery/worker/start.sh /start-celeryworker.sh COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r//' /start-celeryworker.sh RUN sed -i 's/\r//' /start-celeryworker
RUN chmod +x /start-celeryworker.sh RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start.sh /start-celerybeat.sh COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat.sh RUN sed -i 's/\r//' /start-celerybeat
RUN chmod +x /start-celerybeat.sh RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r//' /start-flower
RUN chmod +x /start-flower
{% endif %} {% endif %}
WORKDIR /app WORKDIR /app
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint"]

View File

@ -1,9 +1,7 @@
#!/bin/sh #!/bin/sh
set -o errexit set -o errexit
set -o pipefail
set -o nounset set -o nounset
set -o xtrace
rm -f './celerybeat.pid' rm -f './celerybeat.pid'

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -o errexit
set -o nounset
celery flower \
--app={{cookiecutter.project_slug}}.taskapp \
--broker="${CELERY_BROKER_URL}" \
--basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"

View File

@ -1,9 +1,7 @@
#!/bin/sh #!/bin/sh
set -o errexit set -o errexit
set -o pipefail
set -o nounset set -o nounset
set -o xtrace
celery -A {{cookiecutter.project_slug}}.taskapp worker -l INFO celery -A {{cookiecutter.project_slug}}.taskapp worker -l INFO

View File

@ -3,7 +3,6 @@
set -o errexit set -o errexit
set -o pipefail set -o pipefail
set -o nounset set -o nounset
set -o xtrace
python manage.py migrate python manage.py migrate

View File

@ -9,7 +9,7 @@ RUN apk update \
# Pillow dependencies # Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \ && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies # CFFI dependencies
&& apk add libffi-dev openssl-dev py-cffi && apk add libffi-dev py-cffi
RUN addgroup -S django \ RUN addgroup -S django \
&& adduser -S -G django django && adduser -S -G django django
@ -19,23 +19,29 @@ COPY ./requirements /requirements
RUN pip install --no-cache-dir -r /requirements/production.txt \ RUN pip install --no-cache-dir -r /requirements/production.txt \
&& rm -rf /requirements && rm -rf /requirements
COPY ./compose/production/django/gunicorn.sh /gunicorn.sh COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r//' /gunicorn.sh RUN sed -i 's/\r//' /entrypoint
RUN chmod +x /gunicorn.sh RUN chmod +x /entrypoint
RUN chown django /gunicorn.sh RUN chown django /entrypoint
COPY ./compose/production/django/entrypoint.sh /entrypoint.sh COPY ./compose/production/django/start /start
RUN sed -i 's/\r//' /entrypoint.sh RUN sed -i 's/\r//' /start
RUN chmod +x /entrypoint.sh RUN chmod +x /start
RUN chown django /entrypoint.sh RUN chown django /start
{% if cookiecutter.use_celery == "y" %} {% if cookiecutter.use_celery == "y" %}
COPY ./compose/production/django/celery/worker/start.sh /start-celeryworker.sh COPY ./compose/production/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r//' /start-celeryworker.sh RUN sed -i 's/\r//' /start-celeryworker
RUN chmod +x /start-celeryworker.sh RUN chmod +x /start-celeryworker
RUN chown django /start-celeryworker
COPY ./compose/production/django/celery/beat/start.sh /start-celerybeat.sh COPY ./compose/production/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat.sh RUN sed -i 's/\r//' /start-celerybeat
RUN chmod +x /start-celerybeat.sh RUN chmod +x /start-celerybeat
RUN chown django /start-celerybeat
COPY ./compose/production/django/celery/flower/start /start-flower
RUN sed -i 's/\r//' /start-flower
RUN chmod +x /start-flower
{% endif %} {% endif %}
COPY . /app COPY . /app
@ -45,4 +51,4 @@ USER django
WORKDIR /app WORKDIR /app
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint"]

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -o errexit
set -o nounset
celery flower \
--app={{cookiecutter.project_slug}}.taskapp \
--broker="${CELERY_BROKER_URL}" \
--basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"

View File

@ -5,8 +5,6 @@ set -o pipefail
set -o nounset set -o nounset
cmd="$@"
# N.B. If only .env files supported variable expansion... # N.B. If only .env files supported variable expansion...
export CELERY_BROKER_URL="${REDIS_URL}" export CELERY_BROKER_URL="${REDIS_URL}"
@ -36,12 +34,10 @@ sys.exit(0)
END END
} }
until postgres_ready; do until postgres_ready; do
>&2 echo 'PostgreSQL is unavailable (sleeping)...' >&2 echo 'Waiting for PostgreSQL to become available...'
sleep 1 sleep 1
done done
>&2 echo 'PostgreSQL is available'
>&2 echo 'PostgreSQL is up - continuing...' exec "$@"
exec $cmd

View File

@ -75,7 +75,7 @@ THIRD_PARTY_APPS = [
'rest_framework', 'rest_framework',
] ]
LOCAL_APPS = [ LOCAL_APPS = [
'{{ cookiecutter.project_slug }}.users.apps.UsersConfig', '{{ cookiecutter.project_slug }}.users.apps.UsersAppConfig',
# Your stuff: custom apps go here # Your stuff: custom apps go here
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -228,20 +228,26 @@ MANAGERS = ADMINS
{% if cookiecutter.use_celery == 'y' -%} {% if cookiecutter.use_celery == 'y' -%}
# Celery # Celery
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
INSTALLED_APPS += ['{{cookiecutter.project_slug}}.taskapp.celery.CeleryConfig'] INSTALLED_APPS += ['{{cookiecutter.project_slug}}.taskapp.celery.CeleryAppConfig']
if USE_TZ:
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone
CELERY_TIMEZONE = TIME_ZONE
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url
CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='django://') CELERY_BROKER_URL = env('CELERY_BROKER_URL')
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend
if CELERY_BROKER_URL == 'django://': CELERY_RESULT_BACKEND = CELERY_BROKER_URL
CELERY_RESULT_BACKEND = 'redis://'
else:
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content
CELERY_ACCEPT_CONTENT = ['json'] CELERY_ACCEPT_CONTENT = ['json']
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer
CELERY_RESULT_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json'
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit
# TODO: set to whatever value is adequate in your circumstances
CELERYD_TASK_TIME_LIMIT = 5 * 60
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit
# TODO: set to whatever value is adequate in your circumstances
CELERYD_TASK_SOFT_TIME_LIMIT = 60
{%- endif %} {%- endif %}
# django-allauth # django-allauth

View File

@ -76,8 +76,10 @@ INSTALLED_APPS += ['django_extensions'] # noqa F405
# Celery # Celery
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_always_eager # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-always-eager
CELERY_ALWAYS_EAGER = True CELERY_TASK_ALWAYS_EAGER = True
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates
CELERY_TASK_EAGER_PROPAGATES = True
{%- endif %} {%- endif %}
# Your stuff... # Your stuff...

View File

@ -73,8 +73,6 @@ AWS_SECRET_ACCESS_KEY = env('DJANGO_AWS_SECRET_ACCESS_KEY')
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
AWS_STORAGE_BUCKET_NAME = env('DJANGO_AWS_STORAGE_BUCKET_NAME') AWS_STORAGE_BUCKET_NAME = env('DJANGO_AWS_STORAGE_BUCKET_NAME')
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
AWS_AUTO_CREATE_BUCKET = True
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
AWS_QUERYSTRING_AUTH = False AWS_QUERYSTRING_AUTH = False
# DO NOT change these unless you know what you're doing. # DO NOT change these unless you know what you're doing.
_AWS_EXPIRY = 60 * 60 * 24 * 7 _AWS_EXPIRY = 60 * 60 * 24 * 7
@ -99,7 +97,7 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
MEDIA_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/' MEDIA_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/'
{%- else %} {%- else %}
# region http://stackoverflow.com/questions/10390244/ # region http://stackoverflow.com/questions/10390244/
from storages.backends.s3boto3 import S3Boto3Storage from storages.backends.s3boto3 import S3Boto3Storage # noqa E402
StaticRootS3BotoStorage = lambda: S3Boto3Storage(location='static') # noqa StaticRootS3BotoStorage = lambda: S3Boto3Storage(location='static') # noqa
MediaRootS3BotoStorage = lambda: S3Boto3Storage(location='media', file_overwrite=False) # noqa MediaRootS3BotoStorage = lambda: S3Boto3Storage(location='media', file_overwrite=False) # noqa
# endregion # endregion
@ -158,8 +156,8 @@ INSTALLED_APPS += ['gunicorn'] # noqa F405
# http://whitenoise.evans.io/en/latest/django.html#enable-whitenoise # http://whitenoise.evans.io/en/latest/django.html#enable-whitenoise
MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware'] + MIDDLEWARE # noqa F405 MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware'] + MIDDLEWARE # noqa F405
{%- endif %} {% endif %}
{% if cookiecutter.use_compressor == 'y' -%} {%- if cookiecutter.use_compressor == 'y' -%}
# django-compressor # django-compressor
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED
@ -169,16 +167,16 @@ COMPRESS_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL
COMPRESS_URL = STATIC_URL COMPRESS_URL = STATIC_URL
{%- endif %} {% endif %}
{% if cookiecutter.use_whitenoise == 'n' -%} {%- if cookiecutter.use_whitenoise == 'n' -%}
# Collectfast # Collectfast
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://github.com/antonagestam/collectfast#installation # https://github.com/antonagestam/collectfast#installation
INSTALLED_APPS = ['collectfast'] + INSTALLED_APPS # noqa F405 INSTALLED_APPS = ['collectfast'] + INSTALLED_APPS # noqa F405
AWS_PRELOAD_METADATA = True AWS_PRELOAD_METADATA = True
{%- endif %} {% endif %}
{% if cookiecutter.use_sentry == 'y' -%} {%- if cookiecutter.use_sentry == 'y' -%}
# raven # raven
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.sentry.io/clients/python/integrations/django/ # https://docs.sentry.io/clients/python/integrations/django/
@ -187,7 +185,7 @@ MIDDLEWARE = ['raven.contrib.django.raven_compat.middleware.SentryResponseErrorI
# Sentry # Sentry
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
SENTRY_DSN = env('DJANGO_SENTRY_DSN') SENTRY_DSN = env('SENTRY_DSN')
SENTRY_CLIENT = env('DJANGO_SENTRY_CLIENT', default='raven.contrib.django.raven_compat.DjangoClient') SENTRY_CLIENT = env('DJANGO_SENTRY_CLIENT', default='raven.contrib.django.raven_compat.DjangoClient')
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
@ -241,6 +239,7 @@ SENTRY_CELERY_LOGLEVEL = env.int('DJANGO_SENTRY_LOG_LEVEL', logging.INFO)
RAVEN_CONFIG = { RAVEN_CONFIG = {
'dsn': SENTRY_DSN 'dsn': SENTRY_DSN
} }
{%- else %} {%- else %}
# LOGGING # LOGGING
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -290,6 +289,6 @@ LOGGING = {
} }
} }
{%- endif %} {% endif %}
# Your stuff... # Your stuff...
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -23,12 +23,10 @@ from django.core.wsgi import get_wsgi_application
app_path = os.path.abspath(os.path.join( app_path = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)), os.pardir)) os.path.dirname(os.path.abspath(__file__)), os.pardir))
sys.path.append(os.path.join(app_path, '{{ cookiecutter.project_slug }}')) sys.path.append(os.path.join(app_path, '{{ cookiecutter.project_slug }}'))
{% if cookiecutter.use_sentry == 'y' -%} {% if cookiecutter.use_sentry == 'y' -%}
if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production': if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production':
from raven.contrib.django.raven_compat.middleware.wsgi import Sentry from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
{%- endif %} {%- endif %}
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use # if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use # mod_wsgi daemon mode with each site in its own daemon process, or use

View File

@ -1,8 +1,8 @@
version: '2' version: '3'
volumes: volumes:
postgres_data_local: {} local_postgres_data: {}
postgres_backup_local: {} local_postgres_data_backups: {}
services: services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %} django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
@ -22,7 +22,7 @@ services:
- ./.envs/.local/.postgres - ./.envs/.local/.postgres
ports: ports:
- "8000:8000" - "8000:8000"
command: /start.sh command: /start
postgres: postgres:
build: build:
@ -30,8 +30,8 @@ services:
dockerfile: ./compose/production/postgres/Dockerfile dockerfile: ./compose/production/postgres/Dockerfile
image: {{ cookiecutter.project_slug }}_production_postgres image: {{ cookiecutter.project_slug }}_production_postgres
volumes: volumes:
- postgres_data_local:/var/lib/postgresql/data - local_postgres_data:/var/lib/postgresql/data
- postgres_backup_local:/backups - local_postgres_data_backups:/backups
env_file: env_file:
- ./.envs/.local/.postgres - ./.envs/.local/.postgres
{%- if cookiecutter.use_mailhog == 'y' %} {%- if cookiecutter.use_mailhog == 'y' %}
@ -56,11 +56,8 @@ services:
{% if cookiecutter.use_mailhog == 'y' -%} {% if cookiecutter.use_mailhog == 'y' -%}
- mailhog - mailhog
{%- endif %} {%- endif %}
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports: [] ports: []
command: /start-celeryworker.sh command: /start-celeryworker
celerybeat: celerybeat:
<<: *django <<: *django
@ -71,10 +68,14 @@ services:
{% if cookiecutter.use_mailhog == 'y' -%} {% if cookiecutter.use_mailhog == 'y' -%}
- mailhog - mailhog
{%- endif %} {%- endif %}
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports: [] ports: []
command: /start-celerybeat.sh command: /start-celerybeat
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_local_flower
ports:
- "5555:5555"
command: /start-flower
{%- endif %} {%- endif %}

View File

@ -1,9 +1,9 @@
version: '2' version: '3'
volumes: volumes:
postgres_data: {} production_postgres_data: {}
postgres_backup: {} production_postgres_data_backups: {}
traefik_acme: {} production_traefik: {}
services: services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %} django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
@ -17,7 +17,7 @@ services:
env_file: env_file:
- ./.envs/.production/.django - ./.envs/.production/.django
- ./.envs/.production/.postgres - ./.envs/.production/.postgres
command: /gunicorn.sh command: /start
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.backend=django" - "traefik.backend=django"
@ -30,8 +30,8 @@ services:
dockerfile: ./compose/production/postgres/Dockerfile dockerfile: ./compose/production/postgres/Dockerfile
image: {{ cookiecutter.project_slug }}_production_postgres image: {{ cookiecutter.project_slug }}_production_postgres
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - production_postgres_data:/var/lib/postgresql/data
- postgres_backup:/backups - production_postgres_data_backups:/backups
env_file: env_file:
- ./.envs/.production/.postgres - ./.envs/.production/.postgres
@ -44,11 +44,11 @@ services:
- django - django
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_acme:/etc/traefik/acme - production_traefik:/etc/traefik/acme
ports: ports:
- "80:80" - "0.0.0.0:80:80"
- "443:443" - "0.0.0.0:443:443"
- "8080:8080" - "0.0.0.0:8080:8080"
redis: redis:
image: redis:3.2 image: redis:3.2
@ -57,26 +57,23 @@ services:
celeryworker: celeryworker:
<<: *django <<: *django
image: {{ cookiecutter.project_slug }}_production_celeryworker image: {{ cookiecutter.project_slug }}_production_celeryworker
depends_on: command: /start-celeryworker
- postgres
- redis
env_file:
- ./.envs/.production/.django
- ./.envs/.production/.postgres
command: /start-celeryworker.sh
labels: labels:
- "traefik.enable=false" - "traefik.enable=false"
celerybeat: celerybeat:
<<: *django <<: *django
image: {{ cookiecutter.project_slug }}_production_celerybeat image: {{ cookiecutter.project_slug }}_production_celerybeat
depends_on: command: /start-celerybeat
- postgres labels:
- redis - "traefik.enable=false"
env_file:
- ./.envs/.production/.django flower:
- ./.envs/.production/.postgres <<: *django
command: /start-celerybeat.sh image: {{ cookiecutter.project_slug }}_production_flower
ports:
- "5555:5555"
command: /start-flower
labels: labels:
- "traefik.enable=false" - "traefik.enable=false"

View File

@ -1,6 +1,6 @@
pytz==2018.4 # https://github.com/stub42/pytz pytz==2018.5 # https://github.com/stub42/pytz
python-slugify==1.2.5 # https://github.com/un33k/python-slugify python-slugify==1.2.5 # https://github.com/un33k/python-slugify
Pillow==5.1.0 # https://github.com/python-pillow/Pillow Pillow==5.2.0 # https://github.com/python-pillow/Pillow
{%- if cookiecutter.use_compressor == "y" %} {%- if cookiecutter.use_compressor == "y" %}
rcssmin==1.0.6{% if cookiecutter.windows == 'y' %} --install-option="--without-c-extensions"{% endif %} # https://github.com/ndparker/rcssmin rcssmin==1.0.6{% if cookiecutter.windows == 'y' %} --install-option="--without-c-extensions"{% endif %} # https://github.com/ndparker/rcssmin
{%- endif %} {%- endif %}
@ -10,13 +10,16 @@ whitenoise==3.3.1 # https://github.com/evansd/whitenoise
{%- endif %} {%- endif %}
redis>=2.10.5 # https://github.com/antirez/redis redis>=2.10.5 # https://github.com/antirez/redis
{%- if cookiecutter.use_celery == "y" %} {%- if cookiecutter.use_celery == "y" %}
celery==3.1.25 # pyup: <4.0 # https://github.com/celery/celery celery==4.2.0 # pyup: <5.0 # https://github.com/celery/celery
{%- if cookiecutter.use_docker == 'y' %}
flower==0.9.2 # https://github.com/mher/flower
{%- endif %}
{%- endif %} {%- endif %}
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
django==2.0.5 # pyup: < 2.1 # https://www.djangoproject.com/ django==2.0.7 # pyup: < 2.1 # https://www.djangoproject.com/
django-environ==0.4.4 # https://github.com/joke2k/django-environ django-environ==0.4.5 # https://github.com/joke2k/django-environ
django-model-utils==3.1.2 # https://github.com/jazzband/django-model-utils django-model-utils==3.1.2 # https://github.com/jazzband/django-model-utils
django-allauth==0.36.0 # https://github.com/pennersr/django-allauth django-allauth==0.36.0 # https://github.com/pennersr/django-allauth
django-crispy-forms==1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms django-crispy-forms==1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms

View File

@ -2,16 +2,16 @@
Werkzeug==0.14.1 # https://github.com/pallets/werkzeug Werkzeug==0.14.1 # https://github.com/pallets/werkzeug
ipdb==0.11 # https://github.com/gotcha/ipdb ipdb==0.11 # https://github.com/gotcha/ipdb
Sphinx==1.7.4 # https://github.com/sphinx-doc/sphinx Sphinx==1.7.5 # https://github.com/sphinx-doc/sphinx
{%- if cookiecutter.use_docker == 'y' %} {%- if cookiecutter.use_docker == 'y' %}
psycopg2==2.7.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 psycopg2==2.7.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- else %} {%- else %}
psycopg2-binary==2.7.4 # https://github.com/psycopg/psycopg2 psycopg2-binary==2.7.5 # https://github.com/psycopg/psycopg2
{%- endif %} {%- endif %}
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
pytest==3.5.1 # https://github.com/pytest-dev/pytest pytest==3.6.3 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.1 # https://github.com/Frozenball/pytest-sugar pytest-sugar==0.9.1 # https://github.com/Frozenball/pytest-sugar
# Code quality # Code quality
@ -22,9 +22,8 @@ coverage==4.5.1 # https://github.com/nedbat/coveragepy
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
factory-boy==2.11.1 # https://github.com/FactoryBoy/factory_boy factory-boy==2.11.1 # https://github.com/FactoryBoy/factory_boy
django-test-plus==1.0.22 # https://github.com/revsys/django-test-plus
django-debug-toolbar==1.9.1 # https://github.com/jazzband/django-debug-toolbar django-debug-toolbar==1.9.1 # https://github.com/jazzband/django-debug-toolbar
django-extensions==2.0.7 # https://github.com/django-extensions/django-extensions django-extensions==2.0.7 # https://github.com/django-extensions/django-extensions
django-coverage-plugin==1.5.0 # https://github.com/nedbat/django_coverage_plugin django-coverage-plugin==1.5.0 # https://github.com/nedbat/django_coverage_plugin
pytest-django==3.2.1 # https://github.com/pytest-dev/pytest-django pytest-django==3.3.2 # https://github.com/pytest-dev/pytest-django

View File

@ -8,10 +8,10 @@ psycopg2==2.7.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
Collectfast==0.6.2 # https://github.com/antonagestam/collectfast Collectfast==0.6.2 # https://github.com/antonagestam/collectfast
{%- endif %} {%- endif %}
{%- if cookiecutter.use_sentry == "y" %} {%- if cookiecutter.use_sentry == "y" %}
raven==6.8.0 # https://github.com/getsentry/raven-python raven==6.9.0 # https://github.com/getsentry/raven-python
{%- endif %} {%- endif %}
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
django-storages[boto3]==1.6.6 # https://github.com/jschneier/django-storages django-storages[boto3]==1.6.6 # https://github.com/jschneier/django-storages
django-anymail==2.2 # https://github.com/anymail/django-anymail django-anymail[mailgun]==3.0 # https://github.com/anymail/django-anymail

View File

@ -0,0 +1,20 @@
import pytest
from django.conf import settings
from django.test import RequestFactory
from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory
@pytest.fixture(autouse=True)
def media_storage(settings, tmpdir):
settings.MEDIA_ROOT = tmpdir.strpath
@pytest.fixture
def user() -> settings.AUTH_USER_MODEL:
return UserFactory()
@pytest.fixture
def request_factory() -> RequestFactory:
return RequestFactory()

View File

@ -13,14 +13,16 @@ if not settings.configured:
app = Celery('{{cookiecutter.project_slug}}') app = Celery('{{cookiecutter.project_slug}}')
class CeleryConfig(AppConfig): class CeleryAppConfig(AppConfig):
name = '{{cookiecutter.project_slug}}.taskapp' name = '{{cookiecutter.project_slug}}.taskapp'
verbose_name = 'Celery Config' verbose_name = 'Celery Config'
def ready(self): def ready(self):
# Using a string here means the worker will not have to # Using a string here means the worker will not have to
# pickle the object when using Windows. # pickle the object when using Windows.
app.config_from_object('django.conf:settings') # - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
installed_apps = [app_config.name for app_config in apps.get_app_configs()] installed_apps = [app_config.name for app_config in apps.get_app_configs()]
app.autodiscover_tasks(lambda: installed_apps, force=True) app.autodiscover_tasks(lambda: installed_apps, force=True)

View File

@ -1,15 +1,18 @@
from django.conf import settings from typing import Any
from allauth.account.adapter import DefaultAccountAdapter from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from django.conf import settings
from django.http import HttpRequest
class AccountAdapter(DefaultAccountAdapter): class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request): def is_open_for_signup(self, request: HttpRequest):
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
class SocialAccountAdapter(DefaultSocialAccountAdapter): class SocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin): def is_open_for_signup(self, request: HttpRequest, sociallogin: Any):
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)

View File

@ -1,39 +1,17 @@
from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin from django.contrib.auth import admin as auth_admin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.contrib.auth import get_user_model
from .models import User
from {{ cookiecutter.project_slug }}.users.forms import UserChangeForm, UserCreationForm
class MyUserChangeForm(UserChangeForm): User = get_user_model()
class Meta(UserChangeForm.Meta):
model = User
class MyUserCreationForm(UserCreationForm):
error_message = UserCreationForm.error_messages.update(
{"duplicate_username": "This username has already been taken."}
)
class Meta(UserCreationForm.Meta):
model = User
def clean_username(self):
username = self.cleaned_data["username"]
try:
User.objects.get(username=username)
except User.DoesNotExist:
return username
raise forms.ValidationError(self.error_messages["duplicate_username"])
@admin.register(User) @admin.register(User)
class MyUserAdmin(AuthUserAdmin): class UserAdmin(auth_admin.UserAdmin):
form = MyUserChangeForm
add_form = MyUserCreationForm form = UserChangeForm
fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets add_form = UserCreationForm
list_display = ("username", "name", "is_superuser") fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets
list_display = ["username", "name", "is_superuser"]
search_fields = ["name"] search_fields = ["name"]

View File

@ -1,15 +1,12 @@
from django.apps import AppConfig from django.apps import AppConfig
class UsersConfig(AppConfig): class UsersAppConfig(AppConfig):
name = "{{cookiecutter.project_slug}}.users"
name = "{{ cookiecutter.project_slug }}.users"
verbose_name = "Users" verbose_name = "Users"
def ready(self): def ready(self):
"""Override this to put in:
Users system checks
Users signal registration
"""
try: try:
import users.signals # noqa F401 import users.signals # noqa F401
except ImportError: except ImportError:

View File

@ -0,0 +1,31 @@
from django.contrib.auth import get_user_model, forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
User = get_user_model()
class UserChangeForm(forms.UserChangeForm):
class Meta(forms.UserChangeForm.Meta):
model = User
class UserCreationForm(forms.UserCreationForm):
error_message = forms.UserCreationForm.error_messages.update(
{"duplicate_username": _("This username has already been taken.")}
)
class Meta(forms.UserCreationForm.Meta):
model = User
def clean_username(self):
username = self.cleaned_data["username"]
try:
User.objects.get(username=username)
except User.DoesNotExist:
return username
raise ValidationError(self.error_messages["duplicate_username"])

View File

@ -1,5 +1,5 @@
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db.models import CharField
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -8,10 +8,7 @@ class User(AbstractUser):
# First Name and Last Name do not cover name patterns # First Name and Last Name do not cover name patterns
# around the globe. # around the globe.
name = models.CharField(_("Name of User"), blank=True, max_length=255) name = CharField(_("Name of User"), blank=True, max_length=255)
def __str__(self):
return self.username
def get_absolute_url(self): def get_absolute_url(self):
return reverse("users:detail", kwargs={"username": self.username}) return reverse("users:detail", kwargs={"username": self.username})

View File

@ -1,11 +1,29 @@
import factory from typing import Any, Sequence
from django.contrib.auth import get_user_model
from factory import DjangoModelFactory, Faker, post_generation
class UserFactory(factory.django.DjangoModelFactory): class UserFactory(DjangoModelFactory):
username = factory.Sequence(lambda n: f"user-{n}")
email = factory.Sequence(lambda n: f"user-{n}@example.com") username = Faker("user_name")
password = factory.PostGenerationMethodCall("set_password", "password") email = Faker("email")
name = Faker("name")
@post_generation
def password(self, create: bool, extracted: Sequence[Any], **kwargs):
password = Faker(
"password",
length=42,
special_chars=True,
digits=True,
upper_case=True,
lower_case=True,
).generate(
extra_kwargs={}
)
self.set_password(password)
class Meta: class Meta:
model = "users.User" model = get_user_model()
django_get_or_create = ("username",) django_get_or_create = ["username"]

View File

@ -1,44 +0,0 @@
from test_plus.test import TestCase
from ..admin import MyUserCreationForm
class TestMyUserCreationForm(TestCase):
def setUp(self):
self.user = self.make_user("notalamode", "notalamodespassword")
def test_clean_username_success(self):
# Instantiate the form with a new username
form = MyUserCreationForm(
{
"username": "alamode",
"password1": "7jefB#f@Cc7YJB]2v",
"password2": "7jefB#f@Cc7YJB]2v",
}
)
# Run is_valid() to trigger the validation
valid = form.is_valid()
self.assertTrue(valid)
# Run the actual clean_username method
username = form.clean_username()
self.assertEqual("alamode", username)
def test_clean_username_false(self):
# Instantiate the form with the same username as self.user
form = MyUserCreationForm(
{
"username": self.user.username,
"password1": "notalamodespassword",
"password2": "notalamodespassword",
}
)
# Run is_valid() to trigger the validation, which is going to fail
# because the username is already taken
valid = form.is_valid()
self.assertFalse(valid)
# The form.errors dict should contain a single error called 'username'
self.assertTrue(len(form.errors) == 1)
self.assertTrue("username" in form.errors)

View File

@ -0,0 +1,41 @@
import pytest
from {{ cookiecutter.project_slug }}.users.forms import UserCreationForm
from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory
pytestmark = pytest.mark.django_db
class TestUserCreationForm:
def test_clean_username(self):
# A user with proto_user params does not exist yet.
proto_user = UserFactory.build()
form = UserCreationForm(
{
"username": proto_user.username,
"password1": proto_user._password,
"password2": proto_user._password,
}
)
assert form.is_valid()
assert form.clean_username() == proto_user.username
# Creating a user.
form.save()
# The user with proto_user params already exists,
# hence cannot be created.
form = UserCreationForm(
{
"username": proto_user.username,
"password1": proto_user._password,
"password2": proto_user._password,
}
)
assert not form.is_valid()
assert len(form.errors) == 1
assert "username" in form.errors

View File

@ -1,16 +1,8 @@
from test_plus.test import TestCase import pytest
from django.conf import settings
pytestmark = pytest.mark.django_db
class TestUser(TestCase): def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL):
assert user.get_absolute_url() == f"/users/{user.username}/"
def setUp(self):
self.user = self.make_user()
def test__str__(self):
self.assertEqual(
self.user.__str__(),
"testuser", # This is the default username for self.make_user()
)
def test_get_absolute_url(self):
self.assertEqual(self.user.get_absolute_url(), "/users/testuser/")

View File

@ -1,44 +1,28 @@
import pytest
from django.conf import settings
from django.urls import reverse, resolve from django.urls import reverse, resolve
from test_plus.test import TestCase pytestmark = pytest.mark.django_db
class TestUserURLs(TestCase): def test_detail(user: settings.AUTH_USER_MODEL):
"""Test URL patterns for users app.""" assert (
reverse("users:detail", kwargs={"username": user.username})
== f"/users/{user.username}/"
)
assert resolve(f"/users/{user.username}/").view_name == "users:detail"
def setUp(self):
self.user = self.make_user()
def test_list_reverse(self): def test_list():
"""users:list should reverse to /users/.""" assert reverse("users:list") == "/users/"
self.assertEqual(reverse("users:list"), "/users/") assert resolve("/users/").view_name == "users:list"
def test_list_resolve(self):
"""/users/ should resolve to users:list."""
self.assertEqual(resolve("/users/").view_name, "users:list")
def test_redirect_reverse(self): def test_update():
"""users:redirect should reverse to /users/~redirect/.""" assert reverse("users:update") == "/users/~update/"
self.assertEqual(reverse("users:redirect"), "/users/~redirect/") assert resolve("/users/~update/").view_name == "users:update"
def test_redirect_resolve(self):
"""/users/~redirect/ should resolve to users:redirect."""
self.assertEqual(resolve("/users/~redirect/").view_name, "users:redirect")
def test_detail_reverse(self): def test_redirect():
"""users:detail should reverse to /users/testuser/.""" assert reverse("users:redirect") == "/users/~redirect/"
self.assertEqual( assert resolve("/users/~redirect/").view_name == "users:redirect"
reverse("users:detail", kwargs={"username": "testuser"}), "/users/testuser/"
)
def test_detail_resolve(self):
"""/users/testuser/ should resolve to users:detail."""
self.assertEqual(resolve("/users/testuser/").view_name, "users:detail")
def test_update_reverse(self):
"""users:update should reverse to /users/~update/."""
self.assertEqual(reverse("users:update"), "/users/~update/")
def test_update_resolve(self):
"""/users/~update/ should resolve to users:update."""
self.assertEqual(resolve("/users/~update/").view_name, "users:update")

View File

@ -1,52 +1,53 @@
import pytest
from django.conf import settings
from django.test import RequestFactory from django.test import RequestFactory
from test_plus.test import TestCase from {{ cookiecutter.project_slug }}.users.views import UserRedirectView, UserUpdateView
from ..views import UserRedirectView, UserUpdateView pytestmark = pytest.mark.django_db
class BaseUserTestCase(TestCase): class TestUserUpdateView:
"""
TODO:
extracting view initialization code as class-scoped fixture
would be great if only pytest-django supported non-function-scoped
fixture db access -- this is a work-in-progress for now:
https://github.com/pytest-dev/pytest-django/pull/258
"""
def setUp(self): def test_get_success_url(
self.user = self.make_user() self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
self.factory = RequestFactory() ):
view = UserUpdateView()
request = request_factory.get("/fake-url/")
request.user = user
class TestUserRedirectView(BaseUserTestCase):
def test_get_redirect_url(self):
# Instantiate the view directly. Never do this outside a test!
view = UserRedirectView()
# Generate a fake request
request = self.factory.get("/fake-url")
# Attach the user to the request
request.user = self.user
# Attach the request to the view
view.request = request view.request = request
# Expect: '/users/testuser/', as that is the default username for
# self.make_user() assert view.get_success_url() == f"/users/{user.username}/"
self.assertEqual(view.get_redirect_url(), "/users/testuser/")
def test_get_object(
self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
):
view = UserUpdateView()
request = request_factory.get("/fake-url/")
request.user = user
view.request = request
assert view.get_object() == user
class TestUserUpdateView(BaseUserTestCase): class TestUserRedirectView:
def setUp(self): def test_get_redirect_url(
# call BaseUserTestCase.setUp() self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
super(TestUserUpdateView, self).setUp() ):
# Instantiate the view directly. Never do this outside a test! view = UserRedirectView()
self.view = UserUpdateView() request = request_factory.get("/fake-url")
# Generate a fake request request.user = user
request = self.factory.get("/fake-url")
# Attach the user to the request
request.user = self.user
# Attach the request to the view
self.view.request = request
def test_get_success_url(self): view.request = request
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
self.assertEqual(self.view.get_success_url(), "/users/testuser/")
def test_get_object(self): assert view.get_redirect_url() == f"/users/{user.username}/"
# Expect: self.user, as that is the request's user object
self.assertEqual(self.view.get_object(), self.user)

View File

@ -1,15 +1,16 @@
from django.urls import path from django.urls import path
from . import views from {{ cookiecutter.project_slug }}.users.views import (
user_list_view,
user_redirect_view,
user_update_view,
user_detail_view,
)
app_name = "users" app_name = "users"
urlpatterns = [ urlpatterns = [
path("", view=views.UserListView.as_view(), name="list"), path("", view=user_list_view, name="list"),
path("~redirect/", view=views.UserRedirectView.as_view(), name="redirect"), path("~redirect/", view=user_redirect_view, name="redirect"),
path("~update/", view=views.UserUpdateView.as_view(), name="update"), path("~update/", view=user_update_view, name="update"),
path( path("<str:username>/", view=user_detail_view, name="detail"),
"<str:username>",
view=views.UserDetailView.as_view(),
name="detail",
),
] ]

View File

@ -1,43 +1,52 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse from django.urls import reverse
from django.views.generic import DetailView, ListView, RedirectView, UpdateView from django.views.generic import DetailView, ListView, RedirectView, UpdateView
from .models import User User = get_user_model()
class UserDetailView(LoginRequiredMixin, DetailView): class UserDetailView(LoginRequiredMixin, DetailView):
model = User model = User
# These next two lines tell the view to index lookups by username
slug_field = "username" slug_field = "username"
slug_url_kwarg = "username" slug_url_kwarg = "username"
user_detail_view = UserDetailView.as_view()
class UserListView(LoginRequiredMixin, ListView):
model = User
slug_field = "username"
slug_url_kwarg = "username"
user_list_view = UserListView.as_view()
class UserUpdateView(LoginRequiredMixin, UpdateView):
model = User
fields = ["name"]
def get_success_url(self):
return reverse("users:detail", kwargs={"username": self.request.user.username})
def get_object(self):
return User.objects.get(username=self.request.user.username)
user_update_view = UserUpdateView.as_view()
class UserRedirectView(LoginRequiredMixin, RedirectView): class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False permanent = False
def get_redirect_url(self): def get_redirect_url(self):
return reverse("users:detail", kwargs={"username": self.request.user.username}) return reverse("users:detail", kwargs={"username": self.request.user.username})
class UserUpdateView(LoginRequiredMixin, UpdateView): user_redirect_view = UserRedirectView.as_view()
fields = ["name"]
# we already imported User in the view code above, remember?
model = User
# send the user back to their own page after a successful update
def get_success_url(self):
return reverse("users:detail", kwargs={"username": self.request.user.username})
def get_object(self):
# Only get the User record for the user making the request
return User.objects.get(username=self.request.user.username)
class UserListView(LoginRequiredMixin, ListView):
model = User
# These next two lines tell the view to index lookups by username
slug_field = "username"
slug_url_kwarg = "username"