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.
* **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)
## [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

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
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json

View File

@ -1,5 +1,3 @@
# Config file for automatic testing at travis-ci.org
sudo: required
services:

View File

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

View File

@ -65,7 +65,7 @@ Optional Integrations
*These features can be enabled during initial project setup.*
* 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 Sentry_ for error logging
@ -78,6 +78,7 @@ Optional Integrations
.. _Mailgun: http://www.mailgun.com/
.. _Whitenoise: https://whitenoise.readthedocs.io/
.. _Celery: http://www.celeryproject.org/
.. _Flower: https://github.com/mher/flower
.. _Anymail: https://github.com/anymail/django-anymail
.. _MailHog: https://github.com/mailhog/MailHog
.. _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:
* 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:
@ -158,13 +161,13 @@ Answer the prompts with your own desired options_. For example::
domain_name [example.com]: myreddit.com
version [0.1.0]: 0.0.1
timezone [UTC]: America/Los_Angeles
use_whitenoise [y]: n
use_whitenoise [n]: n
use_celery [n]: y
use_mailhog [n]: n
use_sentry [y]: y
use_sentry [n]: y
use_pycharm [n]: y
windows [n]: n
use_docker [y]: n
use_docker [n]: n
use_heroku [n]: y
use_compressor [n]: y
Select postgresql_version:
@ -279,6 +282,8 @@ experience better.
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
* `Using Cookiecutter to Jumpstart a Django Project on Windows with PyCharm`_ - May 19, 2017
* `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.
.. _`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/
.. _`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/

View File

@ -18,6 +18,7 @@
"use_pycharm": "n",
"use_docker": "n",
"postgresql_version": [
"10.4",
"10.3",
"10.2",
"10.1",
@ -34,8 +35,8 @@
"use_compressor": "n",
"use_celery": "n",
"use_mailhog": "n",
"use_sentry": "y",
"use_whitenoise": "y",
"use_sentry": "n",
"use_whitenoise": "n",
"use_heroku": "n",
"use_travisci": "n",
"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 addons:create heroku-redis:hobby-dev
heroku addons:create mailgun
heroku config:set WEB_CONCURRENCY=4
# 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)/"
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'
# If using mailgun:
heroku addons:create mailgun:starter
heroku config:set DJANGO_AWS_ACCESS_KEY_ID=YOUR_AWS_ID_HERE
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 addons:create sentry:f1
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
heroku run python manage.py migrate
heroku run python manage.py check --deploy
heroku run python manage.py createsuperuser
heroku run python manage.py collectstatic --no-input
heroku run python manage.py check --deploy
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;
* ``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;
* ``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
@ -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.
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.
@ -70,7 +73,7 @@ You can read more about this here at `Automatic HTTPS`_ in the Caddy docs.
(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
@ -84,6 +87,10 @@ Once this is ready, you can run it with::
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::
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: .
dockerfile: ./compose/production/postgres/Dockerfile
volumes:
- postgres_data_local:/var/lib/postgresql/data
- postgres_backup_local:/backups
- local_postgres_data:/var/lib/postgresql/data
- local_postgres_data_backups:/backups
env_file:
- ./.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``.
.. _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_name [My Awesome Project]:
project_name:
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
and in other places where a Python-importable version of your project name
is needed.
description [Behold My Awesome Project!]
description:
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.
email [daniel-roy-greenfeld@example.com]:
email:
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.
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.
open_source_license [1]
open_source_license:
A software license for the project. The choices are:
1. MIT_
@ -34,19 +34,19 @@ open_source_license [1]
4. `Apache Software License 2.0`_
5. Not open source
timezone [UTC]
timezone:
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.
use_pycharm [n]
use_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`_.
postgresql_version [1]
postgresql_version:
Select a PostgreSQL_ version to use. The choices are:
1. 10.3
@ -57,45 +57,45 @@ postgresql_version [1]
6. 9.4
7. 9.3
js_task_runner [1]
js_task_runner:
Select a JavaScript task runner. The choices are:
1. None
2. Gulp_
custom_bootstrap_compilation [n]
custom_bootstrap_compilation:
Indicates whether the project should support Bootstrap recompilation
via the selected JavaScript task runner's task. This can be useful
for real-time Bootstrap variable alteration.
use_compressor [n]
use_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_.
use_mailhog [n]
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_.
use_whitenoise [y]
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
to Heroku_.
use_travisci [n]
use_travisci:
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
(comes in handy when working in teams where local environment reproducibility
is strongly encouraged).
debug [n]
debug:
Indicates whether the project should be configured for debugging.
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_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_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_LOG_LEVEL SENTRY_LOG_LEVEL n/a logging.INFO
MAILGUN_API_KEY MAILGUN_ACCESS_KEY n/a raises error

