mirror of
https://github.com/cookiecutter/cookiecutter-django.git
synced 2024-11-28 04:24:02 +03:00
Merge commit '4b6e2b503a4fdc9739574c78b7f953c18def855a'
This commit is contained in:
commit
7510e009e6
|
@ -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
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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/
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
||||||
heroku config:set DJANGO_AWS_ACCESS_KEY_ID= # Assign with AWS_ACCESS_KEY_ID
|
# Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
|
||||||
heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY= # Assign with AWS_SECRET_ACCESS_KEY
|
heroku config:set DJANGO_ALLOWED_HOSTS=
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -94,6 +94,7 @@ 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.
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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="""" />
|
||||||
<option name="SETTINGS_FILE" value="" />
|
<option name="_new_additionalArguments" value="""" />
|
||||||
<option name="CUSTOM_SETTINGS" value="false" />
|
<option name="_new_target" value=""."" />
|
||||||
<option name="USE_OPTIONS" value="false" />
|
<option name="_new_targetType" value=""PATH"" />
|
||||||
<option name="OPTIONS" value="" />
|
|
||||||
<method />
|
<method />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
|
@ -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="""" />
|
||||||
<option name="SETTINGS_FILE" value="" />
|
<option name="_new_additionalArguments" value="""" />
|
||||||
<option name="CUSTOM_SETTINGS" value="false" />
|
<option name="_new_target" value=""./{{ cookiecutter.project_slug }}/users/"" />
|
||||||
<option name="USE_OPTIONS" value="false" />
|
<option name="_new_targetType" value=""PATH"" />
|
||||||
<option name="OPTIONS" value="" />
|
|
||||||
<method />
|
<method />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}"
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}"
|
|
@ -229,12 +229,12 @@ 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 = '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
|
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content
|
||||||
CELERY_ACCEPT_CONTENT = ['json']
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
@ -242,6 +242,12 @@ CELERY_ACCEPT_CONTENT = ['json']
|
||||||
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
|
||||||
|
|
|
@ -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...
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
python-3.6.5
|
python-3.6.6
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
@ -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');
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"])
|
|
@ -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})
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
@ -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/")
|
|
||||||
|
|
|
@ -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})
|
||||||
def setUp(self):
|
== f"/users/{user.username}/"
|
||||||
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/"
|
|
||||||
)
|
)
|
||||||
|
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):
|
def test_list():
|
||||||
"""users:update should reverse to /users/~update/."""
|
assert reverse("users:list") == "/users/"
|
||||||
self.assertEqual(reverse("users:update"), "/users/~update/")
|
assert resolve("/users/").view_name == "users:list"
|
||||||
|
|
||||||
def test_update_resolve(self):
|
|
||||||
"""/users/~update/ should resolve to users:update."""
|
def test_update():
|
||||||
self.assertEqual(resolve("/users/~update/").view_name, "users: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"
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user