Merge pull request #5 from pydanny/master

Update to Django 3
This commit is contained in:
Jimmy Gitonga 2020-04-24 12:03:39 +03:00 committed by GitHub
commit acf61d2985
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1055 additions and 298 deletions

4
.github/FUNDING.yml vendored
View File

@ -1,7 +1,7 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: danielroygreenfeld
github: pydanny
patreon: roygreenfeld
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

11
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# Add 'docs' to any changes within 'docs' folder or any subfolders
docs:
- 'README.rst'
- 'docs/**/*'
- '{{cookiecutter.project_slug}}/docs/**/*'
# Flag PR related to docker
docker:
- '{{cookiecutter.project_slug}}/compose/**/*'
- '{{cookiecutter.project_slug}}/local.yml'
- '{{cookiecutter.project_slug}}/production.yml'

29
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,29 @@
categories:
- title: 'Breaking Changes'
labels:
- 'breaking'
- title: 'Major Changes'
labels:
- 'major'
- title: 'Minor Changes'
labels:
- 'enhancement'
- title: 'Bugfixes'
labels:
- 'bug'
- title: 'Removals'
labels:
- 'removed'
- title: 'Documentation updates'
labels:
- 'docs'
exclude-labels:
- 'skip-changelog'
- 'update'
- 'project infrastructure'
template: |
## Changes
$CHANGES

14
.github/workflows/draft-release.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Release Drafter
on:
push:
branches:
- master
jobs:
release_notes:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

20
.github/workflows/label.yml vendored Normal file
View File

@ -0,0 +1,20 @@
# This workflow will triage pull requests and apply a label based on the
# paths that are modified in the pull request.
#
# To use this workflow, you will need to set up a .github/labeler.yml
# file with configuration. For more information, see:
# https://github.com/actions/labeler/blob/master/README.md
name: Labeler
on: [pull_request]
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -8,6 +8,11 @@ update: all
# allowed: True, False
pin: True
# add a label to pull requests, default is not set
# requires private repo permissions, even on public repos
# default: empty
label_prs: update
# Specify requirement files by hand, pyup seems to struggle to
# find the ones in the project_slug folder
requirements:

View File

@ -5,7 +5,7 @@ services:
language: python
python: 3.7
python: 3.8
before_install:
- docker-compose -v
@ -14,11 +14,7 @@ before_install:
matrix:
include:
- name: Test results
script: tox -e py37
- name: Run flake8 on result
script: tox -e flake8
- name: Run black on result
script: tox -e black
script: tox -e py38
- name: Black template
script: tox -e black-template
- name: Basic Docker

View File

@ -1,6 +1,36 @@
# Change Log
All enhancements and patches to Cookiecutter Django will be documented in this file.
## [2020-04-13]
### Changed
- Updated to Python 3.8 (@codnee)
- Moved converage config in setup.cfg (@danihodovic)
## [2020-04-08]
### Fixed
- Internal IPs for debug toolbar (@dudanogueira)
## [2020-04-04]
### Fixed
- Added compress command command with Django compressor (@gwiskur)
## [2020-03-23]
### Changed
- Updated project to Django 3.0
## [2020-03-17]
### Changed
- Handle paths using Pathlib (@jules-ch)
### Fixed
- Pre-commit hook regex (@demestav)
## [2020-03-16]
### Added
- Support for all Anymail providers (@Andrew-Chen-Wang)
### Fixed
- Django compressor setup (@jameswilliams1)
## [2020-01-23]
### Changed
- Fix UserFactory to set the password if provided (@BoPeng)

View File

@ -39,9 +39,9 @@ To run all tests using various versions of python in virtualenvs defined in tox.
It is possible to test with a specific version of python. To do this, the command
is::
$ tox -e py37
$ tox -e py38
This will run py.test with the python3.7 interpreter, for example.
This will run py.test with the python3.8 interpreter, for example.
To run a particular test with tox for against your current Python version::

View File

@ -49,6 +49,7 @@ Listed in alphabetical order.
Adam Dobrawy `@ad-m`_
Adam Steele `@adammsteele`_
Agam Dua
Agustín Scaramuzza `@scaramagus`_ @scaramagus
Alberto Sanchez `@alb3rto`_
Alex Tsai `@caffodian`_
Alvaro [Andor] `@andor-pierdelacabeza`_
@ -56,6 +57,7 @@ Listed in alphabetical order.
Andreas Meistad `@ameistad`_
Andres Gonzalez `@andresgz`_
Andrew Mikhnevich `@zcho`_
Andrew Chen Wang `@Andrew-Chen-Wang`_
Andy Rose
Anna Callahan `@jazztpt`_
Anna Sidwell `@takkaria`_
@ -87,6 +89,7 @@ Listed in alphabetical order.
Chris Pappalardo `@ChrisPappalardo`_
Christopher Clarke `@chrisdev`_
Cole Mackenzie `@cmackenzie1`_
Cole Maclean `@cole`_ @cole
Collederas `@Collederas`_
Craig Margieson `@cmargieson`_
Cristian Vargas `@cdvv7788`_
@ -96,6 +99,7 @@ Listed in alphabetical order.
Dani Hodovic `@danihodovic`_
Daniel Hepper `@dhepper`_ @danielhepper
Daniel Hillier `@danifus`_
Daniel Sears `@highpost`_ @highpost
Daniele Tricoli `@eriol`_
David Díaz `@ddiazpinto`_ @DavidDiazPinto
Davit Tovmasyan `@davitovmasyan`_
@ -108,15 +112,20 @@ Listed in alphabetical order.
Diane Chen `@purplediane`_ @purplediane88
Dónal Adams `@epileptic-fish`_
Dong Huynh `@trungdong`_
Duda Nogueira `@dudanogueira`_ @dudanogueira
Emanuel Calso `@bloodpet`_ @bloodpet
Eraldo Energy `@eraldo`_
Eric Groom `@ericgroom`_
Ernesto Cedeno `@codnee`_
Eyad Al Sibai `@eyadsibai`_
Felipe Arruda `@arruda`_
Florian Idelberger `@step21`_ @windrush
Gabriel Mejia `@elgartoinf`_ @elgartoinf
Garry Cairns `@garry-cairns`_
Garry Polley `@garrypolley`_
Gilbishkosma `@Gilbishkosma`_
Glenn Wiskur `@gwiskur`_
Guilherme Guy `@guilherme1guy`_
Hamish Durkin `@durkode`_
Hana Quadara `@hanaquadara`_
Harry Moreno `@morenoh149`_ @morenoh149
@ -128,6 +137,7 @@ Listed in alphabetical order.
Irfan Ahmad `@erfaan`_ @erfaan
Isaac12x `@Isaac12x`_
Ivan Khomutov `@ikhomutov`_
James Williams `@jameswilliams1`_
Jan Van Bruggen `@jvanbrug`_
Jelmer Draaijer `@foarsitter`_
Jerome Caisip `@jeromecaisip`_
@ -135,6 +145,7 @@ Listed in alphabetical order.
Jerome Leclanche `@jleclanche`_ @Adys
Jimmy Gitonga `@afrowave`_ @afrowave
John Cass `@jcass77`_ @cass_john
Jules Cheron `@jules-ch`_
Julien Almarcha `@sladinji`_
Julio Castillo `@juliocc`_
Kaido Kert `@kaidokert`_
@ -175,6 +186,7 @@ Listed in alphabetical order.
Oleg Russkin `@rolep`_
Pablo `@oubiga`_
Parbhat Puri `@parbhat`_
Pawan Chaurasia `@rjsnh1522`_
Peter Bittner `@bittner`_
Peter Coles `@mrcoles`_
Philipp Matthies `@canonnervio`_
@ -190,6 +202,7 @@ Listed in alphabetical order.
Sascha `@saschalalala`_ @saschalalala
Shupeyko Nikita `@webyneter`_
Sławek Ehlert `@slafs`_
Sorasful `@sorasful`_
Srinivas Nyayapati `@shireenrao`_
stepmr `@stepmr`_
Steve Steiner `@ssteinerX`_
@ -205,6 +218,7 @@ Listed in alphabetical order.
Tubo Shi `@Tubo`_
Umair Ashraf `@umrashrf`_ @fabumair
Vadim Iskuchekov `@Egregors`_ @egregors
Vicente G. Reyes `@reyesvicente`_ @highcenburg
Vitaly Babiy
Vivian Guillen `@viviangb`_
Vlad Doster `@vladdoster`_
@ -228,6 +242,7 @@ Listed in alphabetical order.
.. _@andor-pierdelacabeza: https://github.com/andor-pierdelacabeza
.. _@andresgz: https://github.com/andresgz
.. _@antoniablair: https://github.com/antoniablair
.. _@Andrew-Chen-Wang: https://github.com/Andrew-Chen-Wang
.. _@apirobot: https://github.com/apirobot
.. _@archinal: https://github.com/archinal
.. _@areski: https://github.com/areski
@ -258,6 +273,8 @@ Listed in alphabetical order.
.. _@chuckus: https://github.com/chuckus
.. _@cmackenzie1: https://github.com/cmackenzie1
.. _@cmargieson: https://github.com/cmargieson
.. _@codnee: https://github.com/codnee
.. _@cole: https://github.com/cole
.. _@Collederas: https://github.com/Collederas
.. _@curtisstpierre: https://github.com/curtisstpierre
.. _@dadokkio: https://github.com/dadokkio
@ -270,9 +287,12 @@ Listed in alphabetical order.
.. _@dezoito: https://github.com/dezoito
.. _@dhepper: https://github.com/dhepper
.. _@dot2dotseurat: https://github.com/dot2dotseurat
.. _@dudanogueira: https://github.com/dudanogueira
.. _@dsclementsen: https://github.com/dsclementsen
.. _@guilherme1guy: https://github.com/guilherme1guy
.. _@durkode: https://github.com/durkode
.. _@Egregors: https://github.com/Egregors
.. _@elgartoinf: https://gihub.com/elgartoinf
.. _@epileptic-fish: https://gihub.com/epileptic-fish
.. _@eraldo: https://github.com/eraldo
.. _@erfaan: https://github.com/erfaan
@ -284,16 +304,19 @@ Listed in alphabetical order.
.. _@garry-cairns: https://github.com/garry-cairns
.. _@garrypolley: https://github.com/garrypolley
.. _@Gilbishkosma: https://github.com/Gilbishkosma
.. _@gwiskur: https://github.com/gwiskur
.. _@glasslion: https://github.com/glasslion
.. _@goldhand: https://github.com/goldhand
.. _@hackebrot: https://github.com/hackebrot
.. _@hairychris: https://github.com/hairychris
.. _@hanaquadara: https://github.com/hanaquadara
.. _@hendrikschneider: https://github.com/hendrikschneider
.. _@highpost: https://github.com/highpost
.. _@hjwp: https://github.com/hjwp
.. _@howiezhao: https://github.com/howiezhao
.. _@IanLee1521: https://github.com/IanLee1521
.. _@ikhomutov: https://github.com/ikhomutov
.. _@jameswilliams1: https://github.com/jameswilliams1
.. _@ikkebr: https://github.com/ikkebr
.. _@Isaac12x: https://github.com/Isaac12x
.. _@iynaix: https://github.com/iynaix
@ -302,6 +325,7 @@ Listed in alphabetical order.
.. _@jcass77: https://github.com/jcass77
.. _@jeromecaisip: https://github.com/jeromecaisip
.. _@jleclanche: https://github.com/jleclanche
.. _@jules-ch: https://github.com/jules-ch
.. _@juliocc: https://github.com/juliocc
.. _@jvanbrug: https://github.com/jvanbrug
.. _@ka7eh: https://github.com/ka7eh
@ -335,21 +359,25 @@ Listed in alphabetical order.
.. _@originell: https://github.com/originell
.. _@oubiga: https://github.com/oubiga
.. _@parbhat: https://github.com/parbhat
.. _@rjsnh1522: https://github.com/rjsnh1522
.. _@pchiquet: https://github.com/pchiquet
.. _@phiberjenz: https://github.com/phiberjenz
.. _@purplediane: https://github.com/purplediane
.. _@raonyguimaraes: https://github.com/raonyguimaraes
.. _@reggieriser: https://github.com/reggieriser
.. _@reyesvicente: https://github.com/reyesvicente
.. _@rm--: https://github.com/rm--
.. _@rolep: https://github.com/rolep
.. _@romanosipenko: https://github.com/romanosipenko
.. _@saschalalala: https://github.com/saschalalala
.. _@scaramagus: https://github.com/scaramagus
.. _@shireenrao: https://github.com/shireenrao
.. _@show0k: https://github.com/show0k
.. _@shultz: https://github.com/shultz
.. _@siauPatrick: https://github.com/siauPatrick
.. _@sladinji: https://github.com/sladinji
.. _@slafs: https://github.com/slafs
.. _@sorasful:: https://github.com/sorasful
.. _@ssteinerX: https://github.com/ssteinerx
.. _@step21: https://github.com/step21
.. _@stepmr: https://github.com/stepmr

