"
3 |
4 | WORKDIR /app/
5 |
6 | COPY requirements.txt .
7 |
8 | RUN pip install --no-cache-dir -q -r requirements.txt
9 |
10 | COPY . .
11 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: no cover
4 | def __unicode__
5 | def __repr__
6 | def __str__
7 | AssertionError
8 | NotImplementedError
9 |
10 | omit =
11 | manage.py
12 | app/*/migrations/*
13 |
14 | show_missing = True
15 |
--------------------------------------------------------------------------------
/core/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url, include
2 |
3 | from app.routers import router
4 | from app.views import IndexView
5 |
6 | urlpatterns = [
7 | url(r'^$', IndexView.as_view(), name='index'),
8 | url(r'^api/', include(router.urls)),
9 | ]
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [{*.{html,js,yml,css,sh},.coveragerc}]
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SECRET_KEY={Will be automatically generated when not in PRODUCTION; required}
2 | PRODUCTION={Boolean as 0 (false) or 1 (true); false by default}
3 | DEBUG={Boolean as 0 (false) or 1 (true); false if in PRODUCTION, else true}
4 | ALLOWED_HOSTS={Lists of hosts separated by comma}
5 | GOOGLE_ANALYTICS_ID={}
6 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | # Do not use this config file alone
4 | # Instead, do "./compose.sh dev [compose commands]"
5 |
6 | services:
7 |
8 | server:
9 | command: python manage.py runserver 0.0.0.0:8000
10 | environment:
11 | - PRODUCTION=0
12 | volumes:
13 | - .:/app
14 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Files
2 | app.yaml
3 | compose.sh
4 | .coveragerc
5 | Dockerfile
6 | .dockerignore
7 | docker-compose.yml
8 | docker-compose.dev.yml
9 | .editorconfig
10 | .env
11 | .env.example
12 | .gitignore
13 | Procfile
14 | README.md
15 | runtime.txt
16 | .travis.yml
17 |
18 | # Directories
19 | .git/
20 | .idea/
21 | media/
22 | static/
23 | [v]env/
24 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 |
5 | server:
6 | container_name: djskgen_server
7 | build: .
8 | command: gunicorn -b 0.0.0.0 core.wsgi --log-file -
9 | env_file:
10 | - .env
11 | environment:
12 | - PRODUCTION=1
13 | # ALLOWED_HOSTS=* # Must set the allowed host(s) in the production environment through ".env" file
14 | ports:
15 | - "8000:8000"
16 |
--------------------------------------------------------------------------------
/.idea/webResources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/core/tests.py:
--------------------------------------------------------------------------------
1 | from threading import Thread
2 | from unittest import TestCase
3 | from wsgiref.simple_server import make_server
4 |
5 | from core.wsgi import application
6 |
7 |
8 | class TestServer(TestCase):
9 | def test_that_server_turns_on_ok_through_wsgi(self):
10 | httpd = make_server('', 8912, application)
11 | Thread(target=httpd.serve_forever).start() # Start in a thread because it blocks the execution
12 | httpd.shutdown()
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "3.6"
5 |
6 | install: "pip install -r requirements.txt coverage"
7 |
8 | env:
9 | global:
10 | - SECRET_KEY="u@)7349fg!_dzzk^ok(!jyf7hb7jpagnkdpy$zjniz3=2=7snh"
11 | matrix:
12 | - DEBUG=0 PRODUCTION=0
13 | - DEBUG=0 PRODUCTION=1
14 | - DEBUG=1 PRODUCTION=1
15 | - DEBUG=1 PRODUCTION=0
16 |
17 | script:
18 | - coverage run manage.py test
19 |
20 | after_success:
21 | - bash <(curl -s https://codecov.io/bash)
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Django Secret Key Generator
4 |
5 | [![Build Status][travis_badge]][travis_link]
6 | [![codecov][codecov_badge]][codecov_link]
7 |
8 | Live on [https://djskgen.herokuapp.com/](https://djskgen.herokuapp.com/).
9 |
10 |
11 | [travis_badge]: https://travis-ci.org/jlhg/djskgen.svg?branch=master
12 | [travis_link]: https://travis-ci.org/jlhg/djskgen
13 | [codecov_badge]: https://codecov.io/gh/jlhg/djskgen/branch/master/graph/badge.svg
14 | [codecov_link]: https://codecov.io/gh/jlhg/djskgen
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Files
2 | *.egg*
3 | .env
4 | *.py[cod]
5 |
6 | # Directories
7 | media/
8 | static/
9 | [v]env/
10 |
11 | # JetBrains settings: https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
12 | # User-specific stuff
13 | .idea/**/workspace.xml
14 | .idea/**/tasks.xml
15 | .idea/**/dictionaries
16 | .idea/**/shelf
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 |
26 |
--------------------------------------------------------------------------------
/app/assets/toaster.js:
--------------------------------------------------------------------------------
1 | // https://www.w3schools.com/howto/howto_js_snackbar.asp
2 |
3 | window.showToaster = (function () {
4 | return function _showToaster(message) {
5 | var toaster = document.querySelector('.toaster');
6 |
7 | if (/show/.test(toaster.classList)) {
8 | return
9 | }
10 |
11 | toaster.innerText = message;
12 | toaster.className += ' show';
13 |
14 | setTimeout(function _hideToaster() {
15 | toaster.innerText = message;
16 | toaster.className = toaster.className.replace(' show', '');
17 | }, 3000);
18 | }
19 | })();
20 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == '__main__':
6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | message = (
11 | "Couldn't import Django. Are you sure it's installed and "
12 | 'available on your PYTHONPATH environment variable? Did you '
13 | 'forget to activate a virtual environment?'
14 | )
15 | raise ImportError(message) from exc
16 | execute_from_command_line(sys.argv)
17 |
--------------------------------------------------------------------------------
/app/tests/integration.py:
--------------------------------------------------------------------------------
1 | from django.test import SimpleTestCase
2 | from rest_framework import status
3 | from rest_framework.test import APIClient
4 |
5 |
6 | class APITestCase(SimpleTestCase):
7 | client = APIClient()
8 |
9 |
10 | class TestIndexAPI(APITestCase):
11 | def test_that_it_responds_with_ok_status(self):
12 | response = self.client.get('/')
13 |
14 | self.assertEqual(response.status_code, status.HTTP_200_OK)
15 |
16 |
17 | class TestSecretKeysViewAPI(APITestCase):
18 | def test_that_it_responds_with_ok_status(self):
19 | response = self.client.get('/api/secret_key/')
20 |
21 | self.assertEqual(response.status_code, status.HTTP_200_OK)
22 |
--------------------------------------------------------------------------------
/app/assets/githubCorners.css:
--------------------------------------------------------------------------------
1 | .github-corner:hover .octo-arm {
2 | animation: octocat-wave 560ms ease-in-out
3 | }
4 |
5 | .github-corner svg {
6 | top: 0;
7 | right: 0;
8 | border: 0;
9 | color: #fff;
10 | fill: #151513;
11 | position: absolute;
12 | }
13 |
14 | .github-corner .octo-arm {
15 | transform-origin: 130px 106px;
16 | }
17 |
18 | @keyframes octocat-wave {
19 | 0%, 100% {
20 | transform: rotate(0)
21 | }
22 | 20%, 60% {
23 | transform: rotate(-25deg)
24 | }
25 | 40%, 80% {
26 | transform: rotate(10deg)
27 | }
28 | }
29 |
30 | @media (max-width: 500px) {
31 | .github-corner:hover .octo-arm {
32 | animation: none
33 | }
34 |
35 | .github-corner .octo-arm {
36 | animation: octocat-wave 560ms ease-in-out
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/assets/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | outline: none;
3 | }
4 |
5 | body {
6 | display: flex;
7 | align-items: center;
8 | flex-direction: column;
9 | }
10 |
11 | .secret-key {
12 | display: flex;
13 | margin-top: 1em;
14 | min-height: 6em;
15 | align-items: center;
16 | flex-direction: column;
17 | justify-content: space-between;
18 | }
19 |
20 | .secret-key input {
21 | padding: 0;
22 | height: 2em;
23 | cursor: pointer;
24 | font-size: 12pt;
25 | border-radius: 1em;
26 | text-align: center;
27 | border: 1px solid gray;
28 | font-family: Courier, sans-serif;
29 | -webkit-tap-highlight-color: transparent;
30 | }
31 |
32 | .secret-key button {
33 | height: 2em;
34 | font-size: 12pt;
35 | cursor: pointer;
36 | padding: 0 1em;
37 | text-align: center;
38 | border-radius: 1em;
39 | border: 1px solid gray;
40 | background-color: transparent;
41 | }
42 |
--------------------------------------------------------------------------------
/app/views.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.views.generic import TemplateView
3 | from rest_framework.response import Response
4 | from rest_framework.viewsets import GenericViewSet, mixins
5 |
6 | from core.utils import generate_secret_key
7 |
8 |
9 | class IndexView(TemplateView):
10 | template_name = 'index.html'
11 |
12 | def get_context_data(self, **kwargs):
13 | context = super().get_context_data(**kwargs)
14 |
15 | analytics = bool(int(self.request.GET.get('analytics', 1)))
16 | if analytics:
17 | context['GOOGLE_ANALYTICS_ID'] = settings.GOOGLE_ANALYTICS_ID
18 | return context
19 |
20 |
21 | class SecretKeyView(GenericViewSet, mixins.ListModelMixin):
22 | def get_queryset(self):
23 | pass # pragma: no cover
24 |
25 | def list(self, request, *args, **kwargs):
26 | return Response({'secret_key': generate_secret_key()})
27 |
--------------------------------------------------------------------------------
/app/assets/toaster.css:
--------------------------------------------------------------------------------
1 | /* https://www.w3schools.com/howto/howto_js_snackbar.asp */
2 | .toaster {
3 | left: 50%;
4 | z-index: 1;
5 | color: #fff;
6 | bottom: 30px;
7 | padding: 16px;
8 | position: fixed;
9 | min-width: 250px;
10 | visibility: hidden;
11 | text-align: center;
12 | border-radius: 2px;
13 | margin-left: -125px;
14 | background-color: #333;
15 | }
16 |
17 | .toaster.show {
18 | visibility: visible;
19 | animation: fadeIn .5s, fadeOut .5s 2.5s;
20 | }
21 |
22 | @-webkit-keyframes fadeIn {
23 | from {
24 | bottom: 0;
25 | opacity: 0;
26 | }
27 | to {
28 | bottom: 30px;
29 | opacity: 1;
30 | }
31 | }
32 |
33 | @keyframes fadeIn {
34 | from {
35 | bottom: 0;
36 | opacity: 0;
37 | }
38 | to {
39 | bottom: 30px;
40 | opacity: 1;
41 | }
42 | }
43 |
44 | @-webkit-keyframes fadeOut {
45 | from {
46 | bottom: 30px;
47 | opacity: 1;
48 | }
49 | to {
50 | bottom: 0;
51 | opacity: 0;
52 | }
53 | }
54 |
55 | @keyframes fadeOut {
56 | from {
57 | bottom: 30px;
58 | opacity: 1;
59 | }
60 | to {
61 | bottom: 0;
62 | opacity: 0;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/core/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from random import SystemRandom
4 |
5 |
6 | def generate_secret_key():
7 | system_random = SystemRandom()
8 | allowed_chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
9 | return ''.join([system_random.choice(allowed_chars) for _ in range(50)])
10 |
11 |
12 | class EnvHelper:
13 | def __init__(self, env_file: str):
14 | self.env_file = env_file
15 |
16 | def read_env_file(self) -> None: # pragma: no cover
17 | try:
18 | with open(self.env_file) as f:
19 | content = f.read()
20 | except IOError:
21 | pass
22 | else:
23 | for line in content.splitlines():
24 | match = re.match(r'\A(?P[A-Za-z_0-9]+)=(?P.*)\Z',
25 | re.sub(r'( +)?#(.+)?', '', line))
26 | if match:
27 | os.environ.setdefault(*match.groupdict().values())
28 |
29 | def set_secret_key(self) -> None: # pragma: no cover
30 | secret_key = generate_secret_key()
31 | os.environ.setdefault('SECRET_KEY', secret_key)
32 |
33 | with open(self.env_file, 'ab') as env: # pragma: no cover
34 | env.write(b'SECRET_KEY=' + secret_key.encode())
35 |
--------------------------------------------------------------------------------
/app/assets/googleAnalytics.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | window.gtag = (function () {
4 | var gaId = _getParams('googleAnalytics')['id'];
5 | _loadGoogleAnalytics(gaId);
6 |
7 | window.dataLayer = window.dataLayer || [];
8 |
9 | function _gtag() {
10 | dataLayer.push(arguments);
11 | }
12 |
13 | _gtag('js', new Date());
14 | _gtag('config', gaId);
15 |
16 | return _gtag;
17 |
18 | function _loadGoogleAnalytics(googleAnalyticsId) {
19 | var script = document.createElement('script');
20 | script.src = '//www.googletagmanager.com/gtag/js?id=' + googleAnalyticsId;
21 | document.head.appendChild(script);
22 | }
23 |
24 | function _getParams(script_name) {
25 | var scripts = document.getElementsByTagName('script');
26 |
27 | for (var i = 0; i < scripts.length; i++) {
28 | var regexp = new RegExp('/' + script_name + '(\\.[a-zA-Z0-9]{12})?\\.js');
29 | var src = scripts[i].src;
30 |
31 | if (regexp.test(src)) {
32 | var queryParams = src.split('?')[1].split('&');
33 | return queryParams.reduce(_parseParams, {});
34 | }
35 | }
36 |
37 | return {};
38 | }
39 |
40 | function _parseParams(params, value) {
41 | var keyValue = value.split('=');
42 | var _params = JSON.parse(JSON.stringify(params)); // Make a copy of params
43 | _params[keyValue[0]] = keyValue[1];
44 | return _params
45 | }
46 | })();
47 |
--------------------------------------------------------------------------------
/app/assets/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | window.generateSecretKey = (function () {
4 | return function _generateSecretKeys() {
5 | _getSecretKeysRequest(_listener);
6 | };
7 |
8 | function _listener() {
9 | var _secretKeyWrapper = document.querySelector('.secret-key');
10 |
11 | if (this.status === 200) {
12 | /**
13 | * @type {{secretKey}}
14 | */
15 | var camelCaseResponse = _objectAttrsToCamelCase(JSON.parse(this.responseText));
16 | _secretKeyWrapper.querySelector('input').value = '\'' + camelCaseResponse.secretKey + '\'';
17 | }
18 | }
19 |
20 | function _getSecretKeysRequest(listener) {
21 | var xhr = new XMLHttpRequest();
22 | xhr.onload = listener;
23 | xhr.open('get', '/api/secret_key/', true);
24 | xhr.setRequestHeader('Content-Type', 'application/json');
25 | xhr.send();
26 | }
27 |
28 | function _objectAttrsToCamelCase(object) {
29 | var _object = {};
30 |
31 | for (var key in object) {
32 | if (object.hasOwnProperty(key)) {
33 | var camelCaseKey = key.replace(/_([a-z])/g, _innerToUpperCase);
34 | _object[camelCaseKey] = object[key];
35 | }
36 | }
37 |
38 | return _object;
39 | }
40 |
41 | function _innerToUpperCase(_, char) {
42 | return char.toUpperCase();
43 | }
44 | })();
45 |
46 | window.copySecretKey = (function () {
47 | return function _copySecretKey() {
48 | var input = document.querySelector('.secret-key input');
49 | input.select();
50 | if (document.queryCommandSupported('copy')) {
51 | document.execCommand('copy');
52 | window.showToaster('Secret key copied to your clipboard!');
53 | }
54 | }
55 | })();
56 |
--------------------------------------------------------------------------------
/.idea/djskgen.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% load static %}
5 |
6 |
7 | Django secret key generator
8 |
9 |
10 |
11 | {% if GOOGLE_ANALYTICS_ID %}
12 |
13 | {% endif %}
14 |
15 |
16 |
17 |
18 | Django Secret Key Generator
19 |
20 |
24 |
25 |
26 |
27 |
28 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/tests/unit.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | from django.test import SimpleTestCase, RequestFactory
4 | from django.conf import settings
5 |
6 | from app.views import IndexView, SecretKeyView
7 |
8 |
9 | class TestIndexView(SimpleTestCase):
10 | def test_that_it_returns_ok_status_on_get(self):
11 | request = RequestFactory().get('/')
12 |
13 | response = IndexView(request=request).get(request)
14 |
15 | self.assertEqual(response.status_code, 200)
16 |
17 | def test_when_analytics_is_set_then_render_with_analytics(self):
18 | ga_id = 'google-analytics-id'
19 | request = RequestFactory().get('/')
20 |
21 | with mock.patch.object(settings, 'GOOGLE_ANALYTICS_ID', ga_id):
22 | response = IndexView(request=request).get(request)
23 |
24 | response.render()
25 | self.assertIn(ga_id, response.rendered_content)
26 |
27 | def test_when_analytics_is_set_and_query_param_disables_it_then_render_without_analytics(self):
28 | ga_id = 'google-analytics-id'
29 | request = RequestFactory().get('/', {'analytics': '0'})
30 |
31 | with mock.patch.object(settings, 'GOOGLE_ANALYTICS_ID', ga_id):
32 | response = IndexView(request=request).get(request)
33 |
34 | response.render()
35 | self.assertNotIn(ga_id, response.rendered_content)
36 |
37 |
38 | class TestSecretKeysView(SimpleTestCase):
39 | def test_that_it_returns_secret_key_in_response(self):
40 | request = RequestFactory().post('/')
41 |
42 | response = SecretKeyView().list(request)
43 |
44 | self.assertIn('secret_key', response.data)
45 |
46 | def test_that_it_returns_a_secret_key_with_fifty_characters(self):
47 | request = RequestFactory().post('/')
48 |
49 | response = SecretKeyView().list(request)
50 |
51 | self.assertEqual(len(response.data['secret_key']), 50)
52 |
53 | def test_that_it_returns_a_secret_key_as_string(self):
54 | request = RequestFactory().post('/')
55 |
56 | response = SecretKeyView().list(request)
57 |
58 | self.assertIsInstance(response.data['secret_key'], str)
59 |
--------------------------------------------------------------------------------
/core/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from core.utils import EnvHelper
5 |
6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 |
8 | env_helper = EnvHelper(env_file=os.path.join(BASE_DIR, '.env'))
9 | env_helper.read_env_file()
10 |
11 | TEST = 'test' in sys.argv
12 |
13 | PRODUCTION = bool(int(os.getenv('PRODUCTION', 0)))
14 | DEBUG = bool(int(os.getenv('DEBUG', 0 if PRODUCTION else 1)))
15 |
16 | SECRET_KEY = os.getenv('SECRET_KEY')
17 |
18 | # If in production and SECRET_KEY is not set, let it fail
19 | if not SECRET_KEY and PRODUCTION:
20 | env_helper.set_secret_key() # pragma: no cover
21 |
22 | DJANGO_APPS = [
23 | 'django.contrib.auth',
24 | 'django.contrib.contenttypes',
25 | 'django.contrib.staticfiles',
26 | ]
27 |
28 | THIRD_PARTY_APPS = [
29 | 'rest_framework',
30 | ]
31 |
32 | LOCAL_APPS = [
33 | 'app',
34 | ]
35 |
36 | INSTALLED_APPS = LOCAL_APPS + THIRD_PARTY_APPS + DJANGO_APPS
37 |
38 | ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '*' if not PRODUCTION else '').split(',')
39 |
40 | TIME_ZONE = 'UTC'
41 |
42 | LANGUAGE_CODE = 'en-us'
43 |
44 | USE_I18N = True
45 | USE_L10N = True
46 | USE_TZ = False
47 |
48 | STATIC_URL = '/static/'
49 | STATIC_ROOT = os.path.join(BASE_DIR, 'static')
50 | STATICFILES_DIRS = [
51 | os.path.join(BASE_DIR, 'app', 'assets'),
52 | ]
53 |
54 | if not TEST: # pragma: no cover
55 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
56 |
57 | MIDDLEWARE = (
58 | 'django.middleware.security.SecurityMiddleware',
59 | 'whitenoise.middleware.WhiteNoiseMiddleware',
60 | 'django.contrib.sessions.middleware.SessionMiddleware',
61 | 'django.middleware.common.CommonMiddleware',
62 | 'django.middleware.csrf.CsrfViewMiddleware',
63 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
64 | 'django.contrib.messages.middleware.MessageMiddleware',
65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
66 | )
67 |
68 | ROOT_URLCONF = 'core.urls'
69 |
70 | WSGI_APPLICATION = 'core.wsgi.application'
71 |
72 | TEMPLATES = [
73 | {
74 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
75 | 'DIRS': [os.path.join(BASE_DIR, 'app', 'templates')],
76 | 'APP_DIRS': True,
77 | 'OPTIONS': {},
78 | },
79 | ]
80 |
81 | DATABASES = {
82 | 'default': {
83 | 'ENGINE': 'django.db.backends.sqlite3',
84 | 'NAME': ':memory:',
85 | }
86 | }
87 |
88 | GOOGLE_ANALYTICS_ID = os.getenv('GOOGLE_ANALYTICS_ID') if PRODUCTION and not TEST else None
89 |
--------------------------------------------------------------------------------