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 # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: pydanny
patreon: danielroygreenfeld patreon: roygreenfeld
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 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 # allowed: True, False
pin: True 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 # Specify requirement files by hand, pyup seems to struggle to
# find the ones in the project_slug folder # find the ones in the project_slug folder
requirements: requirements:

View File

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

View File

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

View File

@ -36,8 +36,8 @@ production-ready Django projects quickly.
Features Features
--------- ---------
* For Django 2.2 * For Django 3.0
* Works with Python 3.7 * Works with Python 3.8
* Renders Django projects with 100% starting test coverage * Renders Django projects with 100% starting test coverage
* Twitter Bootstrap_ v4 (`maintained Foundation fork`_ also available) * Twitter Bootstrap_ v4 (`maintained Foundation fork`_ also available)
* 12-Factor_ based settings via django-environ_ * 12-Factor_ based settings via django-environ_
@ -45,8 +45,9 @@ Features
* Optimized development and production settings * Optimized development and production settings
* Registration via django-allauth_ * Registration via django-allauth_
* Comes with custom user model ready to go * Comes with custom user model ready to go
* Optional basic ASGI setup for Websockets
* Optional custom static build using Gulp and livereload * 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 * Media storage using Amazon S3 or Google Cloud Storage
* Docker support using docker-compose_ for development and production (using Traefik_ with LetsEncrypt_ support) * Docker support using docker-compose_ for development and production (using Traefik_ with LetsEncrypt_ support)
* Procfile_ for deploying to Heroku * 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: 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 .. image:: https://cdn.shopify.com/s/files/1/0304/6901/files/Django-Crash-Course-300x436.jpg
:name: Two Scoops of Django 1.11 Cover :name: Django Crash Course: Covers Django 3.0 and Python 3.8
:align: center :align: center
:alt: Two Scoops of Django :alt: Django Crash Course
:target: http://twoscoopspress.com/products/two-scoops-of-django-1-11 :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 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:: 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:: 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. * cookiecutter_ so it gets listed in the README as a template.
* The cookiecutter grid_ on Django Packages. * 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/ .. _grid: https://www.djangopackages.com/grids/g/cookiecutters/
Submit a Pull Request Submit a Pull Request

View File