View File

@ -1,4 +1,4 @@
Copyright (c) 2013-2018, Daniel Roy Greenfeld
Copyright (c) 2013-2020, Daniel Roy Greenfeld
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

@ -36,8 +36,8 @@ production-ready Django projects quickly.
Features
---------
* For Django 2.2
* Works with Python 3.7
* For Django 3.0
* Works with Python 3.8
* Renders Django projects with 100% starting test coverage
* Twitter Bootstrap_ v4 (`maintained Foundation fork`_ also available)
* 12-Factor_ based settings via django-environ_
@ -45,8 +45,9 @@ Features
* Optimized development and production settings
* Registration via django-allauth_
* Comes with custom user model ready to go
* Optional basic ASGI setup for Websockets
* Optional custom static build using Gulp and livereload
* Send emails via Anymail_ (using Mailgun_ by default, but switchable)
* Send emails via Anymail_ (using Mailgun_ by default or Amazon SES if AWS is selected cloud provider, but switchable)
* Media storage using Amazon S3 or Google Cloud Storage
* Docker support using docker-compose_ for development and production (using Traefik_ with LetsEncrypt_ support)
* Procfile_ for deploying to Heroku
@ -105,16 +106,16 @@ This project is run by volunteers. Please support them in their efforts to maint
Projects that provide financial support to the maintainers:
Two Scoops of Django 1.11
Django Crash Course
~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://cdn.shopify.com/s/files/1/0304/6901/products/2017-06-29-tsd11-sticker-02.png
:name: Two Scoops of Django 1.11 Cover
.. image:: https://cdn.shopify.com/s/files/1/0304/6901/files/Django-Crash-Course-300x436.jpg
:name: Django Crash Course: Covers Django 3.0 and Python 3.8
:align: center
:alt: Two Scoops of Django
:target: http://twoscoopspress.com/products/two-scoops-of-django-1-11
:alt: Django Crash Course
:target: https://www.roygreenfeld.com/products/django-crash-course
Two Scoops of Django is the best dessert-themed Django reference in the universe
Django Crash Course for Django 3.0 and Python 3.8 is the best cheese-themed Django reference in the universe!
pyup
~~~~~~~~~~~~~~~~~~
@ -135,7 +136,7 @@ and then editing the results to include your name, email, and various configurat
First, get Cookiecutter. Trust me, it's awesome::
$ pip install "cookiecutter>=1.4.0"
$ pip install "cookiecutter>=1.7.0"
Now run it against this repo::
@ -272,7 +273,7 @@ If you do rename your fork, I encourage you to submit it to the following places
* cookiecutter_ so it gets listed in the README as a template.
* The cookiecutter grid_ on Django Packages.
.. _cookiecutter: https://github.com/audreyr/cookiecutter
.. _cookiecutter: https://github.com/cookiecutter/cookiecutter
.. _grid: https://www.djangopackages.com/grids/g/cookiecutters/
Submit a Pull Request

View File

@ -33,6 +33,18 @@
"GCP",
"None"
],
"mail_service": [
"Mailgun",
"Amazon SES",
"Mailjet",
"Mandrill",
"Postmark",
"Sendgrid",
"SendinBlue",
"SparkPost",
"Other SMTP"
],
"use_async": "n",
"use_drf": "n",
"custom_bootstrap_compilation": "n",
"use_compressor": "n",

View File

@ -15,7 +15,7 @@ Full instructions follow, but here's a high-level view.
2. Set your config variables in the *postactivate* script
3. Run the *manage.py* ``migrate`` and ``collectstatic`` commands
3. Run the *manage.py* ``migrate`` and ``collectstatic`` {%- if cookiecutter.use_compressor == "y" %}and ``compress`` {%- endif %}commands
4. Add an entry to the PythonAnywhere *Web tab*
@ -35,7 +35,7 @@ Make sure your project is fully committed and pushed up to Bitbucket or Github o
git clone <my-repo-url> # you can also use hg
cd my-project-name
mkvirtualenv --python=/usr/bin/python3.7 my-project-name
mkvirtualenv --python=/usr/bin/python3.8 my-project-name
pip install -r requirements/production.txt # may take a few minutes
@ -109,6 +109,7 @@ Now run the migration, and collectstatic:
source $VIRTUAL_ENV/bin/postactivate
python manage.py migrate
python manage.py collectstatic
{%- if cookiecutter.use_compressor == "y" %}python manage.py compress {%- endif %}
# and, optionally
python manage.py createsuperuser
@ -175,6 +176,7 @@ For subsequent deployments, the procedure is much simpler. In a Bash console:
git pull
python manage.py migrate
python manage.py collectstatic
{%- if cookiecutter.use_compressor == "y" %}python manage.py compress {%- endif %}
And then go to the Web tab and hit **Reload**

View File

@ -25,7 +25,9 @@ Provided you have opted for Celery (via setting ``use_celery`` to ``y``) there a
* ``celeryworker`` running a Celery worker process;
* ``celerybeat`` running a Celery beat process;
* ``flower`` running Flower_ (for more info, check out :ref:`CeleryFlower` instructions for local environment).
* ``flower`` running Flower_.
The ``flower`` service is served by Traefik over HTTPS, through the port ``5555``. For more information about Flower and its login credentials, check out :ref:`CeleryFlower` instructions for local environment.
.. _`Flower`: https://github.com/mher/flower

View File

@ -9,7 +9,7 @@ Setting Up Development Environment
Make sure to have the following on your host:
* Python 3.7
* Python 3.8
* PostgreSQL_.
* Redis_, if using Celery
@ -17,7 +17,7 @@ First things first.
#. Create a virtualenv: ::
$ python3.7 -m venv <virtual env path>
$ python3.8 -m venv <virtual env path>
#. Activate the virtualenv you have just created: ::
@ -68,10 +68,14 @@ First things first.
$ python manage.py migrate
#. See the application being served through Django development server: ::
#. If you're running synchronously, see the application being served through Django development server: ::
$ python manage.py runserver 0.0.0.0:8000
or if you're running asynchronously: ::
$ gunicorn config.asgi --bind 0.0.0.0:8000 -k uvicorn.workers.UvicornWorker --reload
.. _PostgreSQL: https://www.postgresql.org/download/
.. _Redis: https://redis.io/download
.. _createdb: https://www.postgresql.org/docs/current/static/app-createdb.html

View File

