Merge commit '4b6e2b503a4fdc9739574c78b7f953c18def855a'

This commit is contained in:
Trung Dong Huynh 2018-08-29 17:28:29 +01:00
commit 7510e009e6
49 changed files with 522 additions and 428 deletions

View File

@ -87,7 +87,9 @@ 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`_
Diane Chen `@purplediane`_ @purplediane88
Dong Huynh `@trungdong`_ Dong Huynh `@trungdong`_
Emanuel Calso `@bloodpet`_ @bloodpet Emanuel Calso `@bloodpet`_ @bloodpet
Eraldo Energy `@eraldo`_ Eraldo Energy `@eraldo`_
@ -98,11 +100,13 @@ Listed in alphabetical order.
Garry Polley `@garrypolley`_ Garry Polley `@garrypolley`_
Hamish Durkin `@durkode`_ Hamish Durkin `@durkode`_
Harry Percival `@hjwp`_ Harry Percival `@hjwp`_
Hendrik Schneider `@hendrikschneider`_
Henrique G. G. Pereira `@ikkebr`_ Henrique G. G. Pereira `@ikkebr`_
Ian Lee `@IanLee1521`_ Ian Lee `@IanLee1521`_
Jan Van Bruggen `@jvanbrug`_ Jan Van Bruggen `@jvanbrug`_
Jens Nilsson `@phiberjenz`_ Jens Nilsson `@phiberjenz`_
Jimmy Gitonga `@afrowave`_ @afrowave Jimmy Gitonga `@afrowave`_ @afrowave
John Cass `@jcass77`_ @cass_john
Julien Almarcha `@sladinji`_ Julien Almarcha `@sladinji`_
Julio Castillo `@juliocc`_ Julio Castillo `@juliocc`_
Kaido Kert `@kaidokert`_ Kaido Kert `@kaidokert`_
@ -121,6 +125,7 @@ Listed in alphabetical order.
Malik Sulaimanov `@flyudvik`_ @flyudvik Malik Sulaimanov `@flyudvik`_ @flyudvik
Martin Blech Martin Blech
Martin Saizar `@msaizar`_ Martin Saizar `@msaizar`_
Mateusz Ostaszewski `@mostaszewski`_
Mathijs Hoogland `@MathijsHoogland`_ Mathijs Hoogland `@MathijsHoogland`_
Matt Braymer-Hayes `@mattayes`_ @mattayes Matt Braymer-Hayes `@mattayes`_ @mattayes
Matt Linares Matt Linares
@ -161,6 +166,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
@ -172,6 +178,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
@ -206,6 +213,7 @@ Listed in alphabetical order.
.. _@goldhand: https://github.com/goldhand .. _@goldhand: https://github.com/goldhand
.. _@hackebrot: https://github.com/hackebrot .. _@hackebrot: https://github.com/hackebrot
.. _@hairychris: https://github.com/hairychris .. _@hairychris: https://github.com/hairychris
.. _@hendrikschneider https://github.com/hendrikschneider
.. _@hjwp: https://github.com/hjwp .. _@hjwp: https://github.com/hjwp
.. _@IanLee1521: https://github.com/IanLee1521 .. _@IanLee1521: https://github.com/IanLee1521
.. _@ikkebr: https://github.com/ikkebr .. _@ikkebr: https://github.com/ikkebr
@ -223,6 +231,7 @@ Listed in alphabetical order.
.. _@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
.. _@mostaszewski: https://github.com/mostaszewski
.. _@mfwarren: https://github.com/mfwarren .. _@mfwarren: https://github.com/mfwarren
.. _@mimischi: https://github.com/mimischi .. _@mimischi: https://github.com/mimischi
.. _@mjsisley: https://github.com/mjsisley .. _@mjsisley: https://github.com/mjsisley
@ -263,7 +272,8 @@ 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
.. _@purplediane: https://github.com/purplediane
Special Thanks Special Thanks
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -41,7 +41,7 @@ Features
* For Django 2.0 * For Django 2.0
* Works with Python 3.6 * Works with Python 3.6
* Renders Django projects with 100% starting test coverage * Renders Django projects with 100% starting test coverage
* Twitter Bootstrap_ v4.0.0 (`maintained Foundation fork`_ also available) * Twitter Bootstrap_ v4.1.1 (`maintained Foundation fork`_ also available)
* 12-Factor_ based settings via django-environ_ * 12-Factor_ based settings via django-environ_
* Secure by default. We believe in SSL. * Secure by default. We believe in SSL.
* Optimized development and production settings * Optimized development and production settings
@ -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/

View File

@ -21,17 +21,27 @@ Run these commands to deploy the project to Heroku:
heroku addons:create sentry:f1 heroku addons:create sentry:f1
heroku config:set PYTHONHASHSEED=random heroku config:set PYTHONHASHSEED=random
heroku config:set WEB_CONCURRENCY=4 heroku config:set WEB_CONCURRENCY=4
heroku config:set DJANGO_DEBUG=False heroku config:set DJANGO_DEBUG=False
heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production
heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)" 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": # 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_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A-HJ-NP-Za-km-z2-9' | head -c 32)/"
heroku config:set DJANGO_ALLOWED_HOSTS= # Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
# Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
heroku config:set DJANGO_AWS_ACCESS_KEY_ID= # Assign with AWS_ACCESS_KEY_ID heroku config:set DJANGO_ALLOWED_HOSTS=
heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY= # Assign with AWS_SECRET_ACCESS_KEY
heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME= # Assign with AWS_STORAGE_BUCKET_NAME # 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

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
@ -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

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

@ -93,7 +93,8 @@ use_travisci:
keep_local_envs_in_vcs: 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).
Note: .env(s) are only utilized when Docker Compose and/or Heroku support is enabled.
debug: debug:
Indicates whether the project should be configured for debugging. Indicates whether the project should be configured for debugging.

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.2.1
pytest==3.6.1 pytest==3.7.3
pytest-cookies==0.3.0 pytest-cookies==0.3.0

View File

@ -14,8 +14,11 @@ cd .cache/docker
cookiecutter ../../ --no-input --overwrite-if-exists use_docker=y cookiecutter ../../ --no-input --overwrite-if-exists use_docker=y
cd my_awesome_project cd my_awesome_project
# run the project's type checks
docker-compose -f local.yml run django mypy my_awesome_project
# run the project's tests # run the project's tests
docker-compose -f local.yml run django python manage.py test docker-compose -f local.yml run django pytest
# return non-zero status code if there are migrations that have not been created # return non-zero status code if there are migrations that have not been created
docker-compose -f local.yml run django python manage.py makemigrations --dry-run --check || { echo "ERROR: there were changes in the models, but migration listed above have not been created and are not saved in version control"; exit 1; } docker-compose -f local.yml run django python manage.py makemigrations --dry-run --check || { echo "ERROR: there were changes in the models, but migration listed above have not been created and are not saved in version control"; exit 1; }

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

@ -43,3 +43,11 @@ SENTRY_DSN=
# 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

@ -1,18 +1,14 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - all" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true"> <configuration default="false" name="pytest: ." type="tests" factoryName="py.test" singleton="true">
<module name="{{ cookiecutter.project_slug }}" /> <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>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="" /> <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="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" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<PathMappingSettings> <PathMappingSettings>
<option name="pathMappings"> <option name="pathMappings">
<list> <list>
@ -20,11 +16,10 @@
</list> </list>
</option> </option>
</PathMappingSettings> </PathMappingSettings>
<option name="TARGET" value="." /> <option name="_new_keywords" value="&quot;&quot;" />
<option name="SETTINGS_FILE" value="" /> <option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="CUSTOM_SETTINGS" value="false" /> <option name="_new_target" value="&quot;.&quot;" />
<option name="USE_OPTIONS" value="false" /> <option name="_new_targetType" value="&quot;PATH&quot;" />
<option name="OPTIONS" value="" />
<method /> <method />
</configuration> </configuration>
</component> </component>

View File

@ -1,18 +1,14 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - module: users" type="DjangoTestsConfigurationType" factoryName="Django tests" singleton="true"> <configuration default="false" name="pytest: users" type="tests" factoryName="py.test" singleton="true">
<module name="{{ cookiecutter.project_slug }}" /> <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>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="DJANGO_SETTINGS_MODULE" value="config.settings.test" />
</envs>
<option name="SDK_HOME" value="" /> <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="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" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" enabled="false" sample_coverage="true" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<PathMappingSettings> <PathMappingSettings>
<option name="pathMappings"> <option name="pathMappings">
<list> <list>
@ -20,11 +16,10 @@
</list> </list>
</option> </option>
</PathMappingSettings> </PathMappingSettings>
<option name="TARGET" value="{{ cookiecutter.project_slug }}.users" /> <option name="_new_keywords" value="&quot;&quot;" />
<option name="SETTINGS_FILE" value="" /> <option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="CUSTOM_SETTINGS" value="false" /> <option name="_new_target" value="&quot;./{{ cookiecutter.project_slug }}/users/&quot;" />
<option name="USE_OPTIONS" value="false" /> <option name="_new_targetType" value="&quot;PATH&quot;" />
<option name="OPTIONS" value="" />
<method /> <method />
</configuration> </configuration>
</component> </component>

View File

@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - class: TestUser" type="DjangoTestsConfigurationType" factoryName="Django tests" 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.test" />
</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" />
<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="tests - file: test_models" type="DjangoTestsConfigurationType" factoryName="Django tests" 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.test" />
</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" />
<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="tests - specific: test_get_absolute_url" type="DjangoTestsConfigurationType" factoryName="Django tests" 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.test" />
</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" />
<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

@ -32,12 +32,21 @@ Setting Up Your Users
For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users. For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.
Type checks
^^^^^^^^^^^
Running type checks with mypy:
::
$ mypy {{cookiecutter.project_slug}}
Test coverage Test coverage
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
To run the tests, check your test coverage, and generate an HTML coverage report:: To run the tests, check your test coverage, and generate an HTML coverage report::
$ coverage run manage.py test $ coverage run -m pytest
$ coverage html $ coverage html
$ open htmlcov/index.html $ open htmlcov/index.html
@ -46,7 +55,7 @@ Running tests with py.test
:: ::
$ py.test $ pytest
Live reloading and Sass CSS compilation Live reloading and Sass CSS compilation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -138,7 +147,7 @@ Custom Bootstrap Compilation
^^^^^^ ^^^^^^
The generated CSS is set up with automatic Bootstrap recompilation with variables of your choice. The generated CSS is set up with automatic Bootstrap recompilation with variables of your choice.
Bootstrap v4 is installed using npm and customised by tweaking your variables in ``static/sass/custom_bootstrap_vars``. Bootstrap v4.1.1 is installed using npm and customised by tweaking your variables in ``static/sass/custom_bootstrap_vars``.
You can find a list of available variables `in the bootstrap source`_, or get explanations on them in the `Bootstrap docs`_. You can find a list of available variables `in the bootstrap source`_, or get explanations on them in the `Bootstrap docs`_.
@ -147,6 +156,6 @@ Bootstrap's javascript as well as its dependencies is concatenated into a single
{% endif %} {% endif %}
.. _in the bootstrap source: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss .. _in the bootstrap source: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss
.. _Bootstrap docs: https://getbootstrap.com/docs/4.0/getting-started/theming/ .. _Bootstrap docs: https://getbootstrap.com/docs/4.1/getting-started/theming/
{% endif %} {% endif %}

View File

@ -34,6 +34,10 @@ RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat RUN sed -i 's/\r//' /start-celerybeat
RUN chmod +x /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 %} {% endif %}
WORKDIR /app WORKDIR /app

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

@ -38,6 +38,10 @@ COPY ./compose/production/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat RUN sed -i 's/\r//' /start-celerybeat
RUN chmod +x /start-celerybeat RUN chmod +x /start-celerybeat
RUN chown django /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

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

@ -229,19 +229,25 @@ MANAGERS = ADMINS
# Celery # Celery
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
INSTALLED_APPS += ['{{cookiecutter.project_slug}}.taskapp.celery.CeleryAppConfig'] 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

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 %}
@ -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' %}
@ -71,4 +71,11 @@ services:
ports: [] ports: []
command: /start-celerybeat command: /start-celerybeat
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_local_flower
ports:
- "5555:5555"
command: /start-flower
{%- endif %} {%- endif %}

View File

@ -5,7 +5,7 @@
"devDependencies": { "devDependencies": {
{% if cookiecutter.js_task_runner == 'Gulp' %} {% if cookiecutter.js_task_runner == 'Gulp' %}
{% if cookiecutter.custom_bootstrap_compilation == 'y' %} {% if cookiecutter.custom_bootstrap_compilation == 'y' %}
"bootstrap": "^4.0.0", "bootstrap": "4.1.1",
{% endif %} {% endif %}
"browser-sync": "^2.14.0", "browser-sync": "^2.14.0",
"del": "^2.2.2", "del": "^2.2.2",
@ -23,8 +23,8 @@
"gulp-uglify": "^3.0.0", "gulp-uglify": "^3.0.0",
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
{% if cookiecutter.custom_bootstrap_compilation == 'y' %} {% if cookiecutter.custom_bootstrap_compilation == 'y' %}
"jquery": "^3.2.1-slim", "jquery": "3.3.1-slim",
"popper.js": "^1.12.3", "popper.js": "1.14.3",
{% endif %} {% endif %}
"run-sequence": "^2.1.1" "run-sequence": "^2.1.1"
{% 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: {}
caddy: {} production_caddy: {}
services: services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %} django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
@ -25,8 +25,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
@ -38,7 +38,7 @@ services:
depends_on: depends_on:
- django - django
volumes: volumes:
- caddy:/root/.caddy - production_caddy:/root/.caddy
env_file: env_file:
- ./.envs/.production/.caddy - ./.envs/.production/.caddy
ports: ports:
@ -59,4 +59,11 @@ services:
image: {{ cookiecutter.project_slug }}_production_celerybeat image: {{ cookiecutter.project_slug }}_production_celerybeat
command: /start-celerybeat command: /start-celerybeat
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_production_flower
ports:
- "5555:5555"
command: /start-flower
{%- endif %} {%- endif %}

View File

@ -1,24 +1,27 @@
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 %}
argon2-cffi==18.1.0 # https://github.com/hynek/argon2_cffi argon2-cffi==18.3.0 # https://github.com/hynek/argon2_cffi
{%- if cookiecutter.use_whitenoise == 'y' %} {%- if cookiecutter.use_whitenoise == 'y' %}
whitenoise==3.3.1 # https://github.com/evansd/whitenoise whitenoise==4.0 # 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.26.post2 # pyup: <4.0 # https://github.com/celery/celery celery==4.2.1 # 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.6 # pyup: < 2.1 # https://www.djangoproject.com/ django==2.0.8 # 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.37.1 # 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
{%- if cookiecutter.use_compressor == "y" %} {%- if cookiecutter.use_compressor == "y" %}
django-compressor==2.2 # https://github.com/django-compressor/django-compressor django-compressor==2.2 # https://github.com/django-compressor/django-compressor

View File

@ -2,7 +2,7 @@
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.5 # https://github.com/sphinx-doc/sphinx Sphinx==1.7.8 # 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 %}
@ -11,7 +11,8 @@ psycopg2-binary==2.7.5 # https://github.com/psycopg/psycopg2
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
pytest==3.6.1 # https://github.com/pytest-dev/pytest mypy==0.620 # https://github.com/python/mypy
pytest==3.7.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 +23,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.1.0 # 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.1.2 # 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.3.0 # https://github.com/pytest-dev/pytest-django pytest-django==3.4.2 # https://github.com/pytest-dev/pytest-django

View File

@ -14,4 +14,4 @@ raven==6.9.0 # https://github.com/getsentry/raven-python
# 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[mailgun]==3.0 # https://github.com/anymail/django-anymail django-anymail[mailgun]==4.1 # https://github.com/anymail/django-anymail

View File

@ -1 +1 @@
python-3.6.5 python-3.6.6

View File

@ -5,3 +5,17 @@ exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
[pycodestyle] [pycodestyle]
max-line-length = 120 max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
[mypy]
python_version = 3.6
check_untyped_defs = True
ignore_errors = False
ignore_missing_imports = True
strict_optional = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True

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

@ -1,21 +1 @@
/* Project specific Javascript goes here. */ /* Project specific Javascript goes here. */
/*
Formatting hack to get around crispy-forms unfortunate hardcoding
in helpers.FormHelper:
if template_pack == 'bootstrap4':
grid_colum_matcher = re.compile('\w*col-(xs|sm|md|lg|xl)-\d+\w*')
using_grid_layout = (grid_colum_matcher.match(self.label_class) or
grid_colum_matcher.match(self.field_class))
if using_grid_layout:
items['using_grid_layout'] = True
Issues with the above approach:
1. Fragile: Assumes Bootstrap 4's API doesn't change (it does)
2. Unforgiving: Doesn't allow for any variation in template design
3. Really Unforgiving: No way to override this behavior
4. Undocumented: No mention in the documentation, or it's too hard for me to find
*/
$('.form-group').removeClass('row');

View File

@ -20,7 +20,9 @@ class CeleryAppConfig(AppConfig):
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

@ -17,8 +17,8 @@
{% block css %} {% block css %}
{% endraw %}{% if cookiecutter.custom_bootstrap_compilation == "n" %}{% raw %} {% endraw %}{% if cookiecutter.custom_bootstrap_compilation == "n" %}{% raw %}
<!-- Latest compiled and minified Bootstrap 4 beta CSS --> <!-- Latest compiled and minified Bootstrap 4.1.1 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
{% endraw %}{% endif %}{% raw %} {% endraw %}{% endif %}{% raw %}
<!-- Your stuff: Third-party CSS libraries go here --> <!-- Your stuff: Third-party CSS libraries go here -->
@ -102,10 +102,10 @@
<script src="{% static 'js/vendors.js' %}"></script> <script src="{% static 'js/vendors.js' %}"></script>
{% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% endcompress %}{% endraw %}{% endif %}{% raw %} {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% endcompress %}{% endraw %}{% endif %}{% raw %}
{% endraw %}{% else %}{% raw %} {% endraw %}{% else %}{% raw %}
<!-- Required by Bootstrap v4 beta --> <!-- Required by Bootstrap v4.1.1 -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<!-- Your stuff: Third-party javascript libraries go here --> <!-- Your stuff: Third-party javascript libraries go here -->
{% endraw %}{% endif %}{% raw %} {% endraw %}{% endif %}{% raw %}

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

@ -2,14 +2,11 @@ from django.apps import AppConfig
class UsersAppConfig(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"