View File

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

View File

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

View File

@ -11,7 +11,7 @@ mkdir -p .cache/docker
cd .cache/docker
# 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
# run the project's tests

View File

@ -5,3 +5,11 @@ USE_DOCKER=yes
# Redis
# ------------------------------------------------------------------------------
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' %}
# Sentry
# ------------------------------------------------------------------------------
DJANGO_SENTRY_DSN=
SENTRY_DSN=
{% endif %}
# Redis
# ------------------------------------------------------------------------------
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
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
@ -341,3 +340,5 @@ pip-selfcheck.json
MailHog
{%- endif %}
{{ 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">
<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="PARENT_ENVS" value="true" />
<envs>
@ -10,7 +11,6 @@
<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" />
<option name="SCRIPT_NAME" value="merge_production_dotenvs_in_dotenv.py" />
<option name="PARAMETERS" value="" />

View File

@ -1,17 +1,17 @@
<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="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="SDK_HOME" value="" />
<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_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<PathMappingSettings>
<option name="pathMappings">
<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">
<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="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="SDK_HOME" value="" />
<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_SOURCE_ROOTS" value="true" />
<module name="{{ cookiecutter.project_slug }}" />
<PathMappingSettings>
<option name="pathMappings">
<list>

View File

@ -5,19 +5,32 @@
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<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="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
{% if cookiecutter.js_task_runner != 'None' %}
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/node_modules" />
</content>
{% else %}
<content url="file://$MODULE_DIR$" />
{% endif %}
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings">
<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 name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ RUN apk update \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies
&& apk add libffi-dev openssl-dev py-cffi
&& apk add libffi-dev py-cffi
RUN addgroup -S django \
&& adduser -S -G django django
@ -19,23 +19,29 @@ COPY ./requirements /requirements
RUN pip install --no-cache-dir -r /requirements/production.txt \
&& rm -rf /requirements
COPY ./compose/production/django/gunicorn.sh /gunicorn.sh
RUN sed -i 's/\r//' /gunicorn.sh
RUN chmod +x /gunicorn.sh
RUN chown django /gunicorn.sh
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r//' /entrypoint
RUN chmod +x /entrypoint
RUN chown django /entrypoint
COPY ./compose/production/django/entrypoint.sh /entrypoint.sh
RUN sed -i 's/\r//' /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN chown django /entrypoint.sh
COPY ./compose/production/django/start /start
RUN sed -i 's/\r//' /start
RUN chmod +x /start
RUN chown django /start
{% if cookiecutter.use_celery == "y" %}
COPY ./compose/production/django/celery/worker/start.sh /start-celeryworker.sh
RUN sed -i 's/\r//' /start-celeryworker.sh
RUN chmod +x /start-celeryworker.sh
COPY ./compose/production/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r//' /start-celeryworker
RUN chmod +x /start-celeryworker
RUN chown django /start-celeryworker
COPY ./compose/production/django/celery/beat/start.sh /start-celerybeat.sh
RUN sed -i 's/\r//' /start-celerybeat.sh
RUN chmod +x /start-celerybeat.sh
COPY ./compose/production/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat
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 %}
COPY . /app
@ -45,4 +51,4 @@ USER django
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
cmd="$@"
# N.B. If only .env files supported variable expansion...
export CELERY_BROKER_URL="${REDIS_URL}"
@ -36,12 +34,10 @@ sys.exit(0)
END
}
until postgres_ready; do
>&2 echo 'PostgreSQL is unavailable (sleeping)...'
>&2 echo 'Waiting for PostgreSQL to become available...'
sleep 1
done
>&2 echo 'PostgreSQL is available'
>&2 echo 'PostgreSQL is up - continuing...'
exec $cmd
exec "$@"