@ -24,4 +24,4 @@ Why doesn't this follow the layout from Two Scoops of Django?
You may notice that some elements of this project do not exactly match what we describe in chapter 3 of `Two Scoops of Django 1.11`_. The reason for that is this project, amongst other things, serves as a test bed for trying out new ideas and concepts. Sometimes they work, sometimes they don't, but the end result is that it won't necessarily match precisely what is described in the book I co-authored.
.. _Two Scoops of Django 1.11: https://www.twoscoopspress.com/collections/django/products/two-scoops-of-django-1-11
.. _Two Scoops of Django 1.11: https://www.feldroy.com/collections/django/products/two-scoops-of-django-1-11

View File

@ -5,7 +5,7 @@ Welcome to Cookiecutter Django's documentation!
A Cookiecutter_ template for Django.
.. _cookiecutter: https://github.com/audreyr/cookiecutter
.. _cookiecutter: https://github.com/cookiecutter/cookiecutter
Contents:

View File

@ -70,6 +70,25 @@ cloud_provider:
Note that if you choose no cloud provider, media files won't work.
mail_service:
Select an email service that Django-Anymail provides
1. Mailgun_
2. `Amazon SES`_
3. Mailjet_
4. Mandrill_
5. Postmark_
6. SendGrid_
7. SendinBlue_
8. SparkPost_
9. `Other SMTP`_
use_async:
Indicates whether the project should use web sockets with Uvicorn + Gunicorn.
use_drf:
Indicates whether the project should be configured to use `Django Rest Framework`_.
custom_bootstrap_compilation:
Indicates whether the project should support Bootstrap recompilation
via the selected JavaScript task runner's task. This can be useful
@ -98,8 +117,8 @@ ci_tool:
Select a CI tool for running tests. The choices are:
1. None
2. Travis_
3. Gitlab_
2. `Travis CI`_
3. `Gitlab CI`_
keep_local_envs_in_vcs:
Indicates whether the project's ``.envs/.local/`` should be kept in VCS
@ -129,6 +148,18 @@ debug:
.. _AWS: https://aws.amazon.com/s3/
.. _GCP: https://cloud.google.com/storage/
.. _Amazon SES: https://aws.amazon.com/ses/
.. _Mailgun: https://www.mailgun.com
.. _Mailjet: https://www.mailjet.com
.. _Mandrill: http://mandrill.com
.. _Postmark: https://postmarkapp.com
.. _SendGrid: https://sendgrid.com
.. _SendinBlue: https://www.sendinblue.com
.. _SparkPost: https://www.sparkpost.com
.. _Other SMTP: https://anymail.readthedocs.io/en/stable/
.. _Django Rest Framework: https://github.com/encode/django-rest-framework/
.. _Django Compressor: https://github.com/django-compressor/django-compressor
.. _Celery: https://github.com/celery/celery

View File

@ -52,6 +52,21 @@ DJANGO_SENTRY_LOG_LEVEL SENTRY_LOG_LEVEL n/a
MAILGUN_API_KEY MAILGUN_API_KEY n/a raises error
MAILGUN_DOMAIN MAILGUN_SENDER_DOMAIN n/a raises error
MAILGUN_API_URL n/a n/a "https://api.mailgun.net/v3"
MAILJET_API_KEY MAILJET_API_KEY n/a raises error
MAILJET_SECRET_KEY MAILJET_SECRET_KEY n/a raises error
MAILJET_API_URL n/a n/a "https://api.mailjet.com/v3"
MANDRILL_API_KEY MANDRILL_API_KEY n/a raises error
MANDRILL_API_URL n/a n/a "https://mandrillapp.com/api/1.0"
POSTMARK_SERVER_TOKEN POSTMARK_SERVER_TOKEN n/a raises error
POSTMARK_API_URL n/a n/a "https://api.postmarkapp.com/"
SENDGRID_API_KEY SENDGRID_API_KEY n/a raises error
SENDGRID_GENERATE_MESSAGE_ID True n/a raises error
SENDGRID_MERGE_FIELD_FORMAT None n/a raises error
SENDGRID_API_URL n/a n/a "https://api.sendgrid.com/v3/"
SENDINBLUE_API_KEY SENDINBLUE_API_KEY n/a raises error
SENDINBLUE_API_URL n/a n/a "https://api.sendinblue.com/v3/"
SPARKPOST_API_KEY SPARKPOST_API_KEY n/a raises error
SPARKPOST_API_URL n/a n/a "https://api.sparkpost.com/api/v1"
======================================= =========================== ============================================== ======================================================================
--------------------------

View File

@ -101,6 +101,15 @@ def remove_celery_files():
os.remove(file_name)
def remove_async_files():
file_names = [
os.path.join("config", "asgi.py"),
os.path.join("config", "websocket.py"),
]
for file_name in file_names:
os.remove(file_name)
def remove_dottravisyml_file():
os.remove(".travis.yml")
@ -292,6 +301,10 @@ def remove_drf_starter_files():
shutil.rmtree(os.path.join("{{cookiecutter.project_slug}}", "users", "api"))
def remove_storages_module():
os.remove(os.path.join("{{cookiecutter.project_slug}}", "utils", "storages.py"))
def main():
debug = "{{ cookiecutter.debug }}".lower() == "y"
@ -352,6 +365,7 @@ def main():
WARNING + "You chose not to use a cloud provider, "
"media files won't be served in production." + TERMINATOR
)
remove_storages_module()
if "{{ cookiecutter.use_celery }}".lower() == "n":
remove_celery_files()
@ -367,6 +381,9 @@ def main():
if "{{ cookiecutter.use_drf }}".lower() == "n":
remove_drf_starter_files()
if "{{ cookiecutter.use_async }}".lower() == "n":
remove_async_files()
print(SUCCESS + "Project initialized, keep up the good work!" + TERMINATOR)

View File

@ -35,7 +35,7 @@ if "{{ cookiecutter.use_docker }}".lower() == "n":
if python_major_version == 2:
print(
WARNING + "You're running cookiecutter under Python 2, but the generated "
"project requires Python 3.7+. Do you want to proceed (y/n)? " + TERMINATOR
"project requires Python 3.8+. Do you want to proceed (y/n)? " + TERMINATOR
)
yes_options, no_options = frozenset(["y"]), frozenset(["n"])
while True:
@ -68,3 +68,15 @@ if (
"You should either use Whitenoise or select a Cloud Provider to serve static files"
)
sys.exit(1)
if (
"{{ cookiecutter.cloud_provider }}" == "GCP"
and "{{ cookiecutter.mail_service }}" == "Amazon SES"
) or (
"{{ cookiecutter.cloud_provider }}" == "None"
and "{{ cookiecutter.mail_service }}" == "Amazon SES"
):
print(
"You should either use AWS or select a different Mail Service for sending emails."
)
sys.exit(1)

View File

