diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f8d11cc..304732b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All enhancements and patches to Cookiecutter Django will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [2018-02-16] +### Changed +- Upgraded to Django 2.0 (@epicwhale) + ## [2018-01-15] ### Changed - Removed Elastic Beanstalk support (@pydanny) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index efff1fad9..cfe16740d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -36,13 +36,12 @@ To run all tests using various versions of python in virtualenvs defined in tox. $ tox -It is possible to tests with some versions of python, to do this the command +It is possible to test with a specific version of python. To do this, the command is:: - $ tox -e py34,py35 + $ tox -e py36 -Will run py.test with the python3.4, and python3.5 interpreters, for -example. +This will run py.test with the python3.6 interpreter, for example. To run a particular test with tox for against your current Python version:: diff --git a/README.rst b/README.rst index 6536ab7ec..939ad4a9a 100644 --- a/README.rst +++ b/README.rst @@ -38,10 +38,10 @@ production-ready Django projects quickly. Features --------- -* For Django 1.11 +* For Django 2.0 * Works with Python 3.6 * Renders Django projects with 100% starting test coverage -* Twitter Bootstrap_ v4.0.0 - beta 1 (`maintained Foundation fork`_ also available) +* Twitter Bootstrap_ v4.0.0 (`maintained Foundation fork`_ also available) * 12-Factor_ based settings via django-environ_ * Secure by default. We believe in SSL. * Optimized development and production settings @@ -111,7 +111,7 @@ Two Scoops of Django 1.11 :name: Two Scoops of Django 1.11 Cover :align: center :alt: Two Scoops of Django - :target: http://twoscoopspress.org/products/two-scoops-of-django-1-11 + :target: http://twoscoopspress.com/products/two-scoops-of-django-1-11 Two Scoops of Django is the best dessert-themed Django reference in the universe diff --git a/docs/deployment-on-pythonanywhere.rst b/docs/deployment-on-pythonanywhere.rst index 07b46bc05..d70684bd9 100644 --- a/docs/deployment-on-pythonanywhere.rst +++ b/docs/deployment-on-pythonanywhere.rst @@ -35,7 +35,7 @@ Make sure your project is fully commited and pushed up to Bitbucket or Github or git clone # you can also use hg cd my-project-name - mkvirtualenv --python=/usr/bin/python3.5 my-project-name + mkvirtualenv --python=/usr/bin/python3.6 my-project-name pip install -r requirements/production.txt # may take a few minutes diff --git a/docs/settings.rst b/docs/settings.rst index 8b4ad4053..20bf9e5e1 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -22,7 +22,7 @@ DJANGO_ADMIN_URL n/a r'^admin/' DJANGO_CACHES CACHES (default) locmem redis DJANGO_DATABASES DATABASES (default) See code See code DJANGO_DEBUG DEBUG True False -DJANGO_SECRET_KEY SECRET_KEY CHANGEME!!! raises error +DJANGO_SECRET_KEY SECRET_KEY !!!SET DJANGO_SECRET_KEY!!! raises error DJANGO_SECURE_BROWSER_XSS_FILTER SECURE_BROWSER_XSS_FILTER n/a True DJANGO_SECURE_SSL_REDIRECT SECURE_SSL_REDIRECT n/a True DJANGO_SECURE_CONTENT_TYPE_NOSNIFF SECURE_CONTENT_TYPE_NOSNIFF n/a True diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 9a6da7b67..0e16ffb70 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,238 +1,283 @@ """ -Does the following: +NOTE: + the below code is to be maintained Python 2.x-compatible + as the whole Cookiecutter Django project initialization + can potentially be run in Python 2.x environment + (at least so we presume in `pre_gen_project.py`). -1. Generates and saves random secret key -2. Removes the taskapp if celery isn't going to be used -3. Removes the .idea directory if PyCharm isn't going to be used -4. Copy files from /docs/ to {{ cookiecutter.project_slug }}/docs/ - - TODO: this might have to be moved to a pre_gen_hook - -A portion of this code was adopted from Django's standard crypto functions and -utilities, specifically: - https://github.com/django/django/blob/master/django/utils/crypto.py +TODO: ? restrict Cookiecutter Django project initialization to Python 3.x environments only """ -from __future__ import print_function + import os import random import shutil import string +import sys -# Get the root project directory -PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) - -# Use the system PRNG if possible try: + # Inspired by + # https://github.com/django/django/blob/master/django/utils/crypto.py random = random.SystemRandom() using_sysrandom = True except NotImplementedError: using_sysrandom = False - -def get_random_string(length=50): - """ - Returns a securely generated random string. - The default length of 12 with the a-z, A-Z, 0-9 character set returns - a 71-bit value. log_2((26+26+10)^12) =~ 71 bits - """ - punctuation = string.punctuation.replace('"', '').replace("'", '') - punctuation = punctuation.replace('\\', '') - if using_sysrandom: - return ''.join(random.choice( - string.digits + string.ascii_letters + punctuation - ) for i in range(length)) - - print( - "Cookiecutter Django couldn't find a secure pseudo-random number generator on your system." - " Please change change your SECRET_KEY variables in conf/settings/local.py and env.example" - " manually." - ) - return "CHANGEME!!" +PROJECT_DIR_PATH = os.path.realpath(os.path.curdir) -def set_secret_key(setting_file_location): - # Open locals.py - with open(setting_file_location) as f: - file_ = f.read() - - # Generate a SECRET_KEY that matches the Django standard - SECRET_KEY = get_random_string() - - # Replace "CHANGEME!!!" with SECRET_KEY - file_ = file_.replace('CHANGEME!!!', SECRET_KEY, 1) - - # Write the results to the locals.py module - with open(setting_file_location, 'w') as f: - f.write(file_) +def remove_file(file_path): + if os.path.exists(file_path): + os.remove(file_path) -def make_secret_key(project_directory): - """Generates and saves random secret key""" - # Determine the local_setting_file_location - local_setting = os.path.join( - project_directory, - 'config/settings/local.py' - ) - - # local.py settings file - set_secret_key(local_setting) - - env_file = os.path.join( - project_directory, - 'env.example' - ) - - # env.example file - set_secret_key(env_file) +def remove_open_source_project_only_files(): + file_names = [ + 'CONTRIBUTORS.txt', + ] + for file_name in file_names: + os.remove(os.path.join(PROJECT_DIR_PATH, file_name)) -def remove_file(file_name): - if os.path.exists(file_name): - os.remove(file_name) +def remove_gplv3_files(): + file_names = [ + 'COPYING', + ] + for file_name in file_names: + os.remove(os.path.join(PROJECT_DIR_PATH, file_name)) -def remove_task_app(project_directory): - """Removes the taskapp if celery isn't going to be used""" - # Determine the local_setting_file_location - task_app_location = os.path.join( - PROJECT_DIRECTORY, - '{{ cookiecutter.project_slug }}/taskapp' - ) - shutil.rmtree(task_app_location) +def remove_pycharm_files(): + idea_dir_path = os.path.join(PROJECT_DIR_PATH, '.idea') + if os.path.exists(idea_dir_path): + shutil.rmtree(idea_dir_path) - -def remove_pycharm_dir(project_directory): - """ - Removes directories related to PyCharm - if it isn't going to be used - """ - idea_dir_location = os.path.join(PROJECT_DIRECTORY, '.idea/') - if os.path.exists(idea_dir_location): - shutil.rmtree(idea_dir_location) - - docs_dir_location = os.path.join(PROJECT_DIRECTORY, 'docs/pycharm/') - if os.path.exists(docs_dir_location): - shutil.rmtree(docs_dir_location) - - -def remove_heroku_files(): - """ - Removes files needed for heroku if it isn't going to be used - """ - filenames = ["Procfile", "runtime.txt"] - for filename in ["Procfile", "runtime.txt"]: - file_name = os.path.join(PROJECT_DIRECTORY, filename) - remove_file(file_name) + docs_dir_path = os.path.join(PROJECT_DIR_PATH, 'docs', 'pycharm') + if os.path.exists(docs_dir_path): + shutil.rmtree(docs_dir_path) def remove_docker_files(): - """ - Removes files needed for docker if it isn't going to be used - """ - for filename in ["local.yml", "production.yml", ".dockerignore"]: - filename = os.path.join(PROJECT_DIRECTORY, filename) - if os.path.exists(filename): - os.remove(filename) + shutil.rmtree(os.path.join(PROJECT_DIR_PATH, 'compose')) - shutil.rmtree(os.path.join( - PROJECT_DIRECTORY, "compose" - )) + file_names = [ + 'local.yml', + 'production.yml', + '.dockerignore', + ] + for file_name in file_names: + os.remove(os.path.join(PROJECT_DIR_PATH, file_name)) + + +def remove_heroku_files(): + file_names = [ + 'Procfile', + 'runtime.txt', + ] + for file_name in file_names: + remove_file(os.path.join(PROJECT_DIR_PATH, file_name)) + + +def remove_paas_files(): + none_paas_files_left = True + + if '{{ cookiecutter.use_heroku }}'.lower() == 'n': + remove_heroku_files() + none_paas_files_left &= True + else: + none_paas_files_left &= False + + if none_paas_files_left: + remove_file(os.path.join(PROJECT_DIR_PATH, 'requirements.txt')) def remove_grunt_files(): - """ - Removes files needed for grunt if it isn't going to be used - """ - for filename in ["Gruntfile.js"]: - os.remove(os.path.join( - PROJECT_DIRECTORY, filename - )) + file_names = [ + 'Gruntfile.js', + ] + for file_name in file_names: + os.remove(os.path.join(PROJECT_DIR_PATH, file_name)) + def remove_gulp_files(): + file_names = [ + 'gulpfile.js', + ] + for file_name in file_names: + os.remove(os.path.join(PROJECT_DIR_PATH, file_name)) + + +def remove_packagejson_file(): + file_names = [ + 'package.json', + ] + for file_name in file_names: + os.remove(os.path.join(PROJECT_DIR_PATH, file_name)) + + +def remove_celery_app(): + shutil.rmtree(os.path.join(PROJECT_DIR_PATH, '{{ cookiecutter.project_slug }}', 'taskapp')) + + +def append_to_project_gitignore(path): + gitignore_file_path = os.path.join(PROJECT_DIR_PATH, '.gitignore') + with open(gitignore_file_path, 'a') as gitignore_file: + gitignore_file.write(path) + gitignore_file.write(os.linesep) + + +def generate_random_string(length, + using_digits=False, + using_ascii_letters=False, + using_punctuation=False): """ - Removes files needed for grunt if it isn't going to be used + Example: + opting out for 50 symbol-long, [a-z][A-Z][0-9] string + would yield log_2((26+26+50)^50) ~= 334 bit strength. """ - for filename in ["gulpfile.js"]: - os.remove(os.path.join( - PROJECT_DIRECTORY, filename - )) + if not using_sysrandom: + return None -def remove_packageJSON_file(): - """ - Removes files needed for grunt if it isn't going to be used - """ - for filename in ["package.json"]: - os.remove(os.path.join( - PROJECT_DIRECTORY, filename - )) - -def remove_copying_files(): - """ - Removes files needed for the GPLv3 licence if it isn't going to be used - """ - for filename in ["COPYING"]: - os.remove(os.path.join( - PROJECT_DIRECTORY, filename - )) + symbols = [] + if using_digits: + symbols += string.digits + if using_ascii_letters: + symbols += string.ascii_letters + if using_punctuation: + symbols += string.punctuation \ + .replace('"', '') \ + .replace("'", '') \ + .replace('\\', '') + return ''.join([random.choice(symbols) for _ in range(length)]) -# IN PROGRESS -# def copy_doc_files(project_directory): -# cookiecutters_dir = DEFAULT_CONFIG['cookiecutters_dir'] -# cookiecutter_django_dir = os.path.join( -# cookiecutters_dir, -# 'cookiecutter-django', -# 'docs' -# ) -# target_dir = os.path.join( -# project_directory, -# 'docs' -# ) -# for name in os.listdir(cookiecutter_django_dir): -# if name.endswith('.rst') and not name.startswith('index'): -# src = os.path.join(cookiecutter_django_dir, name) -# dst = os.path.join(target_dir, name) -# shutil.copyfile(src, dst) +def set_flag(file_path, + flag, + value=None, + *args, + **kwargs): + if value is None: + random_string = generate_random_string(*args, **kwargs) + if random_string is None: + import sys + sys.stdout.write( + "We couldn't find a secure pseudo-random number generator on your system. " + "Please, make sure to manually {} later.".format(flag) + ) + random_string = flag + value = random_string -# 1. Generates and saves random secret key -make_secret_key(PROJECT_DIRECTORY) + with open(file_path, 'r+') as f: + file_contents = f.read().replace(flag, value) + f.seek(0) + f.write(file_contents) + f.truncate() -# 2. Removes the taskapp if celery isn't going to be used -if '{{ cookiecutter.use_celery }}'.lower() == 'n': - remove_task_app(PROJECT_DIRECTORY) - -# 3. Removes the .idea directory if PyCharm isn't going to be used -if '{{ cookiecutter.use_pycharm }}'.lower() != 'y': - remove_pycharm_dir(PROJECT_DIRECTORY) - -# 4. Removes all heroku files if it isn't going to be used -if '{{ cookiecutter.use_heroku }}'.lower() != 'y': - remove_heroku_files() - -# 5. Removes all docker files if it isn't going to be used -if '{{ cookiecutter.use_docker }}'.lower() != 'y': - remove_docker_files() - -# 6. Removes all JS task manager files if it isn't going to be used -if '{{ cookiecutter.js_task_runner}}'.lower() == 'gulp': - remove_grunt_files() -elif '{{ cookiecutter.js_task_runner}}'.lower() == 'grunt': - remove_gulp_files() -else: - remove_gulp_files() - remove_grunt_files() - remove_packageJSON_file() + return value -# 9. Display a warning if use_docker and use_grunt are selected. Grunt isn't -# supported by our docker config atm. -if '{{ cookiecutter.js_task_runner }}'.lower() in ['grunt', 'gulp'] and '{{ cookiecutter.use_docker }}'.lower() == 'y': - print( - "You selected to use docker and a JS task runner. This is NOT supported out of the box for now. You " - "can continue to use the project like you normally would, but you will need to add a " - "js task runner service to your docker configuration manually." +def set_django_secret_key(file_path): + django_secret_key = set_flag( + file_path, + '!!!SET DJANGO_SECRET_KEY!!!', + length=50, + using_digits=True, + using_ascii_letters=True ) + return django_secret_key -# 10. Removes files needed for the GPLv3 licence if it isn't going to be used. -if '{{ cookiecutter.open_source_license}}' != 'GPLv3': - remove_copying_files() + +def set_postgres_user(file_path, + value=None): + postgres_user = set_flag( + file_path, + '!!!SET POSTGRES_USER!!!', + value=value, + length=8, + using_ascii_letters=True + ) + return postgres_user + + +def set_postgres_password(file_path): + postgres_password = set_flag( + file_path, + '!!!SET POSTGRES_PASSWORD!!!', + length=42, + using_digits=True, + using_ascii_letters=True + ) + return postgres_password + + +def initialize_dotenv(postgres_user): + # Initializing `env.example` first. + envexample_file_path = os.path.join(PROJECT_DIR_PATH, 'env.example') + set_django_secret_key(envexample_file_path) + set_postgres_user(envexample_file_path, value=postgres_user) + set_postgres_password(envexample_file_path) + # Renaming `env.example` to `.env`. + dotenv_file_path = os.path.join(PROJECT_DIR_PATH, '.env') + shutil.move(envexample_file_path, dotenv_file_path) + + +def initialize_localyml(postgres_user): + set_postgres_user(os.path.join(PROJECT_DIR_PATH, 'local.yml'), value=postgres_user) + + +def initialize_local_settings(): + set_django_secret_key(os.path.join(PROJECT_DIR_PATH, 'config', 'settings', 'local.py')) + + +def initialize_test_settings(): + set_django_secret_key(os.path.join(PROJECT_DIR_PATH, 'config', 'settings', 'test.py')) + + +def main(): + postgres_user = generate_random_string(length=16, using_ascii_letters=True) + initialize_dotenv(postgres_user) + initialize_localyml(postgres_user) + initialize_local_settings() + initialize_test_settings() + + if '{{ cookiecutter.open_source_license }}' == 'Not open source': + remove_open_source_project_only_files() + elif '{{ cookiecutter.open_source_license}}' != 'GPLv3': + remove_gplv3_files() + + if '{{ cookiecutter.use_pycharm }}'.lower() == 'n': + remove_pycharm_files() + + if '{{ cookiecutter.use_docker }}'.lower() == 'n': + remove_docker_files() + + remove_paas_files() + + if '{{ cookiecutter.js_task_runner}}'.lower() == 'gulp': + remove_grunt_files() + elif '{{ cookiecutter.js_task_runner}}'.lower() == 'grunt': + remove_gulp_files() + else: + remove_gulp_files() + remove_grunt_files() + remove_packagejson_file() + + if '{{ cookiecutter.js_task_runner }}'.lower() in ['grunt', 'gulp'] \ + and '{{ cookiecutter.use_docker }}'.lower() == 'y': + TERMINATOR = "\x1b[0m" + INFO = "\x1b[1;33m [INFO]: " + sys.stdout.write( + INFO + + "Docker and {} JS task runner ".format('{{ cookiecutter.js_task_runner }}'.lower().capitalize()) + + "working together not supported yet. " + "You can continue using the generated project like you normally would, " + "however you would need to add a JS task runner service " + "to your Docker Compose configuration manually." + + TERMINATOR + ) + + if '{{ cookiecutter.use_celery }}'.lower() == 'n': + remove_celery_app() + + +if __name__ == '__main__': + main() diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 5f2c9d5d6..c48ce0ad8 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -1,27 +1,62 @@ +""" +NOTE: + the below code is to be maintained Python 2.x-compatible + as the whole Cookiecutter Django project initialization + can potentially be run in Python 2.x environment. + +TODO: ? restrict Cookiecutter Django project initialization to Python 3.x environments only +""" + project_slug = '{{ cookiecutter.project_slug }}' - if hasattr(project_slug, 'isidentifier'): - assert project_slug.isidentifier(), 'Project slug should be valid Python identifier!' + assert project_slug.isidentifier(), "'{}' project slug is not a valid Python identifier.".format(project_slug) -heroku = '{{ cookiecutter.use_heroku }}'.lower() -docker = '{{ cookiecutter.use_docker }}'.lower() +assert "\\" not in "{{ cookiecutter.author_name }}", "Don't include backslashes in author name." -if docker == 'n': - import sys - python_major_version = sys.version_info[0] +using_docker = '{{ cookiecutter.use_docker }}'.lower() +if using_docker == 'n': + TERMINATOR = "\x1b[0m" + WARNING = "\x1b[1;33m [WARNING]: " + INFO = "\x1b[1;33m [INFO]: " + HINT = "\x1b[3;33m" + SUCCESS = "\x1b[1;32m [SUCCESS]: " - if python_major_version == 2: - sys.stdout.write("WARNING: Cookiecutter Django does not support Python 2! Stability is guaranteed with Python 3.4+ only. Are you sure you want to proceed? (y/n)") + import sys - yes_options = set(['y']) - no_options = set(['n', '']) - choice = raw_input().lower() - if choice in no_options: - sys.exit(1) - elif choice in yes_options: - pass - else: - sys.stdout.write("Please respond with %s or %s" - % (', '.join([o for o in yes_options if not o == '']) - , ', '.join([o for o in no_options if not o == '']))) + python_major_version = sys.version_info[0] + if python_major_version == 2: + sys.stdout.write( + WARNING + + "Cookiecutter Django does not support Python 2. " + "Stability is guaranteed with Python 3.6+ only, " + "are you sure you want to proceed (y/n)? " + + TERMINATOR + ) + yes_options, no_options = frozenset(['y']), frozenset(['n']) + while True: + choice = raw_input().lower() + if choice in yes_options: + break + elif choice in no_options: + sys.stdout.write( + INFO + + "Generation process stopped as requested." + + TERMINATOR + ) + sys.exit(1) + else: + sys.stdout.write( + HINT + + "Please respond with {} or {}: ".format( + ', '.join(["'{}'".format(o) for o in yes_options if not o == '']), + ', '.join(["'{}'".format(o) for o in no_options if not o == '']) + ) + + TERMINATOR + ) + + sys.stdout.write( + SUCCESS + + "Project initialized, keep up the good work!" + + TERMINATOR + ) diff --git a/requirements.txt b/requirements.txt index 1790a0216..085d5a147 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ sh==1.12.14 binaryornot==0.4.4 # Testing -pytest==3.3.2 +pytest==3.4.1 pycodestyle==2.3.1 pyflakes==1.6.0 tox==2.9.1 diff --git a/requirements_to_watch.txt b/requirements_to_watch.txt index 4440448d4..91f0041bf 100644 --- a/requirements_to_watch.txt +++ b/requirements_to_watch.txt @@ -1,4 +1,2 @@ -# These requirements prevented an upgrade to Django 1.10. -django-coverage-plugin==1.5.0 +# These requirements prevented an upgrade to Django 1.11. django-autoslug==1.9.3 - diff --git a/setup.py b/setup.py index eb5856e63..a7efb0a64 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ except ImportError: # Our version ALWAYS matches the version of Django we support # If Django has a new release, we branch, tag, then update this setting after the tag. -version = '1.11.9' +version = '2.0.2' if sys.argv[-1] == 'tag': os.system('git tag -a %s -m "version %s"' % (version, version)) @@ -34,16 +34,14 @@ setup( classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', - 'Framework :: Django :: 1.10', + 'Framework :: Django :: 2.0', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development', ], keywords=( diff --git a/tests/test_docker.sh b/tests/test_docker.sh index 137694d7a..d80a091e4 100755 --- a/tests/test_docker.sh +++ b/tests/test_docker.sh @@ -19,3 +19,6 @@ docker-compose -f local.yml run django python manage.py test # return non-zero status code if there are migrations that have not been created docker-compose -f local.yml run django python manage.py makemigrations --dry-run --check || { echo "ERROR: there were changes in the models, but migration listed above have not been created and are not saved in version control"; exit 1; } + +# Test support for translations +docker-compose -f local.yml run --rm django python manage.py makemessages diff --git a/{{cookiecutter.project_slug}}/.travis.yml b/{{cookiecutter.project_slug}}/.travis.yml index 216b8709b..5ca54d000 100644 --- a/{{cookiecutter.project_slug}}/.travis.yml +++ b/{{cookiecutter.project_slug}}/.travis.yml @@ -8,4 +8,4 @@ before_install: - sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm language: python python: - - "3.5" + - "3.6" diff --git a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile index e62b524a4..383b15776 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile @@ -1,7 +1,18 @@ -FROM python:3.5 +FROM python:3.6-alpine ENV PYTHONUNBUFFERED 1 +RUN apk update \ + # psycopg2 dependencies + && apk add --virtual build-deps gcc python3-dev musl-dev \ + && apk add postgresql-dev \ + # Pillow dependencies + && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \ + # CFFI dependencies + && apk add libffi-dev openssl-dev py-cffi \ + # Translations dependencies + && apk add gettext + # Requirements have to be pulled and installed here, otherwise caching won't work COPY ./requirements /requirements RUN pip install -r /requirements/local.txt diff --git a/{{cookiecutter.project_slug}}/compose/local/django/celery/beat/start.sh b/{{cookiecutter.project_slug}}/compose/local/django/celery/beat/start.sh index c26318b44..0ca8bb507 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/celery/beat/start.sh +++ b/{{cookiecutter.project_slug}}/compose/local/django/celery/beat/start.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail diff --git a/{{cookiecutter.project_slug}}/compose/local/django/celery/worker/start.sh b/{{cookiecutter.project_slug}}/compose/local/django/celery/worker/start.sh index 8b50c8cfd..4335340de 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/celery/worker/start.sh +++ b/{{cookiecutter.project_slug}}/compose/local/django/celery/worker/start.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail diff --git a/{{cookiecutter.project_slug}}/compose/local/django/start.sh b/{{cookiecutter.project_slug}}/compose/local/django/start.sh index cf4a41667..50227e19e 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/start.sh +++ b/{{cookiecutter.project_slug}}/compose/local/django/start.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail diff --git a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile index fcbe51844..2ded8253e 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile @@ -1,9 +1,18 @@ -FROM python:3.5 +FROM python:3.6-alpine ENV PYTHONUNBUFFERED 1 -RUN groupadd -r django \ - && useradd -r -g django django +RUN apk update \ + # psycopg2 dependencies + && apk add --virtual build-deps gcc python3-dev musl-dev \ + && apk add postgresql-dev \ + # Pillow dependencies + && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \ + # CFFI dependencies + && apk add libffi-dev openssl-dev py-cffi + +RUN addgroup -S django \ + && adduser -S -G django django # Requirements have to be pulled and installed here, otherwise caching won't work COPY ./requirements /requirements diff --git a/{{cookiecutter.project_slug}}/compose/production/django/celery/beat/start.sh b/{{cookiecutter.project_slug}}/compose/production/django/celery/beat/start.sh index 845db0a3d..def83076c 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/celery/beat/start.sh +++ b/{{cookiecutter.project_slug}}/compose/production/django/celery/beat/start.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail diff --git a/{{cookiecutter.project_slug}}/compose/production/django/celery/worker/start.sh b/{{cookiecutter.project_slug}}/compose/production/django/celery/worker/start.sh index 4529aad92..10f0d20c5 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/celery/worker/start.sh +++ b/{{cookiecutter.project_slug}}/compose/production/django/celery/worker/start.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail diff --git a/{{cookiecutter.project_slug}}/compose/production/django/entrypoint.sh b/{{cookiecutter.project_slug}}/compose/production/django/entrypoint.sh index 3b83c7bb6..a40a2b7e4 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/entrypoint.sh +++ b/{{cookiecutter.project_slug}}/compose/production/django/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail @@ -25,7 +25,7 @@ export DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres:5432/$ export CELERY_BROKER_URL=$REDIS_URL/0 {% endif %} -function postgres_ready(){ +postgres_ready() { python << END import sys import psycopg2 diff --git a/{{cookiecutter.project_slug}}/compose/production/django/gunicorn.sh b/{{cookiecutter.project_slug}}/compose/production/django/gunicorn.sh index 25da06496..8846cafb2 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/gunicorn.sh +++ b/{{cookiecutter.project_slug}}/compose/production/django/gunicorn.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh set -o errexit set -o pipefail diff --git a/{{cookiecutter.project_slug}}/config/settings/local.py b/{{cookiecutter.project_slug}}/config/settings/local.py index 67a7074a5..c903795c7 100644 --- a/{{cookiecutter.project_slug}}/config/settings/local.py +++ b/{{cookiecutter.project_slug}}/config/settings/local.py @@ -24,7 +24,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # Note: This key only used for development and testing. -SECRET_KEY = env('DJANGO_SECRET_KEY', default='CHANGEME!!!') +SECRET_KEY = env('DJANGO_SECRET_KEY', default='!!!SET DJANGO_SECRET_KEY!!!') # Mail settings # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index 33542fbf9..3926f3db1 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -103,12 +103,8 @@ AWS_QUERYSTRING_AUTH = False # AWS cache settings, don't change unless you know what you're doing: AWS_EXPIRY = 60 * 60 * 24 * 7 -# TODO See: https://github.com/jschneier/django-storages/issues/47 -# Revert the following and use str after the above-mentioned bug is fixed in -# either django-storage-redux or boto -control = 'max-age=%d, s-maxage=%d, must-revalidate' % (AWS_EXPIRY, AWS_EXPIRY) -AWS_HEADERS = { - 'Cache-Control': bytes(control, encoding='latin-1') +AWS_S3_OBJECT_PARAMETERS = { + 'CacheControl': 'max-age=%d, s-maxage=%d, must-revalidate' % (AWS_EXPIRY, AWS_EXPIRY), } # URL that handles the media served from MEDIA_ROOT, used for managing @@ -177,7 +173,7 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [ # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ DATABASES['default'] = env.db('DATABASE_URL') DATABASES['default']['CONN_MAX_AGE'] = env.int('CONN_MAX_AGE', default={{ _DEFAULT_CONN_MAX_AGE }}) - +DATABASES['default']['ATOMIC_REQUESTS'] = True # CACHING # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/config/settings/test.py b/{{cookiecutter.project_slug}}/config/settings/test.py index d973428d7..01a412791 100644 --- a/{{cookiecutter.project_slug}}/config/settings/test.py +++ b/{{cookiecutter.project_slug}}/config/settings/test.py @@ -17,7 +17,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = False # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # Note: This key only used for development and testing. -SECRET_KEY = env('DJANGO_SECRET_KEY', default='CHANGEME!!!') +SECRET_KEY = env('DJANGO_SECRET_KEY', default='!!!SET DJANGO_SECRET_KEY!!!') # Mail settings # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/docs/docker_ec2.rst b/{{cookiecutter.project_slug}}/docs/docker_ec2.rst index 3e43ed767..606d29563 100644 --- a/{{cookiecutter.project_slug}}/docs/docker_ec2.rst +++ b/{{cookiecutter.project_slug}}/docs/docker_ec2.rst @@ -58,7 +58,7 @@ The `Docker compose documentation`_ explains in detail what you can accomplish i build: database webapp: build: webapp: - command: /usr/bin/python3.4 manage.py runserver 0.0.0.0:8000 # dev setting + command: /usr/bin/python3.6 manage.py runserver 0.0.0.0:8000 # dev setting # command: gunicorn -b 0.0.0.0:8000 wsgi:application # production setting volumes: - webapp/your_project_name:/path/to/container/workdir/ diff --git a/{{cookiecutter.project_slug}}/env.example b/{{cookiecutter.project_slug}}/env.example index c83b3a83d..757c4ef00 100644 --- a/{{cookiecutter.project_slug}}/env.example +++ b/{{cookiecutter.project_slug}}/env.example @@ -1,7 +1,7 @@ # PostgreSQL -POSTGRES_PASSWORD=mysecretpass -POSTGRES_USER=postgresuser +POSTGRES_PASSWORD=!!!SET POSTGRES_PASSWORD!!! +POSTGRES_USER=!!!SET POSTGRES_USER!!! CONN_MAX_AGE= # Domain name, used by caddy @@ -11,7 +11,7 @@ DOMAIN_NAME={{ cookiecutter.domain_name }} # DJANGO_READ_DOT_ENV_FILE=True DJANGO_ADMIN_URL= DJANGO_SETTINGS_MODULE=config.settings.production -DJANGO_SECRET_KEY=CHANGEME!!! +DJANGO_SECRET_KEY=!!!SET DJANGO_SECRET_KEY!!! DJANGO_ALLOWED_HOSTS=.{{ cookiecutter.domain_name }} # AWS Settings diff --git a/{{cookiecutter.project_slug}}/local.yml b/{{cookiecutter.project_slug}}/local.yml index aaf131fa2..b030e6174 100644 --- a/{{cookiecutter.project_slug}}/local.yml +++ b/{{cookiecutter.project_slug}}/local.yml @@ -15,7 +15,7 @@ services: volumes: - .:/app environment: - - POSTGRES_USER={{cookiecutter.project_slug}} + - POSTGRES_USER=!!!SET POSTGRES_USER!!! - USE_DOCKER=yes ports: - "8000:8000" @@ -29,7 +29,7 @@ services: - postgres_data_local:/var/lib/postgresql/data - postgres_backup_local:/backups environment: - - POSTGRES_USER={{cookiecutter.project_slug}} + - POSTGRES_USER=!!!SET POSTGRES_USER!!! {% if cookiecutter.use_mailhog == 'y' %} mailhog: image: mailhog/mailhog:v1.0.0 diff --git a/{{cookiecutter.project_slug}}/locale/README.rst b/{{cookiecutter.project_slug}}/locale/README.rst new file mode 100644 index 000000000..c2f1dcd6f --- /dev/null +++ b/{{cookiecutter.project_slug}}/locale/README.rst @@ -0,0 +1,6 @@ +Translations +============ + +Translations will be placed in this folder when running:: + + python manage.py makemessages diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index b25951562..c9f784327 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -6,7 +6,7 @@ wheel==0.30.0 # Conservative Django -django==1.11.10 # pyup: <2.0 +django==2.0.2 # pyup: < 2.1 # Configuration django-environ==0.4.4 @@ -36,21 +36,21 @@ django-allauth==0.35.0 # from http://www.lfd.uci.edu/~gohlke/pythonlibs/#psycopg {% else %} # Python-PostgreSQL Database Adapter -psycopg2==2.7.3.2 +psycopg2==2.7.4 --no-binary psycopg2 {%- endif %} # Unicode slugification awesome-slugify==1.6.5 # Time zones support -pytz==2017.3 +pytz==2018.3 # Redis support django-redis==4.8.0 redis>=2.10.5 {% if cookiecutter.use_celery == "y" %} -celery==3.1.25 +celery==3.1.25 # pyup: <4.0 {% endif %} {% if cookiecutter.use_compressor == "y" %} diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 2fccb4092..93e9f741d 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -1,11 +1,11 @@ # Local development dependencies go here -r base.txt -coverage==4.5 +coverage==4.5.1 django-coverage-plugin==1.5.0 -Sphinx==1.6.7 -django-extensions==1.9.9 +Sphinx==1.7.1 +django-extensions==2.0.0 Werkzeug==0.14.1 django-test-plus==1.0.22 factory-boy==2.10.0 @@ -13,7 +13,7 @@ factory-boy==2.10.0 django-debug-toolbar==1.9.1 # improved REPL -ipdb==0.10.3 +ipdb==0.11 pytest-django==3.1.2 -pytest-sugar==0.9.0 +pytest-sugar==0.9.1 diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index 15740618f..7577c7462 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -6,7 +6,7 @@ # Python-PostgreSQL Database Adapter # Assuming Windows is used locally, and *nix -- in production. # ------------------------------------------------------------ -psycopg2==2.7.3.2 +psycopg2==2.7.4 --no-binary psycopg2 {%- endif %} # WSGI Handler @@ -16,7 +16,7 @@ gunicorn==19.7.1 # Static and Media Storage # ------------------------------------------------ -boto3==1.5.22 +boto3==1.5.36 django-storages==1.6.5 {% if cookiecutter.use_whitenoise != 'y' -%} Collectfast==0.6.0 @@ -24,7 +24,7 @@ Collectfast==0.6.0 # Email backends for Mailgun, Postmark, SendGrid and more # ------------------------------------------------------- -django-anymail==1.3 +django-anymail==1.4 {% if cookiecutter.use_sentry_for_error_reporting == "y" -%} # Raven is the Sentry client diff --git a/{{cookiecutter.project_slug}}/requirements/test.txt b/{{cookiecutter.project_slug}}/requirements/test.txt index b5a8ec985..2a6ed87fa 100644 --- a/{{cookiecutter.project_slug}}/requirements/test.txt +++ b/{{cookiecutter.project_slug}}/requirements/test.txt @@ -4,10 +4,10 @@ {% if cookiecutter.windows == 'y' -%} # Python-PostgreSQL Database Adapter # If using Win for dev, this assumes Unix in test/prod -psycopg2==2.7.3.2 +psycopg2==2.7.4 {%- endif %} -coverage==4.5 +coverage==4.5.1 flake8==3.5.0 # pyup: != 2.6.0 django-test-plus==1.0.22 factory-boy==2.10.0 @@ -15,4 +15,4 @@ django-coverage-plugin==1.5.0 # pytest pytest-django==3.1.2 -pytest-sugar==0.9.0 +pytest-sugar==0.9.1 diff --git a/{{cookiecutter.project_slug}}/runtime.txt b/{{cookiecutter.project_slug}}/runtime.txt index cfa5aa5ca..5c4538034 100644 --- a/{{cookiecutter.project_slug}}/runtime.txt +++ b/{{cookiecutter.project_slug}}/runtime.txt @@ -1 +1 @@ -python-3.6.2 +python-3.6.4 diff --git a/{{cookiecutter.project_slug}}/utility/requirements-stretch.apt b/{{cookiecutter.project_slug}}/utility/requirements-stretch.apt new file mode 100644 index 000000000..a2b3a7e5e --- /dev/null +++ b/{{cookiecutter.project_slug}}/utility/requirements-stretch.apt @@ -0,0 +1,23 @@ +##basic build dependencies of various Django apps for Debian Jessie 9.x +#build-essential metapackage install: make, gcc, g++, +build-essential +#required to translate +gettext +python3-dev + +##shared dependencies of: +##Pillow, pylibmc +zlib1g-dev + +##Postgresql and psycopg2 dependencies +libpq-dev + +##Pillow dependencies +libtiff5-dev +libjpeg62-turbo-dev +libfreetype6-dev +liblcms2-dev +libwebp-dev + +##django-extensions +graphviz-dev diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html index a45de763c..c767cb3b9 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html @@ -22,7 +22,7 @@ {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% compress css %}{% endraw %}{% endif %}{% raw %} - {% endraw %}{% if cookiecutter.js_task_runner == "Gulp" %}{% raw %} + {% endraw %}{% if cookiecutter.js_task_runner == "Gulp" and cookiecutter.use_compressor == "n" %}{% raw %} {% endraw %}{% else %}{% raw %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py index b2cfca39f..7cc96f62c 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py index c06f15da4..4b1a10d1e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py @@ -1,11 +1,9 @@ from django.contrib.auth.models import AbstractUser -from django.core.urlresolvers import reverse from django.db import models -from django.utils.encoding import python_2_unicode_compatible +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -@python_2_unicode_compatible class User(AbstractUser): # First Name and Last Name do not cover name patterns diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py index 6e181cc8e..4935b0f3e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py @@ -1,4 +1,4 @@ -from django.core.urlresolvers import reverse, resolve +from django.urls import reverse, resolve from test_plus.test import TestCase diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py index 600ccfbdf..7c83fab98 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py @@ -2,6 +2,7 @@ from django.conf.urls import url from . import views +app_name = 'users' urlpatterns = [ url( regex=r'^$', diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py index 777f42b74..acde4a8fc 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py @@ -1,7 +1,6 @@ -from django.core.urlresolvers import reverse -from django.views.generic import DetailView, ListView, RedirectView, UpdateView - from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse +from django.views.generic import DetailView, ListView, RedirectView, UpdateView from .models import User