View File

@ -75,7 +75,7 @@ THIRD_PARTY_APPS = [
'rest_framework',
]
LOCAL_APPS = [
'{{ cookiecutter.project_slug }}.users.apps.UsersConfig',
'{{ cookiecutter.project_slug }}.users.apps.UsersAppConfig',
# Your stuff: custom apps go here
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -228,20 +228,26 @@ MANAGERS = ADMINS
{% if cookiecutter.use_celery == 'y' -%}
# 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
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
if CELERY_BROKER_URL == 'django://':
CELERY_RESULT_BACKEND = 'redis://'
else:
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content
CELERY_ACCEPT_CONTENT = ['json']
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer
CELERY_TASK_SERIALIZER = 'json'
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer
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 %}
# django-allauth

View File

@ -76,8 +76,10 @@ INSTALLED_APPS += ['django_extensions'] # noqa F405
# Celery
# ------------------------------------------------------------------------------
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_always_eager
CELERY_ALWAYS_EAGER = True
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-always-eager
CELERY_TASK_ALWAYS_EAGER = True
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates
CELERY_TASK_EAGER_PROPAGATES = True
{%- endif %}
# 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
AWS_STORAGE_BUCKET_NAME = env('DJANGO_AWS_STORAGE_BUCKET_NAME')
# 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
# DO NOT change these unless you know what you're doing.
_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}/'
{%- else %}
# 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
MediaRootS3BotoStorage = lambda: S3Boto3Storage(location='media', file_overwrite=False) # noqa
# endregion
@ -158,8 +156,8 @@ INSTALLED_APPS += ['gunicorn'] # noqa F405
# http://whitenoise.evans.io/en/latest/django.html#enable-whitenoise
MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware'] + MIDDLEWARE # noqa F405
{%- endif %}
{% if cookiecutter.use_compressor == 'y' -%}
{% endif %}
{%- if cookiecutter.use_compressor == 'y' -%}
# django-compressor
# ------------------------------------------------------------------------------
# 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
COMPRESS_URL = STATIC_URL
{%- endif %}
{% if cookiecutter.use_whitenoise == 'n' -%}
{% endif %}
{%- if cookiecutter.use_whitenoise == 'n' -%}
# Collectfast
# ------------------------------------------------------------------------------
# https://github.com/antonagestam/collectfast#installation
INSTALLED_APPS = ['collectfast'] + INSTALLED_APPS # noqa F405
AWS_PRELOAD_METADATA = True
{%- endif %}
{% if cookiecutter.use_sentry == 'y' -%}
{% endif %}
{%- if cookiecutter.use_sentry == 'y' -%}
# raven
# ------------------------------------------------------------------------------
# https://docs.sentry.io/clients/python/integrations/django/
@ -187,7 +185,7 @@ MIDDLEWARE = ['raven.contrib.django.raven_compat.middleware.SentryResponseErrorI
# 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')
LOGGING = {
'version': 1,
@ -241,6 +239,7 @@ SENTRY_CELERY_LOGLEVEL = env.int('DJANGO_SENTRY_LOG_LEVEL', logging.INFO)
RAVEN_CONFIG = {
'dsn': SENTRY_DSN
}
{%- else %}
# LOGGING
# ------------------------------------------------------------------------------
@ -290,6 +289,6 @@ LOGGING = {
}
}
{%- endif %}
{% endif %}
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -23,12 +23,10 @@ from django.core.wsgi import get_wsgi_application
app_path = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)), os.pardir))
sys.path.append(os.path.join(app_path, '{{ cookiecutter.project_slug }}'))
{% if cookiecutter.use_sentry == 'y' -%}
if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production':
from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
{%- endif %}
# 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
# 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:
postgres_data_local: {}
postgres_backup_local: {}
local_postgres_data: {}
local_postgres_data_backups: {}
services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
@ -22,7 +22,7 @@ services:
- ./.envs/.local/.postgres
ports:
- "8000:8000"
command: /start.sh
command: /start
postgres:
build:
@ -30,8 +30,8 @@ services:
dockerfile: ./compose/production/postgres/Dockerfile
image: {{ cookiecutter.project_slug }}_production_postgres
volumes:
- postgres_data_local:/var/lib/postgresql/data
- postgres_backup_local:/backups
- local_postgres_data:/var/lib/postgresql/data
- local_postgres_data_backups:/backups
env_file:
- ./.envs/.local/.postgres
{%- if cookiecutter.use_mailhog == 'y' %}
@ -56,11 +56,8 @@ services:
{% if cookiecutter.use_mailhog == 'y' -%}
- mailhog
{%- endif %}
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports: []
command: /start-celeryworker.sh
command: /start-celeryworker
celerybeat:
<<: *django
@ -71,10 +68,14 @@ services:
{% if cookiecutter.use_mailhog == 'y' -%}
- mailhog
{%- endif %}
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports: []
command: /start-celerybeat.sh
command: /start-celerybeat
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_local_flower
ports:
- "5555:5555"
command: /start-flower
{%- endif %}