@ -1,7 +1,4 @@
[pytest]
addopts = -x --tb=short
addopts = -v --tb=short
python_paths = .
norecursedirs = .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/*
markers =
flake8: Run flake8 on all possible template combinations
black: Run black on all possible template combinations

View File

@ -1,4 +1,4 @@
cookiecutter==1.7.0
cookiecutter==1.7.2
sh==1.12.14
binaryornot==0.4.4
@ -6,12 +6,12 @@ binaryornot==0.4.4
# ------------------------------------------------------------------------------
black==19.10b0
flake8==3.7.9
flake8-isort==3.0.0
# Testing
# ------------------------------------------------------------------------------
tox==3.14.3
pytest==5.3.5
pytest_cases==1.12.1
pytest-cookies==0.4.0
pytest-xdist==1.31.0
pyyaml==5.3
tox==3.14.6
pytest==5.4.1
pytest-cookies==0.5.1
pytest-instafail==0.4.1.post0
pyyaml==5.3.1

View File

@ -10,7 +10,7 @@ except ImportError:
# Our version ALWAYS matches the version of Django we support
# If Django has a new release, we branch, tag, then update this setting after the tag.
version = "2.2.1"
version = "3.0.5-01"
if sys.argv[-1] == "tag":
os.system(f'git tag -a {version} -m "version {version}"')
@ -34,13 +34,13 @@ setup(
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Intended Audience :: Developers",
"Natural Language :: English",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development",
],

View File

@ -3,7 +3,6 @@ import re
import pytest
from cookiecutter.exceptions import FailedHookException
from pytest_cases import pytest_fixture_plus
import sh
import yaml
from binaryornot.check import is_binary
@ -26,49 +25,93 @@ def context():
}
@pytest_fixture_plus
@pytest.mark.parametrize("windows", ["y", "n"], ids=lambda yn: f"win:{yn}")
@pytest.mark.parametrize("use_docker", ["y", "n"], ids=lambda yn: f"docker:{yn}")
@pytest.mark.parametrize("use_celery", ["y", "n"], ids=lambda yn: f"celery:{yn}")
@pytest.mark.parametrize("use_mailhog", ["y", "n"], ids=lambda yn: f"mailhog:{yn}")
@pytest.mark.parametrize("use_sentry", ["y", "n"], ids=lambda yn: f"sentry:{yn}")
@pytest.mark.parametrize("use_compressor", ["y", "n"], ids=lambda yn: f"cmpr:{yn}")
@pytest.mark.parametrize("use_drf", ["y", "n"], ids=lambda yn: f"drf:{yn}")
@pytest.mark.parametrize(
"use_whitenoise,cloud_provider",
[
("y", "AWS"),
("y", "GCP"),
("y", "None"),
("n", "AWS"),
("n", "GCP"),
# no whitenoise + no cloud provider is not supported
],
ids=lambda id: f"wnoise:{id[0]}-cloud:{id[1]}",
)
def context_combination(
windows,
use_docker,
use_celery,
use_mailhog,
use_sentry,
use_compressor,
use_whitenoise,
use_drf,
cloud_provider,
):
"""Fixture that parametrize the function where it's used."""
return {
"windows": windows,
"use_docker": use_docker,
"use_compressor": use_compressor,
"use_celery": use_celery,
"use_mailhog": use_mailhog,
"use_sentry": use_sentry,
"use_whitenoise": use_whitenoise,
"use_drf": use_drf,
"cloud_provider": cloud_provider,
}
SUPPORTED_COMBINATIONS = [
{"open_source_license": "MIT"},
{"open_source_license": "BSD"},
{"open_source_license": "GPLv3"},
{"open_source_license": "Apache Software License 2.0"},
{"open_source_license": "Not open source"},
{"windows": "y"},
{"windows": "n"},
{"use_pycharm": "y"},
{"use_pycharm": "n"},
{"use_docker": "y"},
{"use_docker": "n"},
{"postgresql_version": "11.3"},
{"postgresql_version": "10.8"},
{"postgresql_version": "9.6"},
{"postgresql_version": "9.5"},
{"postgresql_version": "9.4"},
{"cloud_provider": "AWS", "use_whitenoise": "y"},
{"cloud_provider": "AWS", "use_whitenoise": "n"},
{"cloud_provider": "GCP", "use_whitenoise": "y"},
{"cloud_provider": "GCP", "use_whitenoise": "n"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailgun"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailjet"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mandrill"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Postmark"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Sendgrid"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "SendinBlue"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "SparkPost"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Other SMTP"},
# Note: cloud_provider=None AND use_whitenoise=n is not supported
{"cloud_provider": "AWS", "mail_service": "Mailgun"},
{"cloud_provider": "AWS", "mail_service": "Amazon SES"},
{"cloud_provider": "AWS", "mail_service": "Mailjet"},
{"cloud_provider": "AWS", "mail_service": "Mandrill"},
{"cloud_provider": "AWS", "mail_service": "Postmark"},
{"cloud_provider": "AWS", "mail_service": "Sendgrid"},
{"cloud_provider": "AWS", "mail_service": "SendinBlue"},
{"cloud_provider": "AWS", "mail_service": "SparkPost"},
{"cloud_provider": "AWS", "mail_service": "Other SMTP"},
{"cloud_provider": "GCP", "mail_service": "Mailgun"},
{"cloud_provider": "GCP", "mail_service": "Mailjet"},
{"cloud_provider": "GCP", "mail_service": "Mandrill"},
{"cloud_provider": "GCP", "mail_service": "Postmark"},
{"cloud_provider": "GCP", "mail_service": "Sendgrid"},
{"cloud_provider": "GCP", "mail_service": "SendinBlue"},
{"cloud_provider": "GCP", "mail_service": "SparkPost"},
{"cloud_provider": "GCP", "mail_service": "Other SMTP"},
# Note: cloud_providers GCP and None with mail_service Amazon SES is not supported
{"use_async": "y"},
{"use_async": "n"},
{"use_drf": "y"},
{"use_drf": "n"},
{"js_task_runner": "None"},
{"js_task_runner": "Gulp"},
{"custom_bootstrap_compilation": "y"},
{"custom_bootstrap_compilation": "n"},
{"use_compressor": "y"},
{"use_compressor": "n"},
{"use_celery": "y"},
{"use_celery": "n"},
{"use_mailhog": "y"},
{"use_mailhog": "n"},
{"use_sentry": "y"},
{"use_sentry": "n"},
{"use_whitenoise": "y"},
{"use_whitenoise": "n"},
{"use_heroku": "y"},
{"use_heroku": "n"},
{"ci_tool": "None"},
{"ci_tool": "Travis"},
{"ci_tool": "Gitlab"},
{"keep_local_envs_in_vcs": "y"},
{"keep_local_envs_in_vcs": "n"},
{"debug": "y"},
{"debug": "n"},
]
UNSUPPORTED_COMBINATIONS = [
{"cloud_provider": "None", "use_whitenoise": "n"},
{"cloud_provider": "GCP", "mail_service": "Amazon SES"},
{"cloud_provider": "None", "mail_service": "Amazon SES"},
]
def _fixture_id(ctx):
"""Helper to get a user friendly test name from the parametrized context."""
return "-".join(f"{key}:{value}" for key, value in ctx.items())
def build_files_list(root_dir):
@ -81,9 +124,7 @@ def build_files_list(root_dir):
def check_paths(paths):
"""Method to check all paths have correct substitutions,
used by other tests cases
"""
"""Method to check all paths have correct substitutions."""
# Assert that no match is found in any of the files
for path in paths:
if is_binary(path):
@ -95,13 +136,10 @@ def check_paths(paths):
assert match is None, msg.format(path)
def test_project_generation(cookies, context, context_combination):
"""
Test that project is generated and fully rendered.
This is parametrized for each combination from ``context_combination`` fixture
"""
result = cookies.bake(extra_context={**context, **context_combination})
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_project_generation(cookies, context, context_override):
"""Test that project is generated and fully rendered."""
result = cookies.bake(extra_context={**context, **context_override})
assert result.exit_code == 0
assert result.exception is None
assert result.project.basename == context["project_slug"]
@ -112,38 +150,34 @@ def test_project_generation(cookies, context, context_combination):
check_paths(paths)
@pytest.mark.flake8
def test_flake8_passes(cookies, context_combination):
"""
Generated project should pass flake8.
This is parametrized for each combination from ``context_combination`` fixture
"""
result = cookies.bake(extra_context=context_combination)
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_flake8_passes(cookies, context_override):
"""Generated project should pass flake8."""
result = cookies.bake(extra_context=context_override)
try:
sh.flake8(str(result.project))
except sh.ErrorReturnCode as e:
pytest.fail(e)
pytest.fail(e.stdout.decode())
@pytest.mark.black
def test_black_passes(cookies, context_combination):
"""
Generated project should pass black.
This is parametrized for each combination from ``context_combination`` fixture
"""
result = cookies.bake(extra_context=context_combination)
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_black_passes(cookies, context_override):
"""Generated project should pass black."""
result = cookies.bake(extra_context=context_override)
try:
sh.black("--check", "--diff", "--exclude", "migrations", f"{result.project}/")
except sh.ErrorReturnCode as e:
pytest.fail(e)
pytest.fail(e.stdout.decode())
def test_travis_invokes_pytest(cookies, context):
context.update({"ci_tool": "Travis"})
@pytest.mark.parametrize(
["use_docker", "expected_test_script"],
[("n", "pytest"), ("y", "docker-compose -f local.yml run django pytest"),],
)
def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_script):
context.update({"ci_tool": "Travis", "use_docker": use_docker})
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
@ -153,13 +187,21 @@ def test_travis_invokes_pytest(cookies, context):
with open(f"{result.project}/.travis.yml", "r") as travis_yml:
try:
assert yaml.load(travis_yml)["script"] == ["pytest"]
yml = yaml.load(travis_yml, Loader=yaml.FullLoader)["jobs"]["include"]
assert yml[0]["script"] == ["flake8"]
assert yml[1]["script"] == [expected_test_script]
except yaml.YAMLError as e:
pytest.fail(e)
pytest.fail(str(e))
def test_gitlab_invokes_flake8_and_pytest(cookies, context):
context.update({"ci_tool": "Gitlab"})
@pytest.mark.parametrize(
["use_docker", "expected_test_script"],
[("n", "pytest"), ("y", "docker-compose -f local.yml run django pytest"),],
)
def test_gitlab_invokes_flake8_and_pytest(
cookies, context, use_docker, expected_test_script
):
context.update({"ci_tool": "Gitlab", "use_docker": use_docker})
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
@ -169,9 +211,9 @@ def test_gitlab_invokes_flake8_and_pytest(cookies, context):
with open(f"{result.project}/.gitlab-ci.yml", "r") as gitlab_yml:
try:
gitlab_config = yaml.load(gitlab_yml)
gitlab_config = yaml.load(gitlab_yml, Loader=yaml.FullLoader)
assert gitlab_config["flake8"]["script"] == ["flake8"]
assert gitlab_config["pytest"]["script"] == ["pytest"]
assert gitlab_config["pytest"]["script"] == [expected_test_script]
except yaml.YAMLError as e:
pytest.fail(e)
@ -187,9 +229,10 @@ def test_invalid_slug(cookies, context, slug):
assert isinstance(result.exception, FailedHookException)
def test_no_whitenoise_and_no_cloud_provider(cookies, context):
"""It should not generate project if neither whitenoise or cloud provider are set"""
context.update({"use_whitenoise": "n", "cloud_provider": "None"})
@pytest.mark.parametrize("invalid_context", UNSUPPORTED_COMBINATIONS)
def test_error_if_incompatible(cookies, context, invalid_context):
"""It should not generate project an incompatible combination is selected."""
context.update(invalid_context)
result = cookies.bake(extra_context=context)
assert result.exit_code != 0

12
tox.ini
View File

@ -1,18 +1,10 @@
[tox]
skipsdist = true
envlist = py37,flake8,black,black-template
envlist = py38,black-template
[testenv]
deps = -rrequirements.txt
commands = pytest -m "not flake8" -m "not black" {posargs:./tests}
[testenv:flake8]
deps = -rrequirements.txt
commands = pytest -m flake8 {posargs:./tests}
[testenv:black]
deps = -rrequirements.txt
commands = pytest -m black {posargs:./tests}
commands = pytest {posargs:./tests}
[testenv:black-template]
deps = black

View File

@ -1,5 +0,0 @@
[run]
include = {{cookiecutter.project_slug}}/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin

View File

@ -13,8 +13,8 @@ indent_style = space
indent_size = 4
[*.py]
line_length = 120
known_first_party = {{ cookiecutter.project_slug }}
line_length = 88
known_first_party = {{cookiecutter.project_slug}},config
multi_line_output = 3
default_section = THIRDPARTY
recursive = true

View File

@ -13,9 +13,26 @@ DJANGO_SECURE_SSL_REDIRECT=False
# Email
# ------------------------------------------------------------------------------
MAILGUN_API_KEY=
DJANGO_SERVER_EMAIL=
{% if cookiecutter.mail_service == 'Mailgun' %}
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
{% elif cookiecutter.mail_service == 'Mailjet' %}
MAILJET_API_KEY=
MAILJET_SECRET_KEY=
{% elif cookiecutter.mail_service == 'Mandrill' %}
MANDRILL_API_KEY=
{% elif cookiecutter.mail_service == 'Postmark' %}
POSTMARK_SERVER_TOKEN=
{% elif cookiecutter.mail_service == 'Sendgrid' %}
SENDGRID_API_KEY=
SENDGRID_GENERATE_MESSAGE_ID=True
SENDGRID_MERGE_FIELD_FORMAT=None
{% elif cookiecutter.mail_service == 'SendinBlue' %}
SENDINBLUE_API_KEY=
{% elif cookiecutter.mail_service == 'SparkPost' %}
SPARKPOST_API_KEY=
{% endif %}
{% if cookiecutter.cloud_provider == 'AWS' %}
# AWS
# ------------------------------------------------------------------------------
@ -31,11 +48,7 @@ DJANGO_GCP_STORAGE_BUCKET_NAME=
# django-allauth
# ------------------------------------------------------------------------------
DJANGO_ACCOUNT_ALLOW_REGISTRATION=True
{% if cookiecutter.use_compressor == 'y' %}
# django-compressor
# ------------------------------------------------------------------------------
COMPRESS_ENABLED=
{% endif %}
# Gunicorn
# ------------------------------------------------------------------------------
WEB_CONCURRENCY=4

View File

@ -6,6 +6,10 @@ variables:
POSTGRES_USER: '{{ cookiecutter.project_slug }}'
POSTGRES_PASSWORD: ''
POSTGRES_DB: 'test_{{ cookiecutter.project_slug }}'
POSTGRES_HOST_AUTH_METHOD: trust
{% if cookiecutter.use_celery == 'y' -%}
CELERY_BROKER_URL: 'redis://redis:6379/0'
{%- endif %}
flake8:
stage: lint
@ -18,8 +22,21 @@ flake8:
pytest:
stage: test
image: python:3.7
{% if cookiecutter.use_docker == 'y' -%}
tags:
- docker
services:
- docker
before_script:
- docker-compose -f local.yml build
# Ensure celerybeat does not crash due to non-existent tables
- docker-compose -f local.yml run --rm django python manage.py migrate
- docker-compose -f local.yml up -d
script:
- docker-compose -f local.yml run django pytest
{%- else %}
tags:
- python
services:
- postgres:11
variables:
@ -30,4 +47,5 @@ pytest:
script:
- pytest
{%- endif %}

View File

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
{%- if cookiecutter.use_celery == 'y' %}
{%- if cookiecutter.use_docker == 'n' %}
<component name="DjangoConsoleOptions"
custom-start-script="import sys; print('Python %s on %s' % (sys.version, sys.platform))&#10;import django; print('Django %s' % django.get_version())&#10;import os&#10;sys.path.extend([WORKING_DIR_AND_PYTHON_PATHS])&#10;if 'setup' in dir(django): django.setup()&#10;import django_manage_shell; django_manage_shell.run(PROJECT_ROOT)"
module-name="{{ cookiecutter.project_slug }}" is-module-sdk="true">
</component>
{%- elif cookiecutter.use_celery == 'y' %}
<component name="DjangoConsoleOptions"
custom-start-script="import sys; print('Python %s on %s' % (sys.version, sys.platform))&#10;import django; print('Django %s' % django.get_version())&#10;import os&#10;os.environ.setdefault(&quot;DATABASE_URL&quot;,&quot;postgres://{}:{}@{}:{}/{}&quot;.format(os.environ['POSTGRES_USER'], os.environ['POSTGRES_PASSWORD'], os.environ['POSTGRES_HOST'], os.environ['POSTGRES_PORT'], os.environ['POSTGRES_DB']))&#10;os.environ.setdefault(&quot;CELERY_BROKER_URL&quot;, os.environ['REDIS_URL'])&#10;sys.path.extend([WORKING_DIR_AND_PYTHON_PATHS])&#10;if 'setup' in dir(django): django.setup()&#10;import django_manage_shell; django_manage_shell.run(PROJECT_ROOT)"
module-name="{{ cookiecutter.project_slug }}" is-module-sdk="true">

View File

@ -7,7 +7,7 @@ repos:
rev: master
hooks:
- id: trailing-whitespace
files: (^|/)a/.+\.(py|html|sh|css|js)$
files: (^|/).+\.(py|html|sh|css|js)$
- repo: local
hooks:
@ -16,4 +16,5 @@ repos:
entry: flake8
language: python
types: [python]
args: ['--config=setup.cfg']

View File

@ -1,17 +1,45 @@
dist: xenial
services:
- postgresql
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb
- sudo apt-get install -qq libjpeg8-dev libfreetype6-dev libwebp-dev
- sudo apt-get install -qq graphviz-dev python-setuptools python3-dev python-virtualenv python-pip
- sudo apt-get install -qq firefox automake libtool libreadline6 libreadline6-dev libreadline-dev
- sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm
language: python
python:
- "3.7"
install:
- pip install -r requirements/local.txt
script:
- "pytest"
- "3.8"
services:
- {% if cookiecutter.use_docker == 'y' %}docker{% else %}postgresql{% endif %}
jobs:
include:
- name: "Linter"
before_script:
- pip install -q flake8
script:
- "flake8"
- name: "Django Test"
{%- if cookiecutter.use_docker == 'y' %}
before_script:
- docker-compose -v
- docker -v
- docker-compose -f local.yml build
# Ensure celerybeat does not crash due to non-existent tables
- docker-compose -f local.yml run --rm django python manage.py migrate
- docker-compose -f local.yml up -d
script:
- "docker-compose -f local.yml run django pytest"
after_failure:
- docker-compose -f local.yml logs
{%- else %}
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb
- sudo apt-get install -qq libjpeg8-dev libfreetype6-dev libwebp-dev
- sudo apt-get install -qq graphviz-dev python-setuptools python3-dev python-virtualenv python-pip
- sudo apt-get install -qq firefox automake libtool libreadline6 libreadline6-dev libreadline-dev
- sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm
language: python
python:
- "3.8"
install:
- pip install -r requirements/local.txt
script:
- "pytest"
{%- endif %}

View File

@ -50,4 +50,196 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
{% elif cookiecutter.open_source_license == 'Apache Software License 2.0' %}
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright {% now 'utc', '%Y' %} {{ cookiecutter.author_name }}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
{% endif %}

View File

@ -1,5 +1,10 @@
release: python manage.py migrate
{% if cookiecutter.use_async == "y" -%}
web: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker
{%- else %}
web: gunicorn config.wsgi:application
{%- endif %}
{% if cookiecutter.use_celery == "y" -%}
worker: celery worker --app=config.celery_app --loglevel=info
beat: celery beat --app=config.celery_app --loglevel=info
{%- endif %}

View File

@ -1,6 +1,7 @@
FROM python:3.7-slim-buster
FROM python:3.8-slim-buster
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update \
# dependencies for building Python packages

View File

@ -6,4 +6,8 @@ set -o nounset
python manage.py migrate
{%- if cookiecutter.use_async == 'y' %}
/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:8000 --chdir=/app -k uvicorn.workers.UvicornWorker --reload
{%- else %}
python manage.py runserver_plus 0.0.0.0:8000
{% endif %}

View File

@ -9,7 +9,7 @@ RUN npm run build
# Python build stage
{%- endif %}
FROM python:3.7-slim-buster
FROM python:3.8-slim-buster
ENV PYTHONUNBUFFERED 1

View File

@ -6,4 +6,29 @@ set -o nounset
python /app/manage.py collectstatic --noinput
{% if cookiecutter.use_whitenoise == 'y' and cookiecutter.use_compressor == 'y' %}
compress_enabled() {
python << END
import sys
from environ import Env
env = Env(COMPRESS_ENABLED=(bool, True))
if env('COMPRESS_ENABLED'):
sys.exit(0)
else:
sys.exit(1)
END
}
if compress_enabled; then
# NOTE this command will fail if django-compressor is disabled
python /app/manage.py compress
fi
{%- endif %}
{% if cookiecutter.use_async == 'y' %}
/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker
{% else %}
/usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app
{%- endif %}

View File

@ -9,6 +9,11 @@ entryPoints:
web-secure:
# https
address: ":443"
{%- if cookiecutter.use_celery == 'y' %}
flower:
address: ":5555"
{%- endif %}
certificatesResolvers:
letsencrypt:
@ -23,7 +28,7 @@ certificatesResolvers:
http:
routers:
web-router:
rule: "Host(`{{ cookiecutter.domain_name }}`)"
rule: "Host(`{{ cookiecutter.domain_name }}`) || Host(`www.{{ cookiecutter.domain_name }}`)"
entryPoints:
- web
middlewares:
@ -32,7 +37,7 @@ http:
service: django
web-secure-router:
rule: "Host(`{{ cookiecutter.domain_name }}`)"
rule: "Host(`{{ cookiecutter.domain_name }}`) || Host(`www.{{ cookiecutter.domain_name }}`)"
entryPoints:
- web-secure
middlewares:
@ -41,6 +46,17 @@ http:
tls:
# https://docs.traefik.io/master/routing/routers/#certresolver
certResolver: letsencrypt
{%- if cookiecutter.use_celery == 'y' %}
flower-secure-router:
rule: "Host(`{{ cookiecutter.domain_name }}`)"
entryPoints:
- flower
service: flower
tls:
# https://docs.traefik.io/master/routing/routers/#certresolver
certResolver: letsencrypt
{%- endif %}
middlewares:
redirect:
@ -52,13 +68,20 @@ http:
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
# https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
headers:
hostsProxyHeaders: ['X-CSRFToken']
hostsProxyHeaders: ["X-CSRFToken"]
services:
django:
loadBalancer:
servers:
- url: http://django:5000
{%- if cookiecutter.use_celery == 'y' %}
flower:
loadBalancer:
servers:
- url: http://flower:5555
{%- endif %}
providers:
# https://docs.traefik.io/master/providers/file/

View File

@ -1,5 +1,6 @@
from rest_framework.routers import DefaultRouter, SimpleRouter
from django.conf import settings
from rest_framework.routers import DefaultRouter, SimpleRouter
from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet
if settings.DEBUG:

View File

@ -0,0 +1,40 @@
"""
ASGI config for {{ cookiecutter.project_name }} project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/dev/howto/deployment/asgi/
"""
import os
import sys
from pathlib import Path
from django.core.asgi import get_asgi_application
# This allows easy placement of apps within the interior
# {{ cookiecutter.project_slug }} directory.
app_path = Path(__file__).parents[1].resolve()
sys.path.append(str(app_path / "{{ cookiecutter.project_slug }}"))
# If DJANGO_SETTINGS_MODULE is unset, default to the local settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
# This application object is used by any ASGI server configured to use this file.
django_application = get_asgi_application()
# Apply ASGI middleware here.
# from helloworld.asgi import HelloWorldApplication
# application = HelloWorldApplication(application)
# Import websocket application here, so apps from django_application are loaded first
from config.websocket import websocket_application # noqa isort:skip
async def application(scope, receive, send):
if scope["type"] == "http":
await django_application(scope, receive, send)
elif scope["type"] == "websocket":
await websocket_application(scope, receive, send)
else:
raise NotImplementedError(f"Unknown scope type {scope['type']}")

View File

@ -1,4 +1,5 @@
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.

View File

@ -1,20 +1,19 @@
"""
Base settings to build other settings files upon.
"""
from pathlib import Path
import environ
ROOT_DIR = (
environ.Path(__file__) - 3
) # ({{ cookiecutter.project_slug }}/config/settings/base.py - 3 = {{ cookiecutter.project_slug }}/)
APPS_DIR = ROOT_DIR.path("{{ cookiecutter.project_slug }}")
ROOT_DIR = Path(__file__).parents[2]
# {{ cookiecutter.project_slug }}/)
APPS_DIR = ROOT_DIR / "{{ cookiecutter.project_slug }}"
env = environ.Env()
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False)
if READ_DOT_ENV_FILE:
# OS environment variables take precedence over variables from .env
env.read_env(str(ROOT_DIR.path(".env")))
env.read_env(str(ROOT_DIR / ".env"))
# GENERAL
# ------------------------------------------------------------------------------
@ -36,7 +35,7 @@ USE_L10N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
LOCALE_PATHS = [ROOT_DIR.path("locale")]
LOCALE_PATHS = [str(ROOT_DIR / "locale")]
# DATABASES
# ------------------------------------------------------------------------------
@ -75,10 +74,13 @@ THIRD_PARTY_APPS = [
"allauth",
"allauth.account",
"allauth.socialaccount",
"rest_framework",
{%- if cookiecutter.use_celery == 'y' %}
"django_celery_beat",
{%- endif %}
{%- if cookiecutter.use_drf == "y" %}
"rest_framework",
"rest_framework.authtoken",
{%- endif %}
]
LOCAL_APPS = [
@ -148,11 +150,11 @@ MIDDLEWARE = [
# STATIC
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = str(ROOT_DIR("staticfiles"))
STATIC_ROOT = str(ROOT_DIR / "staticfiles")
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [str(APPS_DIR.path("static"))]
STATICFILES_DIRS = [str(APPS_DIR / "static")]
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
@ -162,7 +164,7 @@ STATICFILES_FINDERS = [
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR("media"))
MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = "/media/"
@ -174,7 +176,7 @@ TEMPLATES = [
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
"BACKEND": "django.template.backends.django.DjangoTemplates",
# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
"DIRS": [str(APPS_DIR.path("templates"))],
"DIRS": [str(APPS_DIR / "templates")],
"OPTIONS": {
# https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
@ -207,7 +209,7 @@ CRISPY_TEMPLATE_PACK = "bootstrap4"
# FIXTURES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs
FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
# SECURITY
# ------------------------------------------------------------------------------
@ -226,7 +228,7 @@ X_FRAME_OPTIONS = "DENY"
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
)
# https://docs.djangoproject.com/en/2.2/ref/settings/#email-timeout
# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout
EMAIL_TIMEOUT = 5
# ADMIN
@ -309,7 +311,7 @@ INSTALLED_APPS += ["compressor"]
STATICFILES_FINDERS += ["compressor.finders.CompressorFinder"]
{%- endif %}
{% if cookiecutter.use_drf == "y" -%}
# django-reset-framework
# django-rest-framework
# -------------------------------------------------------------------------------
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
REST_FRAMEWORK = {

View File

@ -68,7 +68,7 @@ if env("USE_DOCKER") == "yes":
import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [ip[:-1] + "1" for ip in ips]
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
{%- endif %}
# django-extensions

View File

@ -2,7 +2,6 @@
import logging
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
{%- if cookiecutter.use_celery == 'y' %}
@ -103,11 +102,11 @@ GS_DEFAULT_ACL = "publicRead"
{% if cookiecutter.use_whitenoise == 'y' -%}
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
{% elif cookiecutter.cloud_provider == 'AWS' -%}
STATICFILES_STORAGE = "config.settings.production.StaticRootS3Boto3Storage"
STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticRootS3Boto3Storage"
COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy"
STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/"
{% elif cookiecutter.cloud_provider == 'GCP' -%}
STATICFILES_STORAGE = "config.settings.production.StaticRootGoogleCloudStorage"
STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticRootGoogleCloudStorage"
COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy"
STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/"
{% endif -%}
@ -115,39 +114,10 @@ STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/"
# MEDIA
# ------------------------------------------------------------------------------
{%- if cookiecutter.cloud_provider == 'AWS' %}
# region http://stackoverflow.com/questions/10390244/
# Full-fledge class: https://stackoverflow.com/a/18046120/104731
from storages.backends.s3boto3 import S3Boto3Storage # noqa E402
class StaticRootS3Boto3Storage(S3Boto3Storage):
location = "static"
default_acl = "public-read"
class MediaRootS3Boto3Storage(S3Boto3Storage):
location = "media"
file_overwrite = False
# endregion
DEFAULT_FILE_STORAGE = "config.settings.production.MediaRootS3Boto3Storage"
DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootS3Boto3Storage"
MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/"
{%- elif cookiecutter.cloud_provider == 'GCP' %}
from storages.backends.gcloud import GoogleCloudStorage # noqa E402
class StaticRootGoogleCloudStorage(GoogleCloudStorage):
location = "static"
default_acl = "publicRead"
class MediaRootGoogleCloudStorage(GoogleCloudStorage):
location = "media"
file_overwrite = False
DEFAULT_FILE_STORAGE = "config.settings.production.MediaRootGoogleCloudStorage"
DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootGoogleCloudStorage"
MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/"
{%- endif %}
@ -182,17 +152,80 @@ EMAIL_SUBJECT_PREFIX = env(
# Django Admin URL regex.
ADMIN_URL = env("DJANGO_ADMIN_URL")
# Anymail (Mailgun)
# Anymail
# ------------------------------------------------------------------------------
# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail
INSTALLED_APPS += ["anymail"] # noqa F405
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference
{%- if cookiecutter.mail_service == 'Mailgun' %}
# https://anymail.readthedocs.io/en/stable/esps/mailgun/
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
ANYMAIL = {
"MAILGUN_API_KEY": env("MAILGUN_API_KEY"),
"MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"),
"MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"),
}
{%- elif cookiecutter.mail_service == 'Amazon SES' %}
# https://anymail.readthedocs.io/en/stable/esps/amazon_ses/
EMAIL_BACKEND = "anymail.backends.amazon_ses.EmailBackend"
ANYMAIL = {}
{%- elif cookiecutter.mail_service == 'Mailjet' %}
# https://anymail.readthedocs.io/en/stable/esps/mailjet/
EMAIL_BACKEND = "anymail.backends.mailjet.EmailBackend"
ANYMAIL = {
"MAILJET_API_KEY": env("MAILJET_API_KEY"),
"MAILJET_SECRET_KEY": env("MAILJET_SECRET_KEY"),
"MAILJET_API_URL": env("MAILJET_API_URL", default="https://api.mailjet.com/v3"),
}
{%- elif cookiecutter.mail_service == 'Mandrill' %}
# https://anymail.readthedocs.io/en/stable/esps/mandrill/
EMAIL_BACKEND = "anymail.backends.mandrill.EmailBackend"
ANYMAIL = {
"MANDRILL_API_KEY": env("MANDRILL_API_KEY"),
"MANDRILL_API_URL": env(
"MANDRILL_API_URL", default="https://mandrillapp.com/api/1.0"
),
}
{%- elif cookiecutter.mail_service == 'Postmark' %}
# https://anymail.readthedocs.io/en/stable/esps/postmark/
EMAIL_BACKEND = "anymail.backends.postmark.EmailBackend"
ANYMAIL = {
"POSTMARK_SERVER_TOKEN": env("POSTMARK_SERVER_TOKEN"),
"POSTMARK_API_URL": env("POSTMARK_API_URL", default="https://api.postmarkapp.com/"),
}
{%- elif cookiecutter.mail_service == 'Sendgrid' %}
# https://anymail.readthedocs.io/en/stable/esps/sendgrid/
EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend"
ANYMAIL = {
"SENDGRID_API_KEY": env("SENDGRID_API_KEY"),
"SENDGRID_GENERATE_MESSAGE_ID": env("SENDGRID_GENERATE_MESSAGE_ID"),
"SENDGRID_MERGE_FIELD_FORMAT": env("SENDGRID_MERGE_FIELD_FORMAT"),
"SENDGRID_API_URL": env("SENDGRID_API_URL", default="https://api.sendgrid.com/v3/"),
}
{%- elif cookiecutter.mail_service == 'SendinBlue' %}
# https://anymail.readthedocs.io/en/stable/esps/sendinblue/
EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend"
ANYMAIL = {
"SENDINBLUE_API_KEY": env("SENDINBLUE_API_KEY"),
"SENDINBLUE_API_URL": env(
"SENDINBLUE_API_URL", default="https://api.sendinblue.com/v3/"
),
}
{%- elif cookiecutter.mail_service == 'SparkPost' %}
# https://anymail.readthedocs.io/en/stable/esps/sparkpost/
EMAIL_BACKEND = "anymail.backends.sparkpost.EmailBackend"
ANYMAIL = {
"SPARKPOST_API_KEY": env("SPARKPOST_API_KEY"),
"SPARKPOST_API_URL": env(
"SPARKPOST_API_URL", default="https://api.sparkpost.com/api/v1"
),
}
{%- elif cookiecutter.mail_service == 'Other SMTP' %}
# https://anymail.readthedocs.io/en/stable/esps
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
ANYMAIL = {}
{%- endif %}
{% if cookiecutter.use_compressor == 'y' -%}
# django-compressor
@ -200,9 +233,27 @@ ANYMAIL = {
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED
COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True)
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE
{%- if cookiecutter.cloud_provider == 'AWS' %}
COMPRESS_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
{%- elif cookiecutter.cloud_provider == 'GCP' %}
COMPRESS_STORAGE = "storages.backends.gcloud.GoogleCloudStorage"
{%- elif cookiecutter.cloud_provider == 'None' %}
COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage"
{%- endif %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL
COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %} # noqa F405{% endif %}
{%- if cookiecutter.use_whitenoise == 'y' %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE
COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise
{%- endif %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_FILTERS
COMPRESS_FILTERS = {
"css": [
"compressor.filters.css_default.CssAbsoluteFilter",
"compressor.filters.cssmin.rCSSMinFilter",
],
"js": ["compressor.filters.jsmin.JSMinFilter"],
}
{% endif %}
{%- if cookiecutter.use_whitenoise == 'n' -%}
# Collectfast

View File

@ -1,10 +1,13 @@
from django.conf import settings
from django.urls import include, path
from django.conf.urls.static import static
from django.contrib import admin
from django.views.generic import TemplateView
{%- if cookiecutter.use_async == 'y' %}
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
{%- endif %}
from django.urls import include, path
from django.views import defaults as default_views
{% if cookiecutter.use_drf == 'y' -%}
from django.views.generic import TemplateView
{%- if cookiecutter.use_drf == 'y' %}
from rest_framework.authtoken.views import obtain_auth_token
{%- endif %}
@ -20,7 +23,12 @@ urlpatterns = [
path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
{% if cookiecutter.use_drf == 'y' -%}
{%- if cookiecutter.use_async == 'y' %}
if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns()
{%- endif %}
{% if cookiecutter.use_drf == 'y' %}
# API URLS
urlpatterns += [
# API base url

View File

@ -0,0 +1,13 @@
async def websocket_application(scope, receive, send):
while True:
event = await receive()
if event["type"] == "websocket.connect":
await send({"type": "websocket.accept"})
if event["type"] == "websocket.disconnect":
break
if event["type"] == "websocket.receive":
if event["text"] == "ping":
await send({"type": "websocket.send", "text": "pong!"})

View File

@ -15,15 +15,14 @@ framework.
"""
import os
import sys
from pathlib import Path
from django.core.wsgi import get_wsgi_application
# This allows easy placement of apps within the interior
# {{ cookiecutter.project_slug }} directory.
app_path = os.path.abspath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
)
sys.path.append(os.path.join(app_path, "{{ cookiecutter.project_slug }}"))
app_path = Path(__file__).parents[1].resolve()
sys.path.append(str(app_path / "{{ cookiecutter.project_slug }}"))
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use

View File

@ -14,7 +14,7 @@ This repository comes with already prepared "Run/Debug Configurations" for docke
.. image:: images/2.png
But as you can see, at the beginning there is something wrong with them. They have red X on django icon, and they cannot be used, without configuring remote python interpteter. To do that, you have to go to *Settings > Build, Execution, Deployment* first.
But as you can see, at the beginning there is something wrong with them. They have red X on django icon, and they cannot be used, without configuring remote python interpreter. To do that, you have to go to *Settings > Build, Execution, Deployment* first.
Next, you have to add new remote python interpreter, based on already tested deployment settings. Go to *Settings > Project > Project Interpreter*. Click on the cog icon, and click *Add Remote*.

View File

@ -110,6 +110,18 @@ function imgCompression() {
.pipe(dest(paths.images))
}
{% if cookiecutter.use_async == 'y' -%}
// Run django server
function asyncRunServer() {
var cmd = spawn('gunicorn', [
'config.asgi', '-k', 'uvicorn.workers.UvicornWorker', '--reload'
], {stdio: 'inherit'}
)
cmd.on('close', function(code) {
console.log('gunicorn exited with code ' + code)
})
}
{%- else %}
// Run django server
function runServer(cb) {
var cmd = spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'})
@ -118,6 +130,7 @@ function runServer(cb) {
cb(code)
})
}
{%- endif %}
// Browser sync server for live reload
function initBrowserSync() {
@ -166,8 +179,12 @@ const generateAssets = parallel(
// Set up dev environment
const dev = parallel(
{%- if cookiecutter.use_docker == 'n' %}
{%- if cookiecutter.use_async == 'y' %}
asyncRunServer,
{%- else %}
runServer,
{%- endif %}
{%- endif %}
initBrowserSync,
watchPaths
)

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
import os
import sys
from pathlib import Path
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
@ -24,7 +25,7 @@ if __name__ == "__main__":
# This allows easy placement of apps within the interior
# {{ cookiecutter.project_slug }} directory.
current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, "{{ cookiecutter.project_slug }}"))
current_path = Path(__file__).parent.resolve()
sys.path.append(str(current_path / "{{ cookiecutter.project_slug }}"))
execute_from_command_line(sys.argv)

View File

@ -1,15 +1,16 @@
import os
from pathlib import Path
from typing import Sequence
import pytest
ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__))
PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, ".envs", ".production")
ROOT_DIR_PATH = Path(__file__).parent.resolve()
PRODUCTION_DOTENVS_DIR_PATH = ROOT_DIR_PATH / ".envs" / ".production"
PRODUCTION_DOTENV_FILE_PATHS = [
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".django"),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".postgres"),
PRODUCTION_DOTENVS_DIR_PATH / ".django",
PRODUCTION_DOTENVS_DIR_PATH / ".postgres",
]
DOTENV_FILE_PATH = os.path.join(ROOT_DIR_PATH, ".env")
DOTENV_FILE_PATH = ROOT_DIR_PATH / ".env"
def merge(
@ -31,9 +32,9 @@ def main():
@pytest.mark.parametrize("merged_file_count", range(3))
@pytest.mark.parametrize("append_linesep", [True, False])
def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
tmp_dir_path = str(tmpdir_factory.getbasetemp())
tmp_dir_path = Path(str(tmpdir_factory.getbasetemp()))
output_file_path = os.path.join(tmp_dir_path, ".env")
output_file_path = tmp_dir_path / ".env"
expected_output_file_content = ""
merged_file_paths = []
@ -41,7 +42,7 @@ def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
merged_file_ord = i + 1
merged_filename = ".service{}".format(merged_file_ord)
merged_file_path = os.path.join(tmp_dir_path, merged_filename)
merged_file_path = tmp_dir_path / merged_filename
merged_file_content = merged_filename * merged_file_ord

View File

@ -42,6 +42,9 @@ services:
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
{%- if cookiecutter.use_celery == 'y' %}
- "0.0.0.0:5555:5555"
{%- endif %}
redis:
image: redis:5.0
@ -60,8 +63,6 @@ services:
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_production_flower
ports:
- "5555:5555"
command: /start-flower
{%- endif %}

View File

@ -1,6 +1,6 @@
pytz==2019.3 # https://github.com/stub42/pytz
python-slugify==4.0.0 # https://github.com/un33k/python-slugify
Pillow==7.0.0 # https://github.com/python-pillow/Pillow
Pillow==7.1.1 # https://github.com/python-pillow/Pillow
{%- if cookiecutter.use_compressor == "y" %}
rcssmin==1.0.6{% if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %} --install-option="--without-c-extensions"{% endif %} # https://github.com/ndparker/rcssmin
{%- endif %}
@ -8,27 +8,31 @@ argon2-cffi==19.2.0 # https://github.com/hynek/argon2_cffi
{%- if cookiecutter.use_whitenoise == 'y' %}
whitenoise==5.0.1 # https://github.com/evansd/whitenoise
{%- endif %}
redis==3.3.11 # pyup: != 3.4.0 # https://github.com/andymccurdy/redis-py
redis==3.4.1 # https://github.com/andymccurdy/redis-py
{%- if cookiecutter.use_celery == "y" %}
celery==4.4.0 # pyup: < 5.0 # https://github.com/celery/celery
django-celery-beat==1.6.0 # https://github.com/celery/django-celery-beat
celery==4.4.2 # pyup: < 5.0 # https://github.com/celery/celery
django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat
{%- if cookiecutter.use_docker == 'y' %}
flower==0.9.3 # https://github.com/mher/flower
flower==0.9.4 # https://github.com/mher/flower
{%- endif %}
{%- endif %}
{%- if cookiecutter.use_async == 'y' %}
uvicorn==0.11.3 # https://github.com/encode/uvicorn
gunicorn==20.0.4 # https://github.com/benoitc/gunicorn
{%- endif %}
# Django
# ------------------------------------------------------------------------------
django==2.2.9 # pyup: < 3.0 # https://www.djangoproject.com/
django==3.0.5 # pyup: < 3.1 # https://www.djangoproject.com/
django-environ==0.4.5 # https://github.com/joke2k/django-environ
django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils
django-allauth==0.41.0 # https://github.com/pennersr/django-allauth
django-crispy-forms==1.8.1 # https://github.com/django-crispy-forms/django-crispy-forms
django-crispy-forms==1.9.0 # https://github.com/django-crispy-forms/django-crispy-forms
{%- if cookiecutter.use_compressor == "y" %}
django-compressor==2.4 # https://github.com/django-compressor/django-compressor
{%- endif %}
django-redis==4.11.0 # https://github.com/niwinz/django-redis
{%- if cookiecutter.use_drf == "y" %}
# Django REST Framework
djangorestframework==3.11.0 # https://github.com/encode/django-rest-framework
coreapi==2.3.3 # https://github.com/core-api/python-client
{%- endif %}

View File

@ -1,37 +1,38 @@
-r ./base.txt
Werkzeug==0.16.1 # https://github.com/pallets/werkzeug
ipdb==0.12.3 # https://github.com/gotcha/ipdb
Sphinx==2.3.1 # https://github.com/sphinx-doc/sphinx
Werkzeug==1.0.1 # https://github.com/pallets/werkzeug
ipdb==0.13.2 # https://github.com/gotcha/ipdb
Sphinx==3.0.2 # https://github.com/sphinx-doc/sphinx
{%- if cookiecutter.use_docker == 'y' %}
psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
psycopg2==2.8.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- else %}
psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2
psycopg2-binary==2.8.5 # https://github.com/psycopg/psycopg2
{%- endif %}
# Testing
# ------------------------------------------------------------------------------
mypy==0.761 # https://github.com/python/mypy
django-stubs==1.4.0 # https://github.com/typeddjango/django-stubs
mypy==0.770 # https://github.com/python/mypy
django-stubs==1.5.0 # https://github.com/typeddjango/django-stubs
pytest==5.3.5 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar
# Code quality
# ------------------------------------------------------------------------------
flake8==3.7.9 # https://github.com/PyCQA/flake8
coverage==5.0.3 # https://github.com/nedbat/coveragepy
flake8-isort==3.0.0 # https://github.com/gforcada/flake8-isort
coverage==5.1 # https://github.com/nedbat/coveragepy
black==19.10b0 # https://github.com/ambv/black
pylint-django==2.0.13 # https://github.com/PyCQA/pylint-django
pylint-django==2.0.15 # https://github.com/PyCQA/pylint-django
{%- if cookiecutter.use_celery == 'y' %}
pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
{%- endif %}
pre-commit==2.0.1 # https://github.com/pre-commit/pre-commit
pre-commit==2.3.0 # https://github.com/pre-commit/pre-commit
# Django
# ------------------------------------------------------------------------------
factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy
django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar
django-extensions==2.2.6 # https://github.com/django-extensions/django-extensions
django-extensions==2.2.9 # https://github.com/django-extensions/django-extensions
django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin
pytest-django==3.8.0 # https://github.com/pytest-dev/pytest-django
pytest-django==3.9.0 # https://github.com/pytest-dev/pytest-django

View File

@ -2,20 +2,40 @@
-r ./base.txt
{%- if cookiecutter.use_async == 'n' %}
gunicorn==20.0.4 # https://github.com/benoitc/gunicorn
psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- endif %}
psycopg2==2.8.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- if cookiecutter.use_whitenoise == 'n' %}
Collectfast==1.3.1 # https://github.com/antonagestam/collectfast
Collectfast==2.1.0 # https://github.com/antonagestam/collectfast
{%- endif %}
{%- if cookiecutter.use_sentry == "y" %}
sentry-sdk==0.14.1 # https://github.com/getsentry/sentry-python
sentry-sdk==0.14.3 # https://github.com/getsentry/sentry-python
{%- endif %}
# Django
# ------------------------------------------------------------------------------
{%- if cookiecutter.cloud_provider == 'AWS' %}
django-storages[boto3]==1.8 # https://github.com/jschneier/django-storages
django-storages[boto3]==1.9.1 # https://github.com/jschneier/django-storages
{%- elif cookiecutter.cloud_provider == 'GCP' %}
django-storages[google]==1.8 # https://github.com/jschneier/django-storages
django-storages[google]==1.9.1 # https://github.com/jschneier/django-storages
{%- endif %}
{%- if cookiecutter.mail_service == 'Mailgun' %}
django-anymail[mailgun]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Amazon SES' %}
django-anymail[amazon_ses]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Mailjet' %}
django-anymail[mailjet]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Mandrill' %}
django-anymail[mandrill]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Postmark' %}
django-anymail[postmark]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Sendgrid' %}
django-anymail[sendgrid]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'SendinBlue' %}
django-anymail[sendinblue]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'SparkPost' %}
django-anymail[sparkpost]==7.1.0 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Other SMTP' %}
django-anymail==7.1.0 # https://github.com/anymail/django-anymail
{%- endif %}
django-anymail[mailgun]==7.0.0 # https://github.com/anymail/django-anymail

View File

@ -1 +1 @@
python-3.7.6
python-3.8.2

View File

@ -7,7 +7,7 @@ max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
[mypy]
python_version = 3.7
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
@ -21,3 +21,9 @@ django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = {{cookiecutter.project_slug}}/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin

View File

@ -1,5 +1,4 @@
import pytest
from django.test import RequestFactory
from {{ cookiecutter.project_slug }}.users.models import User
from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory
@ -13,8 +12,3 @@ def media_storage(settings, tmpdir):
@pytest.fixture
def user() -> User:
return UserFactory()
@pytest.fixture
def request_factory() -> RequestFactory:
return RequestFactory()

View File

@ -1,4 +1,5 @@
from rest_framework import serializers
from {{ cookiecutter.project_slug }}.users.models import User

View File

@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, UpdateModelMixin
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

View File

@ -1,4 +1,4 @@
from django.contrib.auth import get_user_model, forms
from django.contrib.auth import forms, get_user_model
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

View File

@ -1,12 +1,12 @@
import pytest
from celery.result import EagerResult
from {{ cookiecutter.project_slug }}.users.tasks import get_users_count
from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory
pytestmark = pytest.mark.django_db
@pytest.mark.django_db
def test_user_count(settings):
"""A basic test to execute the get_users_count Celery task."""
UserFactory.create_batch(3)

View File

@ -1,5 +1,5 @@
import pytest
from django.urls import reverse, resolve
from django.urls import resolve, reverse
from {{ cookiecutter.project_slug }}.users.models import User

View File

@ -16,18 +16,18 @@ class TestUserUpdateView:
https://github.com/pytest-dev/pytest-django/pull/258
"""
def test_get_success_url(self, user: User, request_factory: RequestFactory):
def test_get_success_url(self, user: User, rf: RequestFactory):
view = UserUpdateView()
request = request_factory.get("/fake-url/")
request = rf.get("/fake-url/")
request.user = user
view.request = request
assert view.get_success_url() == f"/users/{user.username}/"
def test_get_object(self, user: User, request_factory: RequestFactory):
def test_get_object(self, user: User, rf: RequestFactory):
view = UserUpdateView()
request = request_factory.get("/fake-url/")
request = rf.get("/fake-url/")
request.user = user
view.request = request
@ -36,9 +36,9 @@ class TestUserUpdateView:
class TestUserRedirectView:
def test_get_redirect_url(self, user: User, request_factory: RequestFactory):
def test_get_redirect_url(self, user: User, rf: RequestFactory):
view = UserRedirectView()
request = request_factory.get("/fake-url")
request = rf.get("/fake-url")
request.user = user
view.request = request

View File

@ -1,9 +1,9 @@
from django.urls import path
from {{ cookiecutter.project_slug }}.users.views import (
user_detail_view,
user_redirect_view,
user_update_view,
user_detail_view,
)
app_name = "users"

View File

@ -1,9 +1,9 @@
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views.generic import DetailView, RedirectView, UpdateView
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, RedirectView, UpdateView
User = get_user_model()

View File

@ -0,0 +1,25 @@
{% if cookiecutter.cloud_provider == 'AWS' -%}
from storages.backends.s3boto3 import S3Boto3Storage
class StaticRootS3Boto3Storage(S3Boto3Storage):
location = "static"
default_acl = "public-read"
class MediaRootS3Boto3Storage(S3Boto3Storage):
location = "media"
file_overwrite = False
{%- elif cookiecutter.cloud_provider == 'GCP' -%}
from storages.backends.gcloud import GoogleCloudStorage
class StaticRootGoogleCloudStorage(GoogleCloudStorage):
location = "static"
default_acl = "publicRead"
class MediaRootGoogleCloudStorage(GoogleCloudStorage):
location = "media"
file_overwrite = False
{%- endif %}