@ -33,6 +33,18 @@
"GCP", "GCP",
"None" "None"
], ],
"mail_service": [
"Mailgun",
"Amazon SES",
"Mailjet",
"Mandrill",
"Postmark",
"Sendgrid",
"SendinBlue",
"SparkPost",
"Other SMTP"
],
"use_async": "n",
"use_drf": "n", "use_drf": "n",
"custom_bootstrap_compilation": "n", "custom_bootstrap_compilation": "n",
"use_compressor": "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 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* 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 git clone <my-repo-url> # you can also use hg
cd my-project-name 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 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 source $VIRTUAL_ENV/bin/postactivate
python manage.py migrate python manage.py migrate
python manage.py collectstatic python manage.py collectstatic
{%- if cookiecutter.use_compressor == "y" %}python manage.py compress {%- endif %}
# and, optionally # and, optionally
python manage.py createsuperuser python manage.py createsuperuser
@ -175,6 +176,7 @@ For subsequent deployments, the procedure is much simpler. In a Bash console:
git pull git pull
python manage.py migrate python manage.py migrate
python manage.py collectstatic python manage.py collectstatic
{%- if cookiecutter.use_compressor == "y" %}python manage.py compress {%- endif %}
And then go to the Web tab and hit **Reload** 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; * ``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`` 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 .. _`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: Make sure to have the following on your host:
* Python 3.7 * Python 3.8
* PostgreSQL_. * PostgreSQL_.
* Redis_, if using Celery * Redis_, if using Celery
@ -17,7 +17,7 @@ First things first.
#. Create a virtualenv: :: #. Create a virtualenv: ::
$ python3.7 -m venv <virtual env path> $ python3.8 -m venv <virtual env path>
#. Activate the virtualenv you have just created: :: #. Activate the virtualenv you have just created: ::
@ -68,10 +68,14 @@ First things first.
$ python manage.py migrate $ 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 $ 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/ .. _PostgreSQL: https://www.postgresql.org/download/
.. _Redis: https://redis.io/download .. _Redis: https://redis.io/download
.. _createdb: https://www.postgresql.org/docs/current/static/app-createdb.html .. _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. 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. A Cookiecutter_ template for Django.
.. _cookiecutter: https://github.com/audreyr/cookiecutter .. _cookiecutter: https://github.com/cookiecutter/cookiecutter
Contents: Contents:

View File

@ -70,6 +70,25 @@ cloud_provider:
Note that if you choose no cloud provider, media files won't work. 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: custom_bootstrap_compilation:
Indicates whether the project should support Bootstrap recompilation Indicates whether the project should support Bootstrap recompilation
via the selected JavaScript task runner's task. This can be useful via the selected JavaScript task runner's task. This can be useful
@ -98,8 +117,8 @@ ci_tool:
Select a CI tool for running tests. The choices are: Select a CI tool for running tests. The choices are:
1. None 1. None
2. Travis_ 2. `Travis CI`_
3. Gitlab_ 3. `Gitlab CI`_
keep_local_envs_in_vcs: keep_local_envs_in_vcs:
Indicates whether the project's ``.envs/.local/`` should be kept in VCS Indicates whether the project's ``.envs/.local/`` should be kept in VCS
@ -129,6 +148,18 @@ debug:
.. _AWS: https://aws.amazon.com/s3/ .. _AWS: https://aws.amazon.com/s3/
.. _GCP: https://cloud.google.com/storage/ .. _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 .. _Django Compressor: https://github.com/django-compressor/django-compressor
.. _Celery: https://github.com/celery/celery .. _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_API_KEY MAILGUN_API_KEY n/a raises error
MAILGUN_DOMAIN MAILGUN_SENDER_DOMAIN 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" 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) 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(): def remove_dottravisyml_file():
os.remove(".travis.yml") os.remove(".travis.yml")
@ -292,6 +301,10 @@ def remove_drf_starter_files():
shutil.rmtree(os.path.join("{{cookiecutter.project_slug}}", "users", "api")) 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(): def main():
debug = "{{ cookiecutter.debug }}".lower() == "y" debug = "{{ cookiecutter.debug }}".lower() == "y"
@ -352,6 +365,7 @@ def main():
WARNING + "You chose not to use a cloud provider, " WARNING + "You chose not to use a cloud provider, "
"media files won't be served in production." + TERMINATOR "media files won't be served in production." + TERMINATOR
) )
remove_storages_module()
if "{{ cookiecutter.use_celery }}".lower() == "n": if "{{ cookiecutter.use_celery }}".lower() == "n":
remove_celery_files() remove_celery_files()
@ -367,6 +381,9 @@ def main():
if "{{ cookiecutter.use_drf }}".lower() == "n": if "{{ cookiecutter.use_drf }}".lower() == "n":
remove_drf_starter_files() remove_drf_starter_files()
if "{{ cookiecutter.use_async }}".lower() == "n":
remove_async_files()
print(SUCCESS + "Project initialized, keep up the good work!" + TERMINATOR) 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: if python_major_version == 2:
print( print(
WARNING + "You're running cookiecutter under Python 2, but the generated " 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"]) yes_options, no_options = frozenset(["y"]), frozenset(["n"])
while True: while True:
@ -68,3 +68,15 @@ if (
"You should either use Whitenoise or select a Cloud Provider to serve static files" "You should either use Whitenoise or select a Cloud Provider to serve static files"
) )
sys.exit(1) 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] [pytest]
addopts = -x --tb=short addopts = -v --tb=short
python_paths = . python_paths = .
norecursedirs = .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/* 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 sh==1.12.14
binaryornot==0.4.4 binaryornot==0.4.4
@ -6,12 +6,12 @@ binaryornot==0.4.4
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
black==19.10b0 black==19.10b0
flake8==3.7.9 flake8==3.7.9
flake8-isort==3.0.0
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
tox==3.14.3 tox==3.14.6
pytest==5.3.5 pytest==5.4.1
pytest_cases==1.12.1 pytest-cookies==0.5.1
pytest-cookies==0.4.0 pytest-instafail==0.4.1.post0
pytest-xdist==1.31.0 pyyaml==5.3.1
pyyaml==5.3

View File

@ -10,7 +10,7 @@ except ImportError:
# Our version ALWAYS matches the version of Django we support # 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. # 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": if sys.argv[-1] == "tag":
os.system(f'git tag -a {version} -m "version {version}"') os.system(f'git tag -a {version} -m "version {version}"')
@ -34,13 +34,13 @@ setup(
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Environment :: Console", "Environment :: Console",
"Framework :: Django :: 2.2", "Framework :: Django :: 3.0",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Natural Language :: English", "Natural Language :: English",
"License :: OSI Approved :: BSD License", "License :: OSI Approved :: BSD License",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development", "Topic :: Software Development",
], ],

View File

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

12
tox.ini
View File

@ -1,18 +1,10 @@
[tox] [tox]
skipsdist = true skipsdist = true
envlist = py37,flake8,black,black-template envlist = py38,black-template
[testenv] [testenv]
deps = -rrequirements.txt deps = -rrequirements.txt
commands = pytest -m "not flake8" -m "not black" {posargs:./tests} commands = pytest {posargs:./tests}
[testenv:flake8]
deps = -rrequirements.txt
commands = pytest -m flake8 {posargs:./tests}
[testenv:black]
deps = -rrequirements.txt
commands = pytest -m black {posargs:./tests}
[testenv:black-template] [testenv:black-template]
deps = black 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 indent_size = 4
[*.py] [*.py]
line_length = 120 line_length = 88
known_first_party = {{ cookiecutter.project_slug }} known_first_party = {{cookiecutter.project_slug}},config
multi_line_output = 3 multi_line_output = 3
default_section = THIRDPARTY default_section = THIRDPARTY
recursive = true recursive = true

View File

@ -13,9 +13,26 @@ DJANGO_SECURE_SSL_REDIRECT=False
# Email # Email
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
MAILGUN_API_KEY=
DJANGO_SERVER_EMAIL= DJANGO_SERVER_EMAIL=
{% if cookiecutter.mail_service == 'Mailgun' %}
MAILGUN_API_KEY=
MAILGUN_DOMAIN= 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' %} {% if cookiecutter.cloud_provider == 'AWS' %}
# AWS # AWS
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -31,11 +48,7 @@ DJANGO_GCP_STORAGE_BUCKET_NAME=
# django-allauth # django-allauth
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DJANGO_ACCOUNT_ALLOW_REGISTRATION=True DJANGO_ACCOUNT_ALLOW_REGISTRATION=True
{% if cookiecutter.use_compressor == 'y' %}
# django-compressor
# ------------------------------------------------------------------------------
COMPRESS_ENABLED=
{% endif %}
# Gunicorn # Gunicorn
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
WEB_CONCURRENCY=4 WEB_CONCURRENCY=4

View File

@ -6,6 +6,10 @@ variables:
POSTGRES_USER: '{{ cookiecutter.project_slug }}' POSTGRES_USER: '{{ cookiecutter.project_slug }}'
POSTGRES_PASSWORD: '' POSTGRES_PASSWORD: ''
POSTGRES_DB: 'test_{{ cookiecutter.project_slug }}' 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: flake8:
stage: lint stage: lint
@ -18,8 +22,21 @@ flake8:
pytest: pytest:
stage: test stage: test
image: python:3.7 image: python:3.7
{% if cookiecutter.use_docker == 'y' -%}
tags: tags:
- docker - 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: services:
- postgres:11 - postgres:11
variables: variables:
@ -30,4 +47,5 @@ pytest:
script: script:
- pytest - pytest
{%- endif %}

View File

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <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" <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)" 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"> module-name="{{ cookiecutter.project_slug }}" is-module-sdk="true">

View File

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

View File

@ -1,6 +1,33 @@
dist: xenial dist: xenial
language: python
python:
- "3.8"
services: services:
- postgresql - {% 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: before_install:
- sudo apt-get update -qq - 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 build-essential gettext python-dev zlib1g-dev libpq-dev xvfb
@ -10,8 +37,9 @@ before_install:
- sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm - sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm
language: python language: python
python: python:
- "3.7" - "3.8"
install: install:
- pip install -r requirements/local.txt - pip install -r requirements/local.txt
script: script:
- "pytest" - "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 You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 %} {% endif %}

View File

@ -1,5 +1,10 @@
release: python manage.py migrate 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 web: gunicorn config.wsgi:application
{%- endif %}
{% if cookiecutter.use_celery == "y" -%} {% if cookiecutter.use_celery == "y" -%}
worker: celery worker --app=config.celery_app --loglevel=info worker: celery worker --app=config.celery_app --loglevel=info
beat: celery beat --app=config.celery_app --loglevel=info
{%- endif %} {%- endif %}

View File

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

View File

@ -6,4 +6,8 @@ set -o nounset
python manage.py migrate 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 python manage.py runserver_plus 0.0.0.0:8000
{% endif %}

View File

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

View File

@ -6,4 +6,29 @@ set -o nounset
python /app/manage.py collectstatic --noinput 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 /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app
{%- endif %}

View File

@ -9,6 +9,11 @@ entryPoints:
web-secure: web-secure:
# https # https
address: ":443" address: ":443"
{%- if cookiecutter.use_celery == 'y' %}
flower:
address: ":5555"
{%- endif %}
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
@ -23,7 +28,7 @@ certificatesResolvers:
http: http:
routers: routers:
web-router: web-router:
rule: "Host(`{{ cookiecutter.domain_name }}`)" rule: "Host(`{{ cookiecutter.domain_name }}`) || Host(`www.{{ cookiecutter.domain_name }}`)"
entryPoints: entryPoints:
- web - web
middlewares: middlewares:
@ -32,7 +37,7 @@ http:
service: django service: django
web-secure-router: web-secure-router:
rule: "Host(`{{ cookiecutter.domain_name }}`)" rule: "Host(`{{ cookiecutter.domain_name }}`) || Host(`www.{{ cookiecutter.domain_name }}`)"
entryPoints: entryPoints:
- web-secure - web-secure
middlewares: middlewares:
@ -41,6 +46,17 @@ http:
tls: tls:
# https://docs.traefik.io/master/routing/routers/#certresolver # https://docs.traefik.io/master/routing/routers/#certresolver
certResolver: letsencrypt 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: middlewares:
redirect: redirect:
@ -52,13 +68,20 @@ http:
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
# https://docs.djangoproject.com/en/dev/ref/csrf/#ajax # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
headers: headers:
hostsProxyHeaders: ['X-CSRFToken'] hostsProxyHeaders: ["X-CSRFToken"]
services: services:
django: django:
loadBalancer: loadBalancer:
servers: servers:
- url: http://django:5000 - url: http://django:5000
{%- if cookiecutter.use_celery == 'y' %}
flower:
loadBalancer:
servers:
- url: http://flower:5555
{%- endif %}
providers: providers:
# https://docs.traefik.io/master/providers/file/ # 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 django.conf import settings
from rest_framework.routers import DefaultRouter, SimpleRouter
from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet
if settings.DEBUG: 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 import os
from celery import Celery from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.

View File

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

View File

@ -68,7 +68,7 @@ if env("USE_DOCKER") == "yes":
import socket import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) 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 %} {%- endif %}
# django-extensions # django-extensions

View File

@ -2,7 +2,6 @@
import logging import logging
import sentry_sdk import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.logging import LoggingIntegration
{%- if cookiecutter.use_celery == 'y' %} {%- if cookiecutter.use_celery == 'y' %}
@ -103,11 +102,11 @@ GS_DEFAULT_ACL = "publicRead"
{% if cookiecutter.use_whitenoise == 'y' -%} {% if cookiecutter.use_whitenoise == 'y' -%}
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
{% elif cookiecutter.cloud_provider == 'AWS' -%} {% 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" COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy"
STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/" STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/"
{% elif cookiecutter.cloud_provider == 'GCP' -%} {% 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" COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy"
STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/" STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/"
{% endif -%} {% endif -%}
@ -115,39 +114,10 @@ STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/"
# MEDIA # MEDIA
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
{%- if cookiecutter.cloud_provider == 'AWS' %} {%- if cookiecutter.cloud_provider == 'AWS' %}
# region http://stackoverflow.com/questions/10390244/ DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootS3Boto3Storage"
# 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"
MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/" MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/"
{%- elif cookiecutter.cloud_provider == 'GCP' %} {%- elif cookiecutter.cloud_provider == 'GCP' %}
from storages.backends.gcloud import GoogleCloudStorage # noqa E402 DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootGoogleCloudStorage"
class StaticRootGoogleCloudStorage(GoogleCloudStorage):
location = "static"
default_acl = "publicRead"
class MediaRootGoogleCloudStorage(GoogleCloudStorage):
location = "media"
file_overwrite = False
DEFAULT_FILE_STORAGE = "config.settings.production.MediaRootGoogleCloudStorage"
MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/"
{%- endif %} {%- endif %}
@ -182,17 +152,80 @@ EMAIL_SUBJECT_PREFIX = env(
# Django Admin URL regex. # Django Admin URL regex.
ADMIN_URL = env("DJANGO_ADMIN_URL") ADMIN_URL = env("DJANGO_ADMIN_URL")
# Anymail (Mailgun) # Anymail
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail
INSTALLED_APPS += ["anymail"] # noqa F405 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 # 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 = { ANYMAIL = {
"MAILGUN_API_KEY": env("MAILGUN_API_KEY"), "MAILGUN_API_KEY": env("MAILGUN_API_KEY"),
"MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"), "MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"),
"MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"), "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' -%} {% if cookiecutter.use_compressor == 'y' -%}
# django-compressor # django-compressor
@ -200,9 +233,27 @@ ANYMAIL = {
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED
COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True) COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True)
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE
{%- if cookiecutter.cloud_provider == 'AWS' %}
COMPRESS_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 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 # 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 %} 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 %} {% endif %}
{%- if cookiecutter.use_whitenoise == 'n' -%} {%- if cookiecutter.use_whitenoise == 'n' -%}
# Collectfast # Collectfast

View File

@ -1,10 +1,13 @@
from django.conf import settings from django.conf import settings
from django.urls import include, path
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin 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 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 from rest_framework.authtoken.views import obtain_auth_token
{%- endif %} {%- endif %}
@ -20,7 +23,12 @@ urlpatterns = [
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here # Your stuff: custom urls includes go here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + 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 # API URLS
urlpatterns += [ urlpatterns += [
# API base url # 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 os
import sys import sys
from pathlib import Path
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
# This allows easy placement of apps within the interior # This allows easy placement of apps within the interior
# {{ cookiecutter.project_slug }} directory. # {{ cookiecutter.project_slug }} directory.
app_path = os.path.abspath( app_path = Path(__file__).parents[1].resolve()
os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) sys.path.append(str(app_path / "{{ cookiecutter.project_slug }}"))
)
sys.path.append(os.path.join(app_path, "{{ cookiecutter.project_slug }}"))
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use # if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use # mod_wsgi daemon mode with each site in its own daemon process, or use

View File

@ -14,7 +14,7 @@ This repository comes with already prepared "Run/Debug Configurations" for docke
.. image:: images/2.png .. 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*. 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)) .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 // Run django server
function runServer(cb) { function runServer(cb) {
var cmd = spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'}) var cmd = spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'})
@ -118,6 +130,7 @@ function runServer(cb) {
cb(code) cb(code)
}) })
} }
{%- endif %}
// Browser sync server for live reload // Browser sync server for live reload
function initBrowserSync() { function initBrowserSync() {
@ -166,8 +179,12 @@ const generateAssets = parallel(
// Set up dev environment // Set up dev environment
const dev = parallel( const dev = parallel(
{%- if cookiecutter.use_docker == 'n' %} {%- if cookiecutter.use_docker == 'n' %}
{%- if cookiecutter.use_async == 'y' %}
asyncRunServer,
{%- else %}
runServer, runServer,
{%- endif %} {%- endif %}
{%- endif %}
initBrowserSync, initBrowserSync,
watchPaths watchPaths
) )

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
pytz==2019.3 # https://github.com/stub42/pytz pytz==2019.3 # https://github.com/stub42/pytz
python-slugify==4.0.0 # https://github.com/un33k/python-slugify 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" %} {%- 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 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 %} {%- endif %}
@ -8,27 +8,31 @@ argon2-cffi==19.2.0 # https://github.com/hynek/argon2_cffi
{%- if cookiecutter.use_whitenoise == 'y' %} {%- if cookiecutter.use_whitenoise == 'y' %}
whitenoise==5.0.1 # https://github.com/evansd/whitenoise whitenoise==5.0.1 # https://github.com/evansd/whitenoise
{%- endif %} {%- 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" %} {%- if cookiecutter.use_celery == "y" %}
celery==4.4.0 # pyup: < 5.0 # https://github.com/celery/celery celery==4.4.2 # pyup: < 5.0 # https://github.com/celery/celery
django-celery-beat==1.6.0 # https://github.com/celery/django-celery-beat django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat
{%- if cookiecutter.use_docker == 'y' %} {%- 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 %}
{%- 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
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-environ==0.4.5 # https://github.com/joke2k/django-environ
django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils 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-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" %} {%- if cookiecutter.use_compressor == "y" %}
django-compressor==2.4 # https://github.com/django-compressor/django-compressor django-compressor==2.4 # https://github.com/django-compressor/django-compressor
{%- endif %} {%- endif %}
django-redis==4.11.0 # https://github.com/niwinz/django-redis django-redis==4.11.0 # https://github.com/niwinz/django-redis
{%- if cookiecutter.use_drf == "y" %}
# Django REST Framework # Django REST Framework
djangorestframework==3.11.0 # https://github.com/encode/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 -r ./base.txt
Werkzeug==0.16.1 # https://github.com/pallets/werkzeug Werkzeug==1.0.1 # https://github.com/pallets/werkzeug
ipdb==0.12.3 # https://github.com/gotcha/ipdb ipdb==0.13.2 # https://github.com/gotcha/ipdb
Sphinx==2.3.1 # https://github.com/sphinx-doc/sphinx Sphinx==3.0.2 # https://github.com/sphinx-doc/sphinx
{%- if cookiecutter.use_docker == 'y' %} {%- 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 %} {%- else %}
psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2 psycopg2-binary==2.8.5 # https://github.com/psycopg/psycopg2
{%- endif %} {%- endif %}
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
mypy==0.761 # https://github.com/python/mypy mypy==0.770 # https://github.com/python/mypy
django-stubs==1.4.0 # https://github.com/typeddjango/django-stubs django-stubs==1.5.0 # https://github.com/typeddjango/django-stubs
pytest==5.3.5 # https://github.com/pytest-dev/pytest pytest==5.3.5 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar
# Code quality # Code quality
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
flake8==3.7.9 # https://github.com/PyCQA/flake8 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 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' %} {%- if cookiecutter.use_celery == 'y' %}
pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
{%- endif %} {%- 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 # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy
django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar 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 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 -r ./base.txt
{%- if cookiecutter.use_async == 'n' %}
gunicorn==20.0.4 # https://github.com/benoitc/gunicorn 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' %} {%- if cookiecutter.use_whitenoise == 'n' %}
Collectfast==1.3.1 # https://github.com/antonagestam/collectfast Collectfast==2.1.0 # https://github.com/antonagestam/collectfast
{%- endif %} {%- endif %}
{%- if cookiecutter.use_sentry == "y" %} {%- 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 %} {%- endif %}
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
{%- if cookiecutter.cloud_provider == 'AWS' %} {%- 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' %} {%- 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 %} {%- 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 exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
[mypy] [mypy]
python_version = 3.7 python_version = 3.8
check_untyped_defs = True check_untyped_defs = True
ignore_missing_imports = True ignore_missing_imports = True
warn_unused_ignores = True warn_unused_ignores = True
@ -21,3 +21,9 @@ django_settings_module = config.settings.test
[mypy-*.migrations.*] [mypy-*.migrations.*]
# Django migrations should not produce any errors: # Django migrations should not produce any errors:
ignore_errors = True ignore_errors = True
[coverage:run]
include = {{cookiecutter.project_slug}}/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action 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.response import Response
from rest_framework.viewsets import GenericViewSet 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.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

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

View File

@ -1,5 +1,5 @@
import pytest import pytest
from django.urls import reverse, resolve from django.urls import resolve, reverse
from {{ cookiecutter.project_slug }}.users.models import User 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 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() view = UserUpdateView()
request = request_factory.get("/fake-url/") request = rf.get("/fake-url/")
request.user = user request.user = user
view.request = request view.request = request
assert view.get_success_url() == f"/users/{user.username}/" 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() view = UserUpdateView()
request = request_factory.get("/fake-url/") request = rf.get("/fake-url/")
request.user = user request.user = user
view.request = request view.request = request
@ -36,9 +36,9 @@ class TestUserUpdateView:
class TestUserRedirectView: 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() view = UserRedirectView()
request = request_factory.get("/fake-url") request = rf.get("/fake-url")
request.user = user request.user = user
view.request = request view.request = request

View File

@ -1,9 +1,9 @@
from django.urls import path from django.urls import path
from {{ cookiecutter.project_slug }}.users.views import ( from {{ cookiecutter.project_slug }}.users.views import (
user_detail_view,
user_redirect_view, user_redirect_view,
user_update_view, user_update_view,
user_detail_view,
) )
app_name = "users" 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 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, RedirectView, UpdateView
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, RedirectView, UpdateView
User = get_user_model() 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 %}