View File

@ -1,9 +1,9 @@
version: '2'
version: '3'
volumes:
postgres_data: {}
postgres_backup: {}
traefik_acme: {}
production_postgres_data: {}
production_postgres_data_backups: {}
production_traefik: {}
services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
@ -17,7 +17,7 @@ services:
env_file:
- ./.envs/.production/.django
- ./.envs/.production/.postgres
command: /gunicorn.sh
command: /start
labels:
- "traefik.enable=true"
- "traefik.backend=django"
@ -30,8 +30,8 @@ services:
dockerfile: ./compose/production/postgres/Dockerfile
image: {{ cookiecutter.project_slug }}_production_postgres
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres_backup:/backups
- production_postgres_data:/var/lib/postgresql/data
- production_postgres_data_backups:/backups
env_file:
- ./.envs/.production/.postgres
@ -44,11 +44,11 @@ services:
- django
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_acme:/etc/traefik/acme
- production_traefik:/etc/traefik/acme
ports:
- "80:80"
- "443:443"
- "8080:8080"
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
- "0.0.0.0:8080:8080"
redis:
image: redis:3.2
@ -57,26 +57,23 @@ services:
celeryworker:
<<: *django
image: {{ cookiecutter.project_slug }}_production_celeryworker
depends_on:
- postgres
- redis
env_file:
- ./.envs/.production/.django
- ./.envs/.production/.postgres
command: /start-celeryworker.sh
command: /start-celeryworker
labels:
- "traefik.enable=false"
celerybeat:
<<: *django
image: {{ cookiecutter.project_slug }}_production_celerybeat
depends_on:
- postgres
- redis
env_file:
- ./.envs/.production/.django
- ./.envs/.production/.postgres
command: /start-celerybeat.sh
command: /start-celerybeat
labels:
- "traefik.enable=false"
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_production_flower
ports:
- "5555:5555"
command: /start-flower
labels:
- "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
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" %}
rcssmin==1.0.6{% if cookiecutter.windows == 'y' %} --install-option="--without-c-extensions"{% endif %} # https://github.com/ndparker/rcssmin
{%- endif %}
@ -10,13 +10,16 @@ whitenoise==3.3.1 # https://github.com/evansd/whitenoise
{%- endif %}
redis>=2.10.5 # https://github.com/antirez/redis
{%- 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 %}
# Django
# ------------------------------------------------------------------------------
django==2.0.5 # pyup: < 2.1 # https://www.djangoproject.com/
django-environ==0.4.4 # https://github.com/joke2k/django-environ
django==2.0.7 # pyup: < 2.1 # https://www.djangoproject.com/
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-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

View File

@ -2,16 +2,16 @@
Werkzeug==0.14.1 # https://github.com/pallets/werkzeug
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' %}
psycopg2==2.7.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- else %}
psycopg2-binary==2.7.4 # https://github.com/psycopg/psycopg2
psycopg2-binary==2.7.5 # https://github.com/psycopg/psycopg2
{%- endif %}
# 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
# Code quality
@ -22,9 +22,8 @@ coverage==4.5.1 # https://github.com/nedbat/coveragepy
# Django
# ------------------------------------------------------------------------------
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-extensions==2.0.7 # https://github.com/django-extensions/django-extensions
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
{%- endif %}
{%- 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 %}
# Django
# ------------------------------------------------------------------------------
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}}')
class CeleryConfig(AppConfig):
class CeleryAppConfig(AppConfig):
name = '{{cookiecutter.project_slug}}.taskapp'
verbose_name = 'Celery Config'
def ready(self):
# Using a string here means the worker will not have to
# 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()]
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.socialaccount.adapter import DefaultSocialAccountAdapter
from django.conf import settings
from django.http import HttpRequest
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)
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)

