diff --git a/README.rst b/README.rst index 30a3a2db4..40e62bee8 100644 --- a/README.rst +++ b/README.rst @@ -163,28 +163,12 @@ Answer the prompts with your own desired options_. For example:: use_celery [n]: y use_mailhog [n]: n use_sentry [n]: y - use_pycharm [n]: y - windows [n]: n - use_docker [n]: n - use_heroku [n]: y use_compressor [n]: y - Select postgresql_version: - 1 - 11.3 - 2 - 10.8 - 3 - 9.6 - 4 - 9.5 - 5 - 9.4 - Choose from 1, 2, 3, 4, 5 [1]: 1 - Select js_task_runner: - 1 - None - 2 - Gulp - Choose from 1, 2 [1]: 1 Select cloud_provider: 1 - AWS 2 - GCP 3 - None Choose from 1, 2, 3 [1]: 1 - custom_bootstrap_compilation [n]: n Select open_source_license: 1 - MIT 2 - BSD @@ -192,8 +176,6 @@ Answer the prompts with your own desired options_. For example:: 4 - Apache Software License 2.0 5 - Not open source Choose from 1, 2, 3, 4, 5 [1]: 1 - keep_local_envs_in_vcs [y]: y - debug[n]: n Enter the project and take a look around:: diff --git a/cookiecutter.json b/cookiecutter.json index d6d217ca4..0a060e03e 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -2,7 +2,7 @@ "project_name": "My Awesome Project", "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}", "description": "Behold My Awesome Project!", - "author_name": "Daniel Roy Greenfeld", + "author_name": "Your Name", "domain_name": "example.com", "email": "{{ cookiecutter.author_name.lower()|replace(' ', '-') }}@example.com", "version": "0.1.0", @@ -14,34 +14,16 @@ "Not open source" ], "timezone": "UTC", - "windows": "n", - "use_pycharm": "n", - "use_docker": "n", - "postgresql_version": [ - "11.3", - "10.8", - "9.6", - "9.5", - "9.4" - ], - "js_task_runner": [ - "None", - "Gulp" - ], "cloud_provider": [ "AWS", + "Azure", "GCP", "None" ], - "custom_bootstrap_compilation": "n", "use_compressor": "n", "use_celery": "n", "use_mailhog": "n", "use_sentry": "n", "use_whitenoise": "n", - "use_heroku": "n", - "use_travisci": "n", - "keep_local_envs_in_vcs": "y", - - "debug": "n" + "use_travisci": "n" } diff --git a/docs/project-generation-options.rst b/docs/project-generation-options.rst index afa9b8aff..1d82cb79a 100644 --- a/docs/project-generation-options.rst +++ b/docs/project-generation-options.rst @@ -37,29 +37,6 @@ open_source_license: timezone: The value to be used for the ``TIME_ZONE`` setting of the project. -windows: - Indicates whether the project should be configured for development on Windows. - -use_pycharm: - Indicates whether the project should be configured for development with PyCharm_. - -use_docker: - Indicates whether the project should be configured to use Docker_ and `Docker Compose`_. - -postgresql_version: - Select a PostgreSQL_ version to use. The choices are: - - 1. 11.3 - 2. 10.8 - 3. 9.6 - 4. 9.5 - 5. 9.4 - -js_task_runner: - Select a JavaScript task runner. The choices are: - - 1. None - 2. Gulp_ cloud_provider: Select a cloud provider for static & media files. The choices are: @@ -70,10 +47,6 @@ cloud_provider: Note that if you choose no cloud provider, media files won't work. -custom_bootstrap_compilation: - Indicates whether the project should support Bootstrap recompilation - via the selected JavaScript task runner's task. This can be useful - for real-time Bootstrap variable alteration. use_compressor: Indicates whether the project should be configured to use `Django Compressor`_. @@ -97,17 +70,6 @@ use_heroku: use_travisci: Indicates whether the project should be configured to use `Travis CI`_. -keep_local_envs_in_vcs: - Indicates whether the project's ``.envs/.local/`` should be kept in VCS - (comes in handy when working in teams where local environment reproducibility - is strongly encouraged). - Note: .env(s) are only utilized when Docker Compose and/or Heroku support is enabled. - -debug: - Indicates whether the project should be configured for debugging. - This option is relevant for Cookiecutter Django developers only. - - .. _MIT: https://opensource.org/licenses/MIT .. _BSD: https://opensource.org/licenses/BSD-3-Clause .. _GPLv3: https://www.gnu.org/licenses/gpl.html diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index ed762b40f..a0ea42774 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -91,7 +91,7 @@ def remove_packagejson_file(): def remove_celery_files(): file_names = [ - os.path.join("config", "celery_app.py"), + os.path.join("{{ cookiecutter.project_slug }}", "celery_app.py"), os.path.join("{{ cookiecutter.project_slug }}", "users", "tasks.py"), os.path.join( "{{ cookiecutter.project_slug }}", "users", "tests", "test_tasks.py" @@ -261,8 +261,12 @@ def set_flags_in_envs(postgres_user, celery_flower_user, debug=False): def set_flags_in_settings_files(): - set_django_secret_key(os.path.join("config", "settings", "local.py")) - set_django_secret_key(os.path.join("config", "settings", "test.py")) + set_django_secret_key( + os.path.join("{{ cookiecutter.project_slug }}", "settings", "dev_template.py") + ) + set_django_secret_key( + os.path.join("{{ cookiecutter.project_slug }}", "settings", "test.py") + ) def remove_envs_and_associated_files(): @@ -280,13 +284,8 @@ def remove_node_dockerfile(): def main(): - debug = "{{ cookiecutter.debug }}".lower() == "y" + set_flags_in_envs(generate_random_user(), generate_random_user()) - set_flags_in_envs( - DEBUG_VALUE if debug else generate_random_user(), - DEBUG_VALUE if debug else generate_random_user(), - debug=debug, - ) set_flags_in_settings_files() if "{{ cookiecutter.open_source_license }}" == "Not open source": @@ -294,39 +293,11 @@ def main(): if "{{ cookiecutter.open_source_license}}" != "GPLv3": remove_gplv3_files() - if "{{ cookiecutter.use_pycharm }}".lower() == "n": - remove_pycharm_files() + remove_utility_files() + remove_heroku_files() - if "{{ cookiecutter.use_docker }}".lower() == "y": - remove_utility_files() - else: - remove_docker_files() - - if "{{ cookiecutter.use_heroku }}".lower() == "n": - remove_heroku_files() - - if ( - "{{ cookiecutter.use_docker }}".lower() == "n" - and "{{ cookiecutter.use_heroku }}".lower() == "n" - ): - if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y": - print( - INFO + ".env(s) are only utilized when Docker Compose and/or " - "Heroku support is enabled so keeping them does not " - "make sense given your current setup." + TERMINATOR - ) - remove_envs_and_associated_files() - else: - append_to_gitignore_file(".env") - append_to_gitignore_file(".envs/*") - if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y": - append_to_gitignore_file("!.envs/.local/") - - if "{{ cookiecutter.js_task_runner}}".lower() == "none": - remove_gulp_files() - remove_packagejson_file() - if "{{ cookiecutter.use_docker }}".lower() == "y": - remove_node_dockerfile() + append_to_gitignore_file(".env") + append_to_gitignore_file(".envs/*") if "{{ cookiecutter.cloud_provider}}".lower() == "none": print( @@ -336,8 +307,7 @@ def main(): if "{{ cookiecutter.use_celery }}".lower() == "n": remove_celery_files() - if "{{ cookiecutter.use_docker }}".lower() == "y": - remove_celery_compose_dirs() + remove_celery_compose_dirs() if "{{ cookiecutter.use_travisci }}".lower() == "n": remove_dottravisyml_file() diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 91332f144..6065e2d7f 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -29,33 +29,3 @@ assert ( assert ( "\\" not in "{{ cookiecutter.author_name }}" ), "Don't include backslashes in author name." - -if "{{ cookiecutter.use_docker }}".lower() == "n": - python_major_version = sys.version_info[0] - if python_major_version == 2: - print( - WARNING + "You're running cookiecutter under Python 2, but the generated " - "project requires Python 3.6+. Do 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: - print(INFO + "Generation process stopped as requested." + TERMINATOR) - sys.exit(1) - else: - print( - 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 - ) diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 77f71df5b..b66e81ecc 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -30,8 +30,6 @@ def context(): @pytest_fixture_plus -@pytest.mark.parametrize("windows", YN_CHOICES, ids=lambda yn: f"win:{yn}") -@pytest.mark.parametrize("use_docker", YN_CHOICES, ids=lambda yn: f"docker:{yn}") @pytest.mark.parametrize("use_celery", YN_CHOICES, ids=lambda yn: f"celery:{yn}") @pytest.mark.parametrize("use_mailhog", YN_CHOICES, ids=lambda yn: f"mailhog:{yn}") @pytest.mark.parametrize("use_sentry", YN_CHOICES, ids=lambda yn: f"sentry:{yn}") @@ -39,19 +37,10 @@ def context(): @pytest.mark.parametrize("use_whitenoise", YN_CHOICES, ids=lambda yn: f"wnoise:{yn}") @pytest.mark.parametrize("cloud_provider", CLOUD_CHOICES, ids=lambda yn: f"cloud:{yn}") def context_combination( - windows, - use_docker, - use_celery, - use_mailhog, - use_sentry, - use_compressor, - use_whitenoise, - cloud_provider, + use_celery, use_mailhog, use_sentry, use_compressor, use_whitenoise, cloud_provider ): """Fixture that parametrize the function where it's used.""" return { - "windows": windows, - "use_docker": use_docker, "use_compressor": use_compressor, "use_celery": use_celery, "use_mailhog": use_mailhog, @@ -125,7 +114,8 @@ def test_black_passes(cookies, context_combination): This is parametrized for each combination from ``context_combination`` fixture """ result = cookies.bake(extra_context=context_combination) - + if result.exception: + pytest.fail(str(result.exception)) try: sh.black("--check", "--diff", "--exclude", "migrations", f"{result.project}/") except sh.ErrorReturnCode as e: diff --git a/tox.ini b/tox.ini index 7ee939158..da0a8992f 100644 --- a/tox.ini +++ b/tox.ini @@ -16,4 +16,4 @@ commands = pytest -m black {posargs:./tests} [testenv:black-template] deps = black -commands = black --check hooks tests setup.py docs +commands = black --diff --check hooks tests setup.py docs diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index c273d716f..a55ba8678 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -161,7 +161,6 @@ typings/ !.vscode/extensions.json -{% if cookiecutter.use_pycharm == 'y' -%} # Provided default Pycharm Run/Debug Configurations should be tracked by git # In case of local modifications made by Pycharm, use update-index command # for each changed file, like this: @@ -216,7 +215,6 @@ com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties -{% endif %} ### Windows template @@ -321,23 +319,19 @@ Session.vim # Auto-generated tag files tags -{% if cookiecutter.use_docker == 'n' %} ### VirtualEnv template # Virtualenv pyvenv.cfg pip-selfcheck.json .env -{% endif %} ### Project template -{% if cookiecutter.use_mailhog == 'y' and cookiecutter.use_docker == 'n' %} +{% if cookiecutter.use_mailhog == 'y' %} MailHog {%- endif %} {{ cookiecutter.project_slug }}/media/ .pytest_cache/ -{% if cookiecutter.use_docker == 'y' %} .ipython/ -{%- endif %} diff --git a/{{cookiecutter.project_slug}}/.idea/{{cookiecutter.project_slug}}.iml b/{{cookiecutter.project_slug}}/.idea/{{cookiecutter.project_slug}}.iml index d408765a5..6cbaa080d 100644 --- a/{{cookiecutter.project_slug}}/.idea/{{cookiecutter.project_slug}}.iml +++ b/{{cookiecutter.project_slug}}/.idea/{{cookiecutter.project_slug}}.iml @@ -13,13 +13,7 @@ - {% if cookiecutter.js_task_runner != 'None' %} - - - - {% else %} - - {% endif %} + diff --git a/{{cookiecutter.project_slug}}/README.rst b/{{cookiecutter.project_slug}}/README.rst index 1deeafbec..5bd309aea 100644 --- a/{{cookiecutter.project_slug}}/README.rst +++ b/{{cookiecutter.project_slug}}/README.rst @@ -88,37 +88,12 @@ Please note: For Celery's import magic to work, it is important *where* the cele Email Server ^^^^^^^^^^^^ -{% if cookiecutter.use_docker == 'y' %} In development, it is often nice to be able to see emails that are being sent from your application. For that reason local SMTP server `MailHog`_ with a web interface is available as docker container. Container mailhog will start automatically when you will run all docker containers. Please check `cookiecutter-django Docker documentation`_ for more details how to start all containers. With MailHog running, to view messages that are sent by your application, open your browser and go to ``http://127.0.0.1:8025`` -{% else %} -In development, it is often nice to be able to see emails that are being sent from your application. If you choose to use `MailHog`_ when generating the project a local SMTP server with a web interface will be available. - -#. `Download the latest MailHog release`_ for your OS. - -#. Rename the build to ``MailHog``. - -#. Copy the file to the project root. - -#. Make it executable: :: - - $ chmod +x MailHog - -#. Spin up another terminal window and start it there: :: - - ./MailHog - -#. Check out ``_ to see how it goes. - -Now you have your own mail server running locally, ready to receive whatever you send it. - -.. _`Download the latest MailHog release`: https://github.com/mailhog/MailHog/releases -{% endif %} -.. _mailhog: https://github.com/mailhog/MailHog {% endif %} {% if cookiecutter.use_sentry == "y" %} @@ -135,16 +110,6 @@ Deployment ---------- The following details how to deploy this application. -{% if cookiecutter.use_heroku.lower() == "y" %} - -Heroku -^^^^^^ - -See detailed `cookiecutter-django Heroku documentation`_. - -.. _`cookiecutter-django Heroku documentation`: http://cookiecutter-django.readthedocs.io/en/latest/deployment-on-heroku.html -{% endif %} -{% if cookiecutter.use_docker.lower() == "y" %} Docker ^^^^^^ @@ -152,22 +117,4 @@ Docker See detailed `cookiecutter-django Docker documentation`_. .. _`cookiecutter-django Docker documentation`: http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html -{% endif %} -{% if cookiecutter.custom_bootstrap_compilation == "y" %} -Custom Bootstrap Compilation -^^^^^^ - -The generated CSS is set up with automatic Bootstrap recompilation with variables of your choice. -Bootstrap v4 is installed using npm and customised by tweaking your variables in ``static/sass/custom_bootstrap_vars``. - -You can find a list of available variables `in the bootstrap source`_, or get explanations on them in the `Bootstrap docs`_. - -{% if cookiecutter.js_task_runner == 'Gulp' %} -Bootstrap's javascript as well as its dependencies is concatenated into a single file: ``static/js/vendors.js``. -{% endif %} - -.. _in the bootstrap source: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss -.. _Bootstrap docs: https://getbootstrap.com/docs/4.1/getting-started/theming/ - -{% endif %} diff --git a/{{cookiecutter.project_slug}}/config/settings/__init__.py b/{{cookiecutter.project_slug}}/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/config/settings/__init__.py rename to {{cookiecutter.project_slug}}/__init__.py diff --git a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile index 37f66ec78..441e687ca 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile @@ -1,4 +1,3 @@ -{% if cookiecutter.js_task_runner == 'Gulp' -%} FROM node:10-stretch-slim as client-builder WORKDIR /app @@ -8,7 +7,6 @@ COPY . /app RUN npm run build # Python build stage -{%- endif %} FROM python:3.6-alpine ENV PYTHONUNBUFFERED 1 @@ -56,11 +54,7 @@ RUN sed -i 's/\r$//g' /start-flower RUN chmod +x /start-flower {%- endif %} -{%- if cookiecutter.js_task_runner == 'Gulp' %} COPY --from=client-builder /app /app -{% else %} -COPY . /app -{%- endif %} RUN chown -R django /app diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile index eca29bada..49dc66d6c 100644 --- a/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:{{ cookiecutter.postgresql_version }} +FROM postgres:11-alpine COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance RUN chmod +x /usr/local/bin/maintenance/* diff --git a/{{cookiecutter.project_slug}}/config/__init__.py b/{{cookiecutter.project_slug}}/config/__init__.py deleted file mode 100644 index 480655af1..000000000 --- a/{{cookiecutter.project_slug}}/config/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -{% if cookiecutter.use_celery == 'y' -%} -# This will make sure the app is always imported when -# Django starts so that shared_task will use this app. -from .celery_app import app as celery_app - -__all__ = ("celery_app",) -{% endif -%} diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py deleted file mode 100644 index 214353deb..000000000 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ /dev/null @@ -1,305 +0,0 @@ -{% if cookiecutter.use_sentry == 'y' -%} -import logging - -import sentry_sdk - -from sentry_sdk.integrations.django import DjangoIntegration -from sentry_sdk.integrations.logging import LoggingIntegration -{%- if cookiecutter.use_celery == 'y' %} -from sentry_sdk.integrations.celery import CeleryIntegration -{% endif %} - -{% endif -%} -from .base import * # noqa -from .base import env - -# GENERAL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key -SECRET_KEY = env("DJANGO_SECRET_KEY") -# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["{{ cookiecutter.domain_name }}"]) - -# DATABASES -# ------------------------------------------------------------------------------ -DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 -DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 -DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 - -# CACHES -# ------------------------------------------------------------------------------ -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": env("REDIS_URL"), - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - # Mimicing memcache behavior. - # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior - "IGNORE_EXCEPTIONS": True, - }, - } -} - -# SECURITY -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect -SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) -# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure -SESSION_COOKIE_SECURE = True -# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure -CSRF_COOKIE_SECURE = True -# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds -# TODO: set this to 60 seconds first and then to 518400 once you prove the former works -SECURE_HSTS_SECONDS = 60 -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains -SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( - "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True -) -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload -SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) -# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff -SECURE_CONTENT_TYPE_NOSNIFF = env.bool( - "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True -) - -{% if cookiecutter.cloud_provider != 'None' -%} -# STORAGES -# ------------------------------------------------------------------------------ -# https://django-storages.readthedocs.io/en/latest/#installation -INSTALLED_APPS += ["storages"] # noqa F405 -{%- endif -%} -{% if cookiecutter.cloud_provider == 'AWS' %} -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_ACCESS_KEY_ID = env("DJANGO_AWS_ACCESS_KEY_ID") -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_SECRET_ACCESS_KEY = env("DJANGO_AWS_SECRET_ACCESS_KEY") -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_STORAGE_BUCKET_NAME = env("DJANGO_AWS_STORAGE_BUCKET_NAME") -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_QUERYSTRING_AUTH = False -# DO NOT change these unless you know what you're doing. -_AWS_EXPIRY = 60 * 60 * 24 * 7 -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_S3_OBJECT_PARAMETERS = { - "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" -} -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_DEFAULT_ACL = None -# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings -AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None) -{% elif cookiecutter.cloud_provider == 'GCP' %} -DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" -GS_BUCKET_NAME = env("DJANGO_GCP_STORAGE_BUCKET_NAME") -GS_DEFAULT_ACL = "publicRead" -{% endif -%} - -{% if cookiecutter.cloud_provider != 'None' or cookiecutter.use_whitenoise == 'y' -%} -# STATIC -# ------------------------ -{% endif -%} -{% if cookiecutter.use_whitenoise == 'y' -%} -STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" -{% elif cookiecutter.cloud_provider == 'AWS' -%} -STATICFILES_STORAGE = "config.settings.production.StaticRootS3Boto3Storage" -STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/" -{% elif cookiecutter.cloud_provider == 'GCP' -%} -STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/" -{% endif -%} - -# MEDIA -# ------------------------------------------------------------------------------ -{%- if cookiecutter.cloud_provider == 'AWS' %} -# region http://stackoverflow.com/questions/10390244/ -# Full-fledge class: https://stackoverflow.com/a/18046120/104731 -from storages.backends.s3boto3 import S3Boto3Storage # noqa E402 - - -class StaticRootS3Boto3Storage(S3Boto3Storage): - location = "static" - default_acl = "public-read" - - -class MediaRootS3Boto3Storage(S3Boto3Storage): - location = "media" - file_overwrite = False - - -# endregion -DEFAULT_FILE_STORAGE = "config.settings.production.MediaRootS3Boto3Storage" -MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/" -{%- elif cookiecutter.cloud_provider == 'GCP' %} -MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" -MEDIA_ROOT = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" -{%- endif %} - -# TEMPLATES -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#templates -TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405 - ( - "django.template.loaders.cached.Loader", - [ - "django.template.loaders.filesystem.Loader", - "django.template.loaders.app_directories.Loader", - ], - ) -] - -# EMAIL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email -DEFAULT_FROM_EMAIL = env( - "DJANGO_DEFAULT_FROM_EMAIL", default="{{cookiecutter.project_name}} " -) -# https://docs.djangoproject.com/en/dev/ref/settings/#server-email -SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) -# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix -EMAIL_SUBJECT_PREFIX = env( - "DJANGO_EMAIL_SUBJECT_PREFIX", default="[{{cookiecutter.project_name}}]" -) - -# ADMIN -# ------------------------------------------------------------------------------ -# Django Admin URL regex. -ADMIN_URL = env("DJANGO_ADMIN_URL") - -# Anymail (Mailgun) -# ------------------------------------------------------------------------------ -# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail -INSTALLED_APPS += ["anymail"] # noqa F405 -EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" -# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference -ANYMAIL = { - "MAILGUN_API_KEY": env("MAILGUN_API_KEY"), - "MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"), - "MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"), -} - -{% if cookiecutter.use_whitenoise == 'y' -%} -# WhiteNoise -# ------------------------------------------------------------------------------ -# http://whitenoise.evans.io/en/latest/django.html#enable-whitenoise -MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") # noqa F405 - -{% endif %} -{%- if cookiecutter.use_compressor == 'y' -%} -# django-compressor -# ------------------------------------------------------------------------------ -# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED -COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True) -# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE -COMPRESS_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" -# 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 %} -{% endif %} -{%- if cookiecutter.use_whitenoise == 'n' -%} -# Collectfast -# ------------------------------------------------------------------------------ -# https://github.com/antonagestam/collectfast#installation -INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa F405 -AWS_PRELOAD_METADATA = True -{% endif %} -# LOGGING -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#logging -# See https://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -{% if cookiecutter.use_sentry == 'n' -%} -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, - "formatters": { - "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" - } - }, - "handlers": { - "mail_admins": { - "level": "ERROR", - "filters": ["require_debug_false"], - "class": "django.utils.log.AdminEmailHandler", - }, - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "root": {"level": "INFO", "handlers": ["console"]}, - "loggers": { - "django.request": { - "handlers": ["mail_admins"], - "level": "ERROR", - "propagate": True, - }, - "django.security.DisallowedHost": { - "level": "ERROR", - "handlers": ["console", "mail_admins"], - "propagate": True, - }, - }, -} -{% else %} -LOGGING = { - "version": 1, - "disable_existing_loggers": True, - "formatters": { - "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" - } - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - } - }, - "root": {"level": "INFO", "handlers": ["console"]}, - "loggers": { - "django.db.backends": { - "level": "ERROR", - "handlers": ["console"], - "propagate": False, - }, - # Errors logged by the SDK itself - "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False}, - "django.security.DisallowedHost": { - "level": "ERROR", - "handlers": ["console"], - "propagate": False, - }, - }, -} - -# Sentry -# ------------------------------------------------------------------------------ -SENTRY_DSN = env("SENTRY_DSN") -SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) - -sentry_logging = LoggingIntegration( - level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs - event_level=logging.ERROR, # Send errors as events -) - -{%- if cookiecutter.use_celery == 'y' %} -sentry_sdk.init( - dsn=SENTRY_DSN, - integrations=[sentry_logging, DjangoIntegration(), CeleryIntegration()], -) -{% else %} -sentry_sdk.init(dsn=SENTRY_DSN, integrations=[sentry_logging, DjangoIntegration()]) -{% endif -%} -{% endif %} -# Your stuff... -# ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/gulpfile.js b/{{cookiecutter.project_slug}}/gulpfile.js index 3c28a7359..7b2b8535e 100644 --- a/{{cookiecutter.project_slug}}/gulpfile.js +++ b/{{cookiecutter.project_slug}}/gulpfile.js @@ -9,9 +9,6 @@ const pjson = require('./package.json') // Plugins const autoprefixer = require('autoprefixer') const browserSync = require('browser-sync').create() -{% if cookiecutter.custom_bootstrap_compilation == 'y' %} -const concat = require('gulp-concat') -{% endif %} const cssnano = require ('cssnano') const imagemin = require('gulp-imagemin') const pixrem = require('pixrem') @@ -29,14 +26,6 @@ function pathsConfig(appName) { const vendorsRoot = 'node_modules' return { - {% if cookiecutter.custom_bootstrap_compilation == 'y' %} - bootstrapSass: `${vendorsRoot}/bootstrap/scss`, - vendorsJs: [ - `${vendorsRoot}/jquery/dist/jquery.slim.js`, - `${vendorsRoot}/popper.js/dist/umd/popper.js`, - `${vendorsRoot}/bootstrap/dist/js/bootstrap.js`, - ], - {% endif %} app: this.app, templates: `${this.app}/templates`, css: `${this.app}/static/css`, @@ -67,9 +56,6 @@ function styles() { return src(`${paths.sass}/project.scss`) .pipe(sass({ includePaths: [ - {% if cookiecutter.custom_bootstrap_compilation == 'y' %} - paths.bootstrapSass, - {% endif %} paths.sass ] }).on('error', sass.logError)) @@ -90,19 +76,6 @@ function scripts() { .pipe(dest(paths.js)) } -{% if cookiecutter.custom_bootstrap_compilation == 'y' %} -// Vendor Javascript minification -function vendorScripts() { - return src(paths.vendorsJs) - .pipe(concat('vendors.js')) - .pipe(dest(paths.js)) - .pipe(plumber()) // Checks for errors - .pipe(uglify()) // Minifies the js - .pipe(rename({ suffix: '.min' })) - .pipe(dest(paths.js)) -} -{% endif %} - // Image compression function imgCompression() { return src(`${paths.images}/*`) @@ -128,22 +101,7 @@ function initBrowserSync() { `${paths.templates}/*.html` ], { // https://www.browsersync.io/docs/options/#option-proxy - {%- if cookiecutter.use_docker == 'n' %} proxy: 'localhost:8000' - {% else %} - proxy: { - target: 'django:8000', - proxyReq: [ - function(proxyReq, req) { - // Assign proxy "host" header same as current request at Browsersync server - proxyReq.setHeader('Host', req.headers.host) - } - ] - }, - // https://www.browsersync.io/docs/options/#option-open - // Disable as it doesn't work from inside a container - open: false - {%- endif %} } ) } @@ -159,15 +117,12 @@ function watchPaths() { const generateAssets = parallel( styles, scripts, - {% if cookiecutter.custom_bootstrap_compilation == 'y' %}vendorScripts,{% endif %} imgCompression ) // Set up dev environment const dev = parallel( - {%- if cookiecutter.use_docker == 'n' %} runServer, - {%- endif %} initBrowserSync, watchPaths ) diff --git a/{{cookiecutter.project_slug}}/local.yml b/{{cookiecutter.project_slug}}/local.yml index 60f8f9228..ef2dce986 100644 --- a/{{cookiecutter.project_slug}}/local.yml +++ b/{{cookiecutter.project_slug}}/local.yml @@ -79,7 +79,6 @@ services: command: /start-flower {%- endif %} - {%- if cookiecutter.js_task_runner == 'Gulp' %} node: build: @@ -97,5 +96,3 @@ services: - "3000:3000" # Expose browsersync UI: https://www.browsersync.io/docs/options/#option-ui - "3001:3001" - - {%- endif %} diff --git a/{{cookiecutter.project_slug}}/package.json b/{{cookiecutter.project_slug}}/package.json index 6edf2e114..b5cd6a794 100644 --- a/{{cookiecutter.project_slug}}/package.json +++ b/{{cookiecutter.project_slug}}/package.json @@ -3,13 +3,6 @@ "version": "{{ cookiecutter.version }}", "dependencies": {}, "devDependencies": { - {% if cookiecutter.js_task_runner == 'Gulp' -%} - {% if cookiecutter.custom_bootstrap_compilation == 'y' -%} - "bootstrap": "4.3.1", - "gulp-concat": "^2.6.1", - "jquery": "3.3.1", - "popper.js": "1.14.3", - {% endif -%} "autoprefixer": "^9.4.7", "browser-sync": "^2.14.0", "cssnano": "^4.1.10", @@ -21,7 +14,6 @@ "gulp-sass": "^4.0.2", "gulp-uglify-es": "^1.0.4", "pixrem": "^5.0.0" - {%- endif %} }, "engines": { "node": ">=8" @@ -30,9 +22,7 @@ "last 2 versions" ], "scripts": { - {% if cookiecutter.js_task_runner == 'Gulp' -%} "dev": "gulp", "build": "gulp generate-assets" - {%- endif %} } } diff --git a/{{cookiecutter.project_slug}}/pytest.ini b/{{cookiecutter.project_slug}}/pytest.ini index 9fb488cbc..efa7e432a 100644 --- a/{{cookiecutter.project_slug}}/pytest.ini +++ b/{{cookiecutter.project_slug}}/pytest.ini @@ -1,5 +1,3 @@ [pytest] addopts = --ds=config.settings.test -{%- if cookiecutter.js_task_runner != 'None' %} norecursedirs = node_modules -{%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index 9513b2238..8b1151eb1 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -2,7 +2,7 @@ pytz==2019.1 # https://github.com/stub42/pytz python-slugify==3.0.2 # https://github.com/un33k/python-slugify Pillow==6.1.0 # https://github.com/python-pillow/Pillow {%- if cookiecutter.use_compressor == "y" %} -rcssmin==1.0.6{% if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %} --install-option="--without-c-extensions"{% endif %} # https://github.com/ndparker/rcssmin +rcssmin==1.0.6 # https://github.com/ndparker/rcssmin {%- endif %} argon2-cffi==19.1.0 # https://github.com/hynek/argon2_cffi {%- if cookiecutter.use_whitenoise == 'y' %} @@ -12,10 +12,8 @@ redis==3.2.1 # https://github.com/antirez/redis {%- if cookiecutter.use_celery == "y" %} celery==4.3.0 # pyup: < 5.0 # https://github.com/celery/celery django-celery-beat==1.5.0 # https://github.com/celery/django-celery-beat -{%- if cookiecutter.use_docker == 'y' %} flower==0.9.3 # https://github.com/mher/flower {%- endif %} -{%- endif %} # Django # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index cfb2d28f7..c3c8b4069 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -3,11 +3,7 @@ Werkzeug==0.14.1 # pyup: < 0.15 # https://github.com/pallets/werkzeug ipdb==0.12 # https://github.com/gotcha/ipdb Sphinx==2.1.2 # https://github.com/sphinx-doc/sphinx -{%- if cookiecutter.use_docker == 'y' %} -psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 -{%- else %} psycopg2-binary==2.8.3 # https://github.com/psycopg/psycopg2 -{%- endif %} # Testing # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/utility/install_python_dependencies.sh b/{{cookiecutter.project_slug}}/utility/install_python_dependencies.sh index 517934841..77dd95f9c 100755 --- a/{{cookiecutter.project_slug}}/utility/install_python_dependencies.sh +++ b/{{cookiecutter.project_slug}}/utility/install_python_dependencies.sh @@ -35,7 +35,5 @@ if [ -z "$VIRTUAL_ENV" ]; then else pip install -r $PROJECT_DIR/requirements/local.txt - {% if cookiecutter.use_heroku == "y" -%} pip install -r $PROJECT_DIR/requirements.txt - {%- endif %} fi diff --git a/{{cookiecutter.project_slug}}/config/celery_app.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/celery_app.py similarity index 100% rename from {{cookiecutter.project_slug}}/config/celery_app.py rename to {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/celery_app.py diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/__init__.py new file mode 100644 index 000000000..b4243811e --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/__init__.py @@ -0,0 +1,4 @@ +try: + from .dev import * # NOQA +except ImportError: + from .prod import * # NOQA diff --git a/{{cookiecutter.project_slug}}/config/settings/base.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/base.py similarity index 51% rename from {{cookiecutter.project_slug}}/config/settings/base.py rename to {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/base.py index 7def8f489..e83c7c5e3 100644 --- a/{{cookiecutter.project_slug}}/config/settings/base.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/base.py @@ -1,7 +1,21 @@ """ Base settings to build other settings files upon. """ +{% if cookiecutter.use_sentry == 'y' -%} +import logging +import sentry_sdk + +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.logging import LoggingIntegration +{%- if cookiecutter.use_celery == 'y' %} +from sentry_sdk.integrations.celery import CeleryIntegration +{% endif %}{% endif -%} +{% if cookiecutter.cloud_provider == 'AWS' %} +from storages.backends.s3boto3 import S3Boto3Storage +{% elif cookiecutter.cloud_provider == 'Azure' %} +from storages.backends.azure_storage import AzureStorage +{%- endif %} import environ ROOT_DIR = ( @@ -11,51 +25,29 @@ APPS_DIR = ROOT_DIR.path("{{ cookiecutter.project_slug }}") env = environ.Env() -READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) -if READ_DOT_ENV_FILE: - # OS environment variables take precedence over variables from .env - env.read_env(str(ROOT_DIR.path(".env"))) +# OS environment variables take precedence over variables from .env +env.read_env(str(ROOT_DIR.path(".env"))) # GENERAL # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#debug DEBUG = env.bool("DJANGO_DEBUG", False) -# Local time zone. Choices are -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# though not all of them may be available with every OS. -# In Windows, this must be set to your system time zone. -TIME_ZONE = "{{ cookiecutter.timezone }}" -# https://docs.djangoproject.com/en/dev/ref/settings/#language-code -LANGUAGE_CODE = "en-us" -# https://docs.djangoproject.com/en/dev/ref/settings/#site-id SITE_ID = 1 -# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n -USE_I18N = True -# https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n -USE_L10N = True -# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz -USE_TZ = True -# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths -LOCALE_PATHS = [ROOT_DIR.path("locale")] +ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["{{ cookiecutter.domain_name }}"]) + # DATABASES -# ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#databases -{% if cookiecutter.use_docker == "y" -%} +# ------------------------------------------------------------------------------ DATABASES = {"default": env.db("DATABASE_URL")} -{%- else %} -DATABASES = { - "default": env.db("DATABASE_URL", default="postgres://{% if cookiecutter.windows == 'y' %}localhost{% endif %}/{{cookiecutter.project_slug}}") -} -{%- endif %} DATABASES["default"]["ATOMIC_REQUESTS"] = True +DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) + # URLS # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf -ROOT_URLCONF = "config.urls" -# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application -WSGI_APPLICATION = "config.wsgi.application" +ROOT_URLCONF = "{{ cookiecutter.project_slug }}.urls" +WSGI_APPLICATION = "{{ cookiecutter.project_slug }}.wsgi.application" + # APPS # ------------------------------------------------------------------------------ @@ -70,7 +62,6 @@ DJANGO_APPS = [ "django.contrib.admin", ] THIRD_PARTY_APPS = [ - "crispy_forms", "allauth", "allauth.account", "allauth.socialaccount", @@ -78,37 +69,84 @@ THIRD_PARTY_APPS = [ {%- if cookiecutter.use_celery == 'y' %} "django_celery_beat", {%- endif %} +{%- if cookiecutter.use_compressor == 'y' %} + "compressor", +{%- endif %} +{%- if cookiecutter.cloud_provider != 'None' %} + "storages", +{%- endif %} ] LOCAL_APPS = [ "{{ cookiecutter.project_slug }}.users.apps.UsersConfig", # Your stuff: custom apps go here ] -# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS -# MIGRATIONS + +# CACHES +# https://docs.djangoproject.com/en/2.2/ref/settings/#caches # ------------------------------------------------------------------------------ +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": env("REDIS_URL"), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + # Mimicing memcache behavior. + # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior + "IGNORE_EXCEPTIONS": True, + }, + } +} + + +# SECURITY +# ------------------------------------------------------------------------------ +CSRF_COOKIE_HTTPONLY = True +CSRF_COOKIE_SECURE = True +SECRET_KEY = env("DJANGO_SECRET_KEY") +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) +# TODO: set this to 60 seconds first and then to 518400 once you prove the former works +SECURE_HSTS_SECONDS = 60 +SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( + "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True +) +SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) +SECURE_CONTENT_TYPE_NOSNIFF = env.bool( + "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True +) +SECURE_BROWSER_XSS_FILTER = True +X_FRAME_OPTIONS = "DENY" + + +# SESSIONS +# https://docs.djangoproject.com/en/2.2/ref/settings/#sessions +# ------------------------------------------------------------------------------ +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SECURE = True +SESSION_ENGINE = "django.contrib.sessions.backends.cache" + + +# MIGRATIONS # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules +# ------------------------------------------------------------------------------ MIGRATION_MODULES = {"sites": "{{ cookiecutter.project_slug }}.contrib.sites.migrations"} + # AUTHENTICATION +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", ] -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model AUTH_USER_MODEL = "users.User" -# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url -LOGIN_REDIRECT_URL = "users:redirect" -# https://docs.djangoproject.com/en/dev/ref/settings/#login-url -LOGIN_URL = "account_login" -# PASSWORDS -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers +# use named URL patterns instead of hardcoded paths +LOGIN_REDIRECT_URL = "users:redirect" +LOGIN_URL = "account_login" PASSWORD_HASHERS = [ # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django "django.contrib.auth.hashers.Argon2PasswordHasher", @@ -116,7 +154,6 @@ PASSWORD_HASHERS = [ "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", ] -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" @@ -127,10 +164,13 @@ AUTH_PASSWORD_VALIDATORS = [ ] # MIDDLEWARE -# ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#middleware +# ------------------------------------------------------------------------------ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", +{%- if cookiecutter.use_whitenoise == 'y' %} + "whitenoise.middleware.WhiteNoiseMiddleware", +{%- endif %} "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", @@ -140,44 +180,131 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -# STATIC + +# Globalization (i18n/l10n) +# https://docs.djangoproject.com/en/2.2/ref/settings/#globalization-i18n-l10n +# ------------------------------------------------------------------------------ +TIME_ZONE = "{{ cookiecutter.timezone }}" +LANGUAGE_CODE = "en-us" +USE_I18N = True +USE_L10N = True +USE_TZ = True +LOCALE_PATHS = [ROOT_DIR.path("locale")] + +{%- if cookiecutter.cloud_provider != 'None' %} + + +# STORAGES +# https://django-storages.readthedocs.io/en/latest/#installation +# ------------------------------------------------------------------------------ +{% if cookiecutter.cloud_provider == 'AWS' %} +# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings +AWS_ACCESS_KEY_ID = env("DJANGO_AWS_ACCESS_KEY_ID") +AWS_SECRET_ACCESS_KEY = env("DJANGO_AWS_SECRET_ACCESS_KEY") +AWS_STORAGE_BUCKET_NAME = env("DJANGO_AWS_STORAGE_BUCKET_NAME") +AWS_QUERYSTRING_AUTH = False +_AWS_EXPIRY = 60 * 60 * 24 * 7 +AWS_S3_OBJECT_PARAMETERS = { + "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" +} +AWS_DEFAULT_ACL = None +AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None) +{% elif cookiecutter.cloud_provider == 'GCP' %} +GS_BUCKET_NAME = env("DJANGO_GCP_STORAGE_BUCKET_NAME") +GS_DEFAULT_ACL = "publicRead" +{% elif cookiecutter.cloud_provider == 'Azure' %} +# https://django-storages.readthedocs.io/en/latest/backends/azure.html#install +AZURE_ACCOUNT_NAME = env("DJANGO_AZURE_ACCOUNT_NAME") +AZURE_ACCOUNT_KEY = env("DJANGO_AZURE_ACCOUNT_KEY") +AZURE_CONTAINER = env("DJANGO_AZURE_CONTAINER") +AZURE_URL_EXPIRATION_SECS = 60 * 60 * 24 * 7 +{% endif -%} + + +# STATIC +# https://docs.djangoproject.com/en/2.2/ref/settings/#static-files # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#static-root STATIC_ROOT = str(ROOT_DIR("staticfiles")) -# https://docs.djangoproject.com/en/dev/ref/settings/#static-url -STATIC_URL = "/static/" -# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS STATICFILES_DIRS = [str(APPS_DIR.path("static"))] -# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", +{%- if cookiecutter.use_compressor == 'y' %} + "compressor.finders.CompressorFinder", +{%- endif %} ] +{%- if cookiecutter.cloud_provider == 'AWS' %} -# MEDIA + +class StaticRootS3Boto3Storage(S3Boto3Storage): + location = "static" + default_acl = "public-read" + + +STATICFILES_STORAGE = "settings.base.StaticRootS3Boto3Storage" +STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/" +{% elif cookiecutter.cloud_provider == 'GCP' %} +STATICFILES_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" +STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/" +{% elif cookiecutter.cloud_provider == 'Azure' %} + + +class PublicAzureStorage(AzureStorage): + account_name = AZURE_ACCOUNT_NAME + account_key = AZURE_ACCOUNT_KEY + azure_container = AZURE_CONTAINER + expiration_secs = None + +STATICFILES_STORAGE = "settings.base.PublicAzureStorage" +{% elif cookiecutter.use_whitenoise == 'y' %} +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +{% else %} +STATIC_URL = "/static/" +{%- endif %} +{%- if cookiecutter.use_compressor == 'y' %} +COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True) +COMPRESS_STORAGE = STATICFILES_STORAGE +COMPRESS_URL = STATIC_URL +{% endif %} + +{%- endif %} + +# FILE UPLOADS +# https://docs.djangoproject.com/en/2.2/ref/settings/#file-uploads # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#media-root -MEDIA_ROOT = str(APPS_DIR("media")) -# https://docs.djangoproject.com/en/dev/ref/settings/#media-url +{%- if cookiecutter.cloud_provider == 'AWS' %} + + +class MediaRootS3Boto3Storage(S3Boto3Storage): + location = "media" + file_overwrite = False + + +DEFAULT_FILE_STORAGE = "config.settings.production.MediaRootS3Boto3Storage" +MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/" +{% elif cookiecutter.cloud_provider == 'GCP' %} +DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" +MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" +MEDIA_ROOT = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" +{% elif cookiecutter.cloud_provider == 'Azure' %} +DEFAULT_FILE_STORAGE = "storages.backends.azure_storage.AzureStorage" +{% else %} +MEDIA_ROOT = str(ROOT_DIR("media")) MEDIA_URL = "/media/" +{%- endif %} # TEMPLATES -# ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#templates +# ------------------------------------------------------------------------------ TEMPLATES = [ { - # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND "BACKEND": "django.template.backends.django.DjangoTemplates", - # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs "DIRS": [str(APPS_DIR.path("templates"))], "OPTIONS": { - # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders - # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types "loaders": [ "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", ], - # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", @@ -191,46 +318,42 @@ TEMPLATES = [ }, } ] -# http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs -CRISPY_TEMPLATE_PACK = "bootstrap4" # FIXTURES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) -# SECURITY -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly -SESSION_COOKIE_HTTPONLY = True -# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly -CSRF_COOKIE_HTTPONLY = True -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter -SECURE_BROWSER_XSS_FILTER = True -# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options -X_FRAME_OPTIONS = "DENY" # EMAIL +# https://docs.djangoproject.com/en/2.2/ref/settings/#email # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = env( "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend" ) -# https://docs.djangoproject.com/en/2.2/ref/settings/#email-timeout EMAIL_TIMEOUT = 5 +EMAIL_HOST = env("EMAIL_HOST", default="localhost") +EMAIL_HOST_USER = env("EMAIL_HOST_USER", default="") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", default="") +EMAIL_PORT = env.int("EMAIL_PORT", 25) +EMAIL_SUBJECT_PREFIX = env( + "DJANGO_EMAIL_SUBJECT_PREFIX", default="[{{cookiecutter.project_name}}]" +) +DEFAULT_FROM_EMAIL = env( + "DJANGO_DEFAULT_FROM_EMAIL", default="{{cookiecutter.project_name}} " +) +SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) + # ADMIN # ------------------------------------------------------------------------------ -# Django Admin URL. -ADMIN_URL = "admin/" -# https://docs.djangoproject.com/en/dev/ref/settings/#admins +ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/") ADMINS = [("""{{cookiecutter.author_name}}""", "{{cookiecutter.email}}")] -# https://docs.djangoproject.com/en/dev/ref/settings/#managers MANAGERS = ADMINS # LOGGING -# ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#logging +# ------------------------------------------------------------------------------ # See https://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { @@ -250,34 +373,65 @@ LOGGING = { } }, "root": {"level": "INFO", "handlers": ["console"]}, + "loggers": { + "django.db.backends": { + "level": "ERROR", + "handlers": ["console"], + "propagate": False, + }, + "django.security.DisallowedHost": { + "level": "ERROR", + "handlers": ["console"], + "propagate": False, + }, +{%- if cookiecutter.use_sentry == 'y' %} + "sentry_sdk": { # Errors logged by Sentry's SDK itself + "level": "ERROR", + "handlers": ["console"], + "propagate": False, + }, + {%- endif %} + }, } -{% if cookiecutter.use_celery == 'y' -%} +{%- if cookiecutter.use_sentry == 'y' %} +# Sentry +# ------------------------------------------------------------------------------ +SENTRY_DSN = env("SENTRY_DSN") +SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) + +sentry_logging = LoggingIntegration( + level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs + event_level=logging.ERROR, # Send errors as events +) +{%- if cookiecutter.use_celery == 'y' %} +sentry_sdk.init( + dsn=SENTRY_DSN, + integrations=[sentry_logging, DjangoIntegration(), CeleryIntegration()], +) +{%- else %} +sentry_sdk.init(dsn=SENTRY_DSN, integrations=[sentry_logging, DjangoIntegration()]) +{% endif -%}{%- endif %} +{%- if cookiecutter.use_celery == 'y' %} + + # Celery +# http://docs.celeryproject.org/en/latest/userguide/configuration.html # ------------------------------------------------------------------------------ if USE_TZ: - # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone CELERY_TIMEZONE = TIME_ZONE -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url CELERY_BROKER_URL = env("CELERY_BROKER_URL") -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend CELERY_RESULT_BACKEND = CELERY_BROKER_URL -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content CELERY_ACCEPT_CONTENT = ["json"] -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer CELERY_TASK_SERIALIZER = "json" -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer CELERY_RESULT_SERIALIZER = "json" -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit # TODO: set to whatever value is adequate in your circumstances CELERY_TASK_TIME_LIMIT = 5 * 60 -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit # TODO: set to whatever value is adequate in your circumstances CELERY_TASK_SOFT_TIME_LIMIT = 60 -# http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" - {%- endif %} + # django-allauth # ------------------------------------------------------------------------------ ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) @@ -292,13 +446,6 @@ ACCOUNT_ADAPTER = "{{cookiecutter.project_slug}}.users.adapters.AccountAdapter" # https://django-allauth.readthedocs.io/en/latest/configuration.html SOCIALACCOUNT_ADAPTER = "{{cookiecutter.project_slug}}.users.adapters.SocialAccountAdapter" -{% if cookiecutter.use_compressor == 'y' -%} -# django-compressor -# ------------------------------------------------------------------------------ -# https://django-compressor.readthedocs.io/en/latest/quickstart/#installation -INSTALLED_APPS += ["compressor"] -STATICFILES_FINDERS += ["compressor.finders.CompressorFinder"] -{%- endif %} # Your stuff... # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/config/settings/local.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/dev_template.py similarity index 89% rename from {{cookiecutter.project_slug}}/config/settings/local.py rename to {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/dev_template.py index bbc9ab357..1088a2d1c 100644 --- a/{{cookiecutter.project_slug}}/config/settings/local.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/dev_template.py @@ -25,12 +25,9 @@ CACHES = { # EMAIL # ------------------------------------------------------------------------------ -{% if cookiecutter.use_mailhog == 'y' and cookiecutter.use_docker == 'y' -%} +{% if cookiecutter.use_mailhog == 'y' -%} # https://docs.djangoproject.com/en/dev/ref/settings/#email-host EMAIL_HOST = env("EMAIL_HOST", default="mailhog") -{%- elif cookiecutter.use_mailhog == 'y' and cookiecutter.use_docker == 'n' -%} -# https://docs.djangoproject.com/en/dev/ref/settings/#email-host -EMAIL_HOST = "localhost" {%- else -%} # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = env( @@ -55,13 +52,11 @@ DEBUG_TOOLBAR_CONFIG = { } # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] -{% if cookiecutter.use_docker == 'y' -%} if env("USE_DOCKER") == "yes": import socket hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) INTERNAL_IPS += [ip[:-1] + "1" for ip in ips] -{%- endif %} # django-extensions # ------------------------------------------------------------------------------ @@ -71,10 +66,8 @@ INSTALLED_APPS += ["django_extensions"] # noqa F405 # Celery # ------------------------------------------------------------------------------ -{% if cookiecutter.use_docker == 'n' -%} # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-always-eager CELERY_TASK_ALWAYS_EAGER = True -{%- endif %} # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates CELERY_TASK_EAGER_PROPAGATES = True diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/prod.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/prod.py new file mode 100644 index 000000000..ea1063d7c --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/prod.py @@ -0,0 +1,15 @@ +from .base import * # noqa + + +# TEMPLATES +# https://docs.djangoproject.com/en/dev/ref/settings/#templates +# ------------------------------------------------------------------------------ +TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405 + ( + "django.template.loaders.cached.Loader", + [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ], + ) +] diff --git a/{{cookiecutter.project_slug}}/config/settings/test.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/test.py similarity index 100% rename from {{cookiecutter.project_slug}}/config/settings/test.py rename to {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/test.py diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/project.scss b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/project.scss index 9fbf1b746..be2a0c7bd 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/project.scss +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/project.scss @@ -1,10 +1,3 @@ -{% if cookiecutter.custom_bootstrap_compilation == 'y' %} -@import "custom_bootstrap_vars"; -@import "bootstrap"; -{% endif %} - - - // project specific CSS goes here //////////////////////////////// diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html index cba1b7cf7..457599c9b 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html @@ -16,15 +16,13 @@ {% block css %} - {% endraw %}{% if cookiecutter.custom_bootstrap_compilation == "n" %}{% raw %} - {% endraw %}{% endif %}{% raw %} {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% compress css %}{% endraw %}{% endif %}{% raw %} - {% endraw %}{% if cookiecutter.js_task_runner == "Gulp" and cookiecutter.use_compressor == "n" %}{% raw %} + {% endraw %}{% if cookiecutter.use_compressor == "n" %}{% raw %} {% endraw %}{% else %}{% raw %} @@ -96,22 +94,19 @@ ================================================== --> {% block javascript %} - {% endraw %}{% if cookiecutter.custom_bootstrap_compilation == "y" and cookiecutter.js_task_runner == "Gulp" %}{% raw %} {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% compress js %}{% endraw %}{% endif %}{% raw %} {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% endcompress %}{% endraw %}{% endif %}{% raw %} - {% endraw %}{% else %}{% raw %} - {% endraw %}{% endif %}{% raw %} - {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% compress js %}{% endraw %}{% endif %}{% raw %} + {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% compress js %}{% endraw %}{% endif %}{% raw %} {% endraw %}{% if cookiecutter.use_compressor == "y" %}{% raw %}{% endcompress %}{% endraw %}{% endif %}{% raw %} diff --git a/{{cookiecutter.project_slug}}/config/urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/urls.py similarity index 100% rename from {{cookiecutter.project_slug}}/config/urls.py rename to {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/urls.py diff --git a/{{cookiecutter.project_slug}}/config/wsgi.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/wsgi.py similarity index 100% rename from {{cookiecutter.project_slug}}/config/wsgi.py rename to {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/wsgi.py