View File

@ -1,39 +1,17 @@
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from .models import User
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model
from {{ cookiecutter.project_slug }}.users.forms import UserChangeForm, UserCreationForm
class MyUserChangeForm(UserChangeForm):
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"])
User = get_user_model()
@admin.register(User)
class MyUserAdmin(AuthUserAdmin):
form = MyUserChangeForm
add_form = MyUserCreationForm
fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets
list_display = ("username", "name", "is_superuser")
class UserAdmin(auth_admin.UserAdmin):
form = UserChangeForm
add_form = UserCreationForm
fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets
list_display = ["username", "name", "is_superuser"]
search_fields = ["name"]

View File

@ -1,15 +1,12 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = "{{cookiecutter.project_slug}}.users"
class UsersAppConfig(AppConfig):
name = "{{ cookiecutter.project_slug }}.users"
verbose_name = "Users"
def ready(self):
"""Override this to put in:
Users system checks
Users signal registration
"""
try:
import users.signals # noqa F401
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.db import models
from django.db.models import CharField
from django.urls import reverse
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
# around the globe.
name = models.CharField(_("Name of User"), blank=True, max_length=255)
def __str__(self):
return self.username
name = CharField(_("Name of User"), blank=True, max_length=255)
def get_absolute_url(self):
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):
username = factory.Sequence(lambda n: f"user-{n}")
email = factory.Sequence(lambda n: f"user-{n}@example.com")
password = factory.PostGenerationMethodCall("set_password", "password")
class UserFactory(DjangoModelFactory):
username = Faker("user_name")
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:
model = "users.User"
django_get_or_create = ("username",)
model = get_user_model()
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 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/")
def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL):
assert user.get_absolute_url() == f"/users/{user.username}/"

View File

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

View File

@ -1,52 +1,53 @@
import pytest
from django.conf import settings
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):
self.user = self.make_user()
self.factory = RequestFactory()
def test_get_success_url(
self, user: settings.AUTH_USER_MODEL, request_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
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
self.assertEqual(view.get_redirect_url(), "/users/testuser/")
assert view.get_success_url() == f"/users/{user.username}/"
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):
# call BaseUserTestCase.setUp()
super(TestUserUpdateView, self).setUp()
# Instantiate the view directly. Never do this outside a test!
self.view = UserUpdateView()
# 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
self.view.request = request
def test_get_redirect_url(
self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
):
view = UserRedirectView()
request = request_factory.get("/fake-url")
request.user = user
def test_get_success_url(self):
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
self.assertEqual(self.view.get_success_url(), "/users/testuser/")
view.request = request
def test_get_object(self):
# Expect: self.user, as that is the request's user object
self.assertEqual(self.view.get_object(), self.user)
assert view.get_redirect_url() == f"/users/{user.username}/"

View File

@ -1,15 +1,16 @@
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"
urlpatterns = [
path("", view=views.UserListView.as_view(), name="list"),
path("~redirect/", view=views.UserRedirectView.as_view(), name="redirect"),
path("~update/", view=views.UserUpdateView.as_view(), name="update"),
path(
"<str:username>",
view=views.UserDetailView.as_view(),
name="detail",
),
path("", view=user_list_view, name="list"),
path("~redirect/", view=user_redirect_view, name="redirect"),
path("~update/", view=user_update_view, name="update"),
path("<str:username>/", view=user_detail_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.urls import reverse
from django.views.generic import DetailView, ListView, RedirectView, UpdateView
from .models import User
User = get_user_model()
class UserDetailView(LoginRequiredMixin, DetailView):
model = User
# These next two lines tell the view to index lookups by username
slug_field = "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):
permanent = False
def get_redirect_url(self):
return reverse("users:detail", kwargs={"username": self.request.user.username})
class UserUpdateView(LoginRequiredMixin, UpdateView):
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"
user_redirect_view = UserRedirectView.as_view()