├── requirements.txt ├── .gitattributes ├── {{cookiecutter.github_repository}} ├── docs │ ├── img │ │ └── .gitkeep │ ├── index.md │ ├── releases.md │ ├── graphql │ │ ├── errors.md │ │ ├── 2-users.md │ │ ├── 0-overview.md │ │ ├── 1-auth.md │ │ └── errors_handling.md │ ├── api │ │ ├── 2-current-user.md │ │ ├── errors.md │ │ ├── 0-overview.md │ │ └── 1-auth.md │ └── backend │ │ ├── api_mixins.md │ │ └── coding_rules.md ├── runtime.txt ├── ansible.cfg ├── tests │ ├── __init__.py │ ├── graphql │ │ ├── __init__.py │ │ ├── test_current_user_api.py │ │ └── test_users_list_api.py │ ├── unit │ │ ├── __init__.py │ │ ├── test_example.py │ │ └── test_api_versioning.py │ ├── integration │ │ ├── __init__.py │ │ ├── test_site_pages.py │ │ └── test_current_user_api.py │ ├── utils.py │ └── factories.py ├── {{cookiecutter.main_module}} │ ├── locale │ │ └── .gitkeep │ ├── static │ │ ├── fonts │ │ │ └── .gitkeep │ │ ├── images │ │ │ ├── .gitkeep │ │ │ └── favicon.png │ │ └── js │ │ │ └── main.js │ ├── users │ │ ├── __init__.py │ │ ├── auth │ │ │ ├── __init__.py │ │ │ ├── services.py │ │ │ ├── utils.py │ │ │ ├── backends.py │ │ │ ├── tokens.py │ │ │ ├── serializers.py │ │ │ └── api.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0002_auto_20171024_1200.py │ │ ├── serializers.py │ │ ├── services.py │ │ ├── tests │ │ │ └── test_models.py │ │ ├── api.py │ │ ├── admin.py │ │ └── models.py │ ├── base │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── schemas.py │ │ │ ├── routers.py │ │ │ ├── mixins.py │ │ │ └── pagination.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── urls_extra.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ └── urls.py │ │ ├── context_processors.py │ │ ├── renderers.py │ │ ├── models.py │ │ └── views.py │ ├── graphql │ │ ├── __init__.py │ │ ├── users │ │ │ ├── resolvers.py │ │ │ ├── types.py │ │ │ └── schema.py │ │ ├── api.py │ │ ├── middleware.py │ │ ├── decorators.py │ │ └── utils.py │ ├── templates │ │ ├── robots.txt │ │ ├── pages │ │ │ ├── base.html │ │ │ ├── about.html │ │ │ └── home.html │ │ ├── rest_framework │ │ │ ├── login.html │ │ │ └── api.html │ │ ├── humans.txt │ │ ├── 403.html │ │ ├── 400.html │ │ ├── admin │ │ │ └── auth │ │ │ │ └── user │ │ │ │ └── add_form.html │ │ ├── 500.html │ │ ├── 404.html │ │ ├── email │ │ │ └── password_reset_mail.tpl │ │ ├── 403_csrf.html │ │ └── base.html │ ├── __init__.py │ ├── api_urls.py │ ├── celery.py │ └── urls.py ├── provisioner │ ├── roles │ │ ├── celery │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ ├── templates │ │ │ │ ├── celery.log.j2 │ │ │ │ ├── celerybeat.service.j2 │ │ │ │ └── celery.service.j2 │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── common │ │ │ ├── files │ │ │ │ ├── pydistutils.cfg │ │ │ │ ├── inputrc │ │ │ │ ├── pip.conf │ │ │ │ └── bashrc │ │ │ ├── templates │ │ │ │ └── locale.j2 │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── postgresql │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── templates │ │ │ │ └── pg_hba.conf.j2 │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── redis │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── project_data │ │ │ ├── templates │ │ │ │ ├── django.logrotate.j2 │ │ │ │ ├── env.env.j2 │ │ │ │ ├── django.asgi.ini.j2 │ │ │ │ ├── django.uwsgi.ini.j2 │ │ │ │ └── uwsgi-emperor-init.d.j2 │ │ │ ├── tasks │ │ │ │ ├── asgi-setup.yml │ │ │ │ ├── uwsgi-setup.yml │ │ │ │ └── main.yml │ │ │ └── defaults │ │ │ │ └── main.yml │ │ └── nginx │ │ │ ├── tasks │ │ │ ├── htpasswd.yml │ │ │ ├── letsencrypt.yml │ │ │ └── main.yml │ │ │ ├── handlers │ │ │ └── main.yml │ │ │ ├── templates │ │ │ ├── site.80.conf.j2 │ │ │ ├── site.443.conf.j2 │ │ │ └── nginx.conf.j2 │ │ │ └── defaults │ │ │ └── main.yml │ ├── ansible.cfg │ ├── vars.yml │ ├── hosts │ └── site.yml ├── compose │ ├── dev │ │ ├── postgres │ │ │ ├── maintenance │ │ │ │ ├── _sourced │ │ │ │ │ ├── constants.sh │ │ │ │ │ ├── yes_no.sh │ │ │ │ │ ├── countdown.sh │ │ │ │ │ └── messages.sh │ │ │ │ ├── backups │ │ │ │ ├── backup │ │ │ │ └── restore │ │ │ └── Dockerfile │ │ └── django │ │ │ ├── celery │ │ │ ├── beat │ │ │ │ └── start │ │ │ ├── worker │ │ │ │ └── start │ │ │ └── flower │ │ │ │ └── start │ │ │ ├── start │ │ │ ├── entrypoint │ │ │ └── Dockerfile │ ├── local │ │ ├── start │ │ ├── entrypoint │ │ └── Dockerfile │ └── fly │ │ └── django │ │ └── Dockerfile ├── uwsgi.ini ├── .gitattributes ├── settings │ ├── __init__.py │ ├── testing.py │ └── development.py ├── Procfile ├── bin │ ├── post_compile │ └── generate_db_schema.py ├── .github │ └── workflows │ │ ├── fly.yml │ │ └── main.yml ├── .editorconfig ├── asgi.py ├── manage.py ├── .pre-commit-config.yaml ├── setup.cfg ├── dev.yml ├── .gitignore ├── .env.sample ├── wsgi.py ├── CONTRIBUTING.md ├── Vagrantfile ├── local.yml ├── .envs │ ├── .local │ │ └── .env.sample │ └── .dev │ │ └── .env.sample └── mkdocs.yml ├── .github ├── django-init-logo.png ├── PULL_REQUEST_TEMPLATE └── workflows │ └── main.yml ├── hooks └── pre_gen_project.sh ├── renovate.json ├── cookiecutter-test-config.yaml ├── AUTHORS.md ├── .editorconfig ├── generate-history.sh ├── run_test.sh ├── cookiecutter.json ├── .gitignore └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/img/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.0 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/ansible.cfg: -------------------------------------------------------------------------------- 1 | provisioner/ansible.cfg -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/locale/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/graphql/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/static/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/static/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/__init__.py: -------------------------------------------------------------------------------- 1 | # user app 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # user auth 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /.github/django-init-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fueled/django-init/HEAD/.github/django-init-logo.png -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # base templatetags 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/static/js/main.js: -------------------------------------------------------------------------------- 1 | // Add your javascript code here. 2 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -n "==> Generating project files at ./{{ cookiecutter.github_repository }} " 4 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - { role: project_data } 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/files/pydistutils.cfg: -------------------------------------------------------------------------------- 1 | [easy_install] 2 | index-url=https://pypi.python.org/simple/ 3 | -------------------------------------------------------------------------------- /cookiecutter-test-config.yaml: -------------------------------------------------------------------------------- 1 | default_context: 2 | enable_whitenoise: "y" 3 | add_celery: "y" 4 | add_graphql: "y" 5 | add_asgi: "y" 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/unit/test_example.py: -------------------------------------------------------------------------------- 1 | def func(x): 2 | return x + 1 3 | 4 | 5 | def test_answer(): 6 | assert func(3) == 4 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/files/inputrc: -------------------------------------------------------------------------------- 1 | ## arrow up 2 | "\e[A":history-search-backward 3 | ## arrow down 4 | "\e[B":history-search-forward 5 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | 3 | User-agent: * 4 | Disallow: /api/ 5 | Disallow: /admin/ 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/files/pip.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | index-url=https://pypi.python.org/simple/ 3 | 4 | [install] 5 | trusted-host=pypi.python.org 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/postgresql/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart postgresql 3 | service: name=postgresql state=restarted 4 | become: true 5 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | allow_world_readable_tmpfiles = True 3 | 4 | # human-readable stdout/stderr results display 5 | stdout_callback = debug 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | processes = 4 5 | die-on-term = true 6 | module = wsgi:application 7 | memory-report = true 8 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 3 | * text=auto 4 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | celery -A {{ cookiecutter.main_module }} beat -l INFO 9 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | celery -A {{ cookiecutter.main_module }} worker -l INFO 9 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/pages/base.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends 'base.html' %} 2 | {% block body_classes %}{{ block.super}} pages {% endblock body_classes %}{% endraw %} 3 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/users/resolvers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | 4 | def get_all_users(info): 5 | return get_user_model().objects.all() 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/postgresql/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pg_hstore: False 3 | pg_gis: False 4 | pg_db: '{% raw %}{{ project_namespace }}{% endraw %}' 5 | pg_user: dev 6 | pg_password: password 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fueled/django-init/HEAD/{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/static/images/favicon.png -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/redis/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: reload redis 3 | service: name=redis-server state=reloaded 4 | 5 | - name: restart redis 6 | service: name=redis-server state=restarted 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/settings/__init__.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import sys 3 | 4 | if "test" in sys.argv: 5 | print("\033[1;91mNo django tests.\033[0m") 6 | print("Try: \033[1;33mpytest\033[0m") 7 | sys.exit(0) 8 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/context_processors.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.conf import settings 3 | 4 | 5 | def site_settings(context): 6 | return {"site_info": settings.SITE_INFO} 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | > Why was this change necessary? 2 | 3 | add your text here... 4 | 5 | > How does it address the problem? 6 | 7 | add your text here... 8 | 9 | > Are there any side effects? 10 | 11 | add your text here... 12 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/templates/celery.log.j2: -------------------------------------------------------------------------------- 1 | {% raw %}"{{celery_log_file}}" { 2 | copytruncate 3 | daily 4 | rotate 5 5 | compress 6 | delaycompress 7 | missingok 8 | notifempty 9 | }{% endraw %} 10 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/unit/test_api_versioning.py: -------------------------------------------------------------------------------- 1 | def test_api_default_and_allowed_versions(settings): 2 | assert settings.REST_FRAMEWORK["DEFAULT_VERSION"] == "1.0" 3 | assert settings.REST_FRAMEWORK["ALLOWED_VERSIONS"] == ["1.0"] 4 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/templates/django.logrotate.j2: -------------------------------------------------------------------------------- 1 | {% raw %}"{{ uwsgi_log_file }}" { 2 | copytruncate 3 | daily 4 | rotate 5 5 | compress 6 | delaycompress 7 | missingok 8 | notifempty 9 | }{% endraw %} 10 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = "{{ cookiecutter.version }}" 3 | {%- if cookiecutter.add_celery.lower() == 'y' %} 4 | 5 | from .celery import app as celery_app # noqa 6 | {%- endif %} 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/Procfile: -------------------------------------------------------------------------------- 1 | web: {% if cookiecutter.add_newrelic == 'y' %}newrelic-admin run-program {% endif %}uwsgi uwsgi.ini 2 | {%- if cookiecutter.add_celery == 'y' %} 3 | worker: celery -A {{ cookiecutter.main_module }} worker -l info --concurrency=2 -B 4 | {%- endif %} 5 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/templates/env.env.j2: -------------------------------------------------------------------------------- 1 | DATABASE_URL="{% if cookiecutter.add_postgis == 'y' %}postgis{% else %}postgres{% endif %}{%raw%}://{{ pg_user }}:{{ pg_password }}@localhost/{{ pg_db }}" 2 | DJANGO_SETTINGS_MODULE="{{ django_settings }}" 3 | {%endraw%} 4 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/handlers/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | - name: restart celery 3 | systemd: state=restarted daemon_reload=yes name=celery-{{ project_namespace }} 4 | 5 | - name: reload celery 6 | systemd: state=reloaded name=celery-{{ project_namespace }}{% endraw %} 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/redis/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update apt-cache if not already ran 3 | shell: apt update 4 | become: yes 5 | when: apt_updated is not defined 6 | 7 | - name: install redis server 8 | apt: 9 | pkg: redis-server 10 | state: present 11 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery \ 8 | -A {{ cookiecutter.main_module }} \ 9 | -b "${CELERY_BROKER_URL}" \ 10 | flower \ 11 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 12 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/serializers.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework import serializers 3 | 4 | from . import models 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = models.User 10 | fields = ["id", "first_name", "last_name", "email"] 11 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/local/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python manage.py migrate 9 | 10 | {%- if cookiecutter.add_asgi.lower() == "y" %} 11 | uvicorn config.asgi:application --host 0.0.0.0 --reload 12 | {%- else %} 13 | python manage.py runserver_plus 0.0.0.0:8000 14 | {%- endif %} 15 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/tasks/htpasswd.yml: -------------------------------------------------------------------------------- 1 | {%raw%}--- 2 | # htpasswd module needs passlib 3 | - name: Install passlib 4 | pip: name=passlib 5 | 6 | - name: Create htpasswd file 7 | htpasswd: 8 | path: '{{ htpasswd_file_path }}' 9 | name: '{{ nginx_docs_username }}' 10 | password: '{{ nginx_docs_password }}'{%endraw%} 11 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run migration while answering "yes" to any question prompted. 4 | yes "yes" | python manage.py migrate 5 | 6 | # Run production checks 7 | python manage.py check --deploy 8 | 9 | # Run collectstatic 10 | python manage.py collectstatic --noinput 11 | 12 | # used by heroku after slug compilation 13 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/rest_framework/login.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "rest_framework/login_base.html" %} 2 | 3 | {# Override this template in your own templates directory to customize #} 4 | {% block branding %}

{% endraw %} 5 | {{ cookiecutter.project_name }}{% raw %} API

{% endblock %}{% endraw %} -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.add_postgis.lower() == "y" %}FROM postgis/postgis:13-3.3{% else %}FROM postgres:13{% endif %} 2 | 3 | COPY ./compose/dev/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/index.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.project_name }} Documentation 2 | 3 | __Version:__ {{ cookiecutter.version }} 4 | 5 | {{ cookiecutter.project_description }} 6 | 7 | ## Quick Links 8 | 9 | - JIRA Board 10 | - [API Playground](/api-plaground) 11 | - [Django Admin](/admin) 12 | - [Database Schema](backend/database-schema.svg) 13 | 14 | {!releases.md!} 15 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/renderers.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework.renderers import JSONRenderer 3 | 4 | 5 | class {{ cookiecutter.main_module|replace('_', ' ')|replace('-', ' ')|title|replace(' ', '') }}ApiRenderer(JSONRenderer): 6 | media_type = "application/vnd.{{ cookiecutter.main_module|replace('_', '')|replace('-', '') }}+json" 7 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/bin/generate_db_schema.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import os 3 | from os.path import abspath, dirname 4 | 5 | parent_dir = dirname(dirname(abspath(__file__))) 6 | os.system( 7 | f"python {parent_dir}/manage.py graph_models -a > db.dot && dot -Tsvg db.dot -o db.svg" 8 | ) 9 | 10 | os.system(f"mv db.svg {parent_dir}/docs/backend/database-schema.svg") 11 | os.system("rm db.dot") 12 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends 'pages/base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans 'About' %} • {{ block.super }}{% endblock title %} 4 | {% block body_classes %}{{ block.super}} page-about {% endblock body_classes %} 5 | 6 | {% block content %} 7 |

{% trans 'About page coming soon!' %}

8 | {% endblock content %}{% endraw %} 9 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends 'pages/base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans 'Home' %} • {{ block.super }}{% endblock title %} 4 | {% block body_classes %}{{ block.super}} page-home {% endblock body_classes %} 5 | 6 | {% block content %} 7 |
{% trans 'Homepage coming soon!!' %}
8 | {% endblock content %}{% endraw %} 9 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/templates/locale.j2: -------------------------------------------------------------------------------- 1 | {% raw %}LANG="{{ lc_lang }}" 2 | {% if lc_all is defined %} 3 | LC_ALL="{{ lc_all }}" 4 | {% endif %} 5 | {% if lc_language is defined %} 6 | LANGUAGE="{{ lc_language }}" 7 | {% endif %} 8 | {% if lc_numeric is defined %} 9 | LC_NUMERIC="{{ lc_numeric }}" 10 | {% endif %} 11 | {% if lc_time is defined %} 12 | LC_TIME="{{ lc_time }}" 13 | {% endif %}{% endraw %} 14 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python /app/manage.py collectstatic --noinput 9 | {%- if cookiecutter.add_asgi.lower() == "y" %} 10 | gunicorn asgi --bind 0.0.0.0:8000 --chdir=/app -k uvicorn.workers.UvicornWorker 11 | {%- else %} 12 | gunicorn wsgi --bind 0.0.0.0:8000 --chdir=/app --access-logfile - --error-logfile - 13 | {%- endif %} 14 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/api.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | import graphene 3 | from graphene_django.debug import DjangoDebug 4 | 5 | from .users.schema import UserMutations, UserQueries 6 | 7 | 8 | class Query(UserQueries): 9 | debug = graphene.Field(DjangoDebug, name="_debug") 10 | 11 | 12 | class Mutation(UserMutations): 13 | pass 14 | 15 | 16 | schema = graphene.Schema(query=Query, mutation=Mutation) 17 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/settings/testing.py: -------------------------------------------------------------------------------- 1 | # Do this here, so that .env get loaded while running `pytest` from shell 2 | # Third Party Stuff 3 | from dotenv import find_dotenv, load_dotenv 4 | 5 | load_dotenv(find_dotenv()) 6 | 7 | from .development import * # noqa F405 8 | 9 | MEDIA_ROOT = "/tmp" 10 | 11 | SECRET_KEY = "top-scret!" 12 | 13 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 14 | INSTALLED_APPS += ("tests",) # noqa: F405 15 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/humans.txt: -------------------------------------------------------------------------------- 1 | /* http://humanstxt.org/ */ 2 | /* TEAM */ 3 | 4 | Your title: Your name. 5 | Site: email, link to a contact form, etc. 6 | Twitter: your Twitter username. 7 | Location: City, Country. 8 | 9 | /* THANKS */ 10 | 11 | Name: name or url 12 | 13 | /* SITE */ 14 | 15 | Last update: YYYY/MM/DD 16 | Standards: HTML5, CSS3,.. 17 | Components: jQuery,... 18 | Software: Software used for the development 19 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | name: Deploy app 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: superfly/flyctl-actions/setup-flyctl@master 13 | - run: flyctl deploy --dockerfile ./compose/fly/django/Dockerfile 14 | env: 15 | FLY_API_TOKEN: ${{ "{{" }} secrets.FLY_API_TOKEN {{ "}}" }} 16 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | # Use pyproject.toml for iSort related config. 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{py,rst,ini}] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.{html,css,scss,json,yml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | [Saurabh Kumar]: http://github.com/theskumar 2 | [Sanyam Khurana]: http://github.com/CuriousLearner 3 | [Mayank Jain]: http://github.com/jainmickey 4 | [Akash Mishra]: https://github.com/tucosaurus 5 | [Karambir Singh Nain]: https://github.com/karambir 6 | [Vikalp Jain]: https://github.com/vikalpj 7 | [Anuvrat Parashar]: https://github.com/bhanuvrat 8 | [Aniket Maithani]: https://github.com/aniketmaithani 9 | [Vikrant Pogula | Fueled]: https://github.com/vikrantpogula 10 | [Shiva Saxena]: https://github.com/GeekyShacklebolt 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs. 2 | # See: http://editorconfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/releases.md: -------------------------------------------------------------------------------- 1 | ## Upgrading your API 2 | 3 | [TODO] 4 | 5 | ---- 6 | 7 | ## Dev ({% now 'local', '%d/%m/%Y' %} -- [end_date]) 8 | 9 | **{% now 'local', '%d/%m/%Y' %}** 10 | 11 | - Add `GET /me` endpoint. 12 | - Add `PATCH /api/me` endpoint. 13 | - Add `POST /api/auth/password_reset_confirm` endpoint. 14 | - Add `POST /api/auth/password_change` endpoint. 15 | - Add `POST /api/auth/register` endpoint. 16 | - Add `POST /api/auth/login` endpoint. 17 | - Setup project with [django-init](https://github.com/Fueled/django-init). 18 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project_name: {{ cookiecutter.main_module }} 3 | project_repo_url: git@github.com:{{ cookiecutter.github_username }}/{{ cookiecutter.github_repository }}.git 4 | project_repo_remote: origin 5 | repo_version: master 6 | pg_gis: {{ 'True' if cookiecutter.add_postgis.lower() == 'y' else 'False' }} 7 | ansible_python_interpreter: /usr/bin/python3 8 | django_settings: settings.production 9 | django_requirements_file: requirements.txt 10 | python_version: 3.9 11 | postgresql_version: 13 12 | postgis_version: 3 13 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/403.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %}{% trans '403 Forbidden' %}{% endblock %} 4 | {% block head_extras %} 5 | 6 | {% endblock head_extras %} 7 | {% block body_classes %}{{ block.super }} page-error page-403 {% endblock body_classes %} 8 | 9 | {% block content %} 10 |

{% trans '403 Forbidden' %}

11 |

{% trans 'You are not authorized to access this page.' %}

12 | {% endblock content %}{% endraw %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/asgi.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import os 3 | 4 | # Third Party Stuff 5 | from django.core.asgi import get_asgi_application 6 | from dotenv import load_dotenv 7 | 8 | # Read .env file and set key/value inside it as environment variables 9 | # see: http://github.com/theskumar/python-dotenv 10 | load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) 11 | 12 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.production") 14 | 15 | application = get_asgi_application() 16 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/400.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %}{% trans 'Bad Request (400)' %}{% endblock %} 4 | {% block head_extras %} 5 | 6 | {% endblock head_extras %} 7 | {% block body_classes %}{{ block.super }} page-error page-400 {% endblock body_classes %} 8 | 9 | {% block content %} 10 |

{% trans 'Bad Request (400)' %}

11 |

{% trans 'The request cannot be fulfilled due to bad syntax.' %}

12 | {% endblock content %}{% endraw %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/admin/auth/user/add_form.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block form_top %} 5 | {% if not is_popup %} 6 |

{% trans "First, enter an email and password. Then, you'll be able to edit more user options." %}

7 | {% else %} 8 |

{% trans "Enter an email and password." %}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block after_field_sets %} 13 | 14 | {% endblock %} 15 | {% endraw %} 16 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/services.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.conf import settings 3 | from mail_templated import send_mail 4 | 5 | from .tokens import get_token_for_password_reset 6 | 7 | 8 | def send_password_reset_mail(user, template_name="email/password_reset_mail.tpl"): 9 | ctx = {"user": user, "token": get_token_for_password_reset(user)} 10 | 11 | return send_mail( 12 | from_email=settings.DEFAULT_FROM_EMAIL, 13 | recipient_list=[user.email], 14 | template_name=template_name, 15 | context=ctx, 16 | ) 17 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/graphql/errors.md: -------------------------------------------------------------------------------- 1 | # Errors 2 | 3 | ## Generic Errors 4 | 5 | 6 | For `/graphql` requests, the API will return the error in the following format: 7 | 8 | ```json 9 | { 10 | "errors": [ 11 | { 12 | "message": "You do not have permission to perform this action", 13 | "locations": [ 14 | { 15 | "line": 33, 16 | "column": 3 17 | } 18 | ], 19 | "path": [ 20 | "users" 21 | ] 22 | } 23 | ] 24 | } 25 | ``` 26 | 27 | __NOTE__: The copy for most of these error messages can be changed by backend developers. 28 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Standard Library 4 | import os 5 | import sys 6 | 7 | # Third Party Stuff 8 | from django.core.management import execute_from_command_line 9 | from dotenv import load_dotenv 10 | 11 | if __name__ == "__main__": 12 | 13 | # Read .env file and set key/value inside it as environement variables 14 | # see: http://github.com/theskumar/python-dotenv 15 | load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) 16 | 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.development") 18 | 19 | execute_from_command_line(sys.argv) 20 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "rest_framework/base.html" %} 2 | 3 | {# Override this template in your own templates directory to customize #} 4 | {% block title %}{% endraw %}{{ cookiecutter.project_name }}{% raw %} API{% endblock %} 5 | {% block branding %}{% endraw %}{{ cookiecutter.project_name }}{% raw %} API {% endraw %}{{ cookiecutter.version }}{% raw %}{% endblock %}{% endraw %} 6 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/api/schemas.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework.permissions import AllowAny 3 | from drf_yasg.views import get_schema_view 4 | from drf_yasg import openapi 5 | 6 | 7 | schema_view = get_schema_view( 8 | openapi.Info( 9 | title="{{ cookiecutter.project_name }} API", 10 | default_version="{{ cookiecutter.version }}", 11 | description="{{ cookiecutter.project_description }}", 12 | ), 13 | public=True, 14 | permission_classes=[AllowAny], 15 | ) 16 | 17 | swagger_schema_view = schema_view.with_ui("swagger", cache_timeout=0) 18 | -------------------------------------------------------------------------------- /generate-history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generates changelog day by day 4 | NEXT=$(date +%F) 5 | echo "History" 6 | echo "----------------------" 7 | echo "Note: This file is autogenerated with [generate-history.sh](generate-history.sh)" 8 | git log --no-merges --format="%cd" --date=short | sort -u -r | while read DATE ; do 9 | echo 10 | echo "### $DATE" 11 | GIT_PAGER=cat git log --no-merges --invert-grep --grep="release:" --format=" - %s ([%aN])" --since="$DATE 00:00" --until="$DATE 23:59" 12 | done 13 | 14 | echo "" 15 | echo "----------------------" 16 | echo "" 17 | if test -f "AUTHORS.md"; then 18 | cat AUTHORS.md 19 | fi 20 | 21 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/defaults/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | celery_user: www-data 3 | celery_group: www-data 4 | celery_log_dir: /var/log/celery 5 | celery_log_file: "{{ celery_log_dir }}/{{ project_namespace }}.log" 6 | celerybeat_log_file: "{{ celery_log_dir }}/{{ project_namespace }}.celerybeat.log" 7 | celery_log_level: "INFO" 8 | celery_runtime_dir: celery 9 | celerybeat_schedule_dir: "/var/run/{{ celery_runtime_dir }}" 10 | celerybeat_schedule_file: "{{ celerybeat_schedule_dir }}/schedule-{{ project_namespace }}.db" 11 | celery_pid_file: "/tmp/celery-{{ project_namespace }}.pid" 12 | celerybeat_pid_file: "/tmp/celerybeat-{{ project_namespace }}.pid" 13 | {% endraw %} 14 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/500.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %}{% trans "Server Error" %}{% endblock %} 4 | {% block head_extras %} 5 | 6 | {% endblock head_extras %} 7 | {% block body_classes %}{{ block.super }} page-error page-500 {% endblock body_classes %} 8 | 9 | {% block content %} 10 |

Ooops!!! 500

11 | 12 |

{% trans "Looks like something went wrong!" %}

13 | 14 |

{% trans "We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing." %}

15 | {% endblock content %}{% endraw %} 16 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/templates/celerybeat.service.j2: -------------------------------------------------------------------------------- 1 | {%raw%} 2 | [Unit] 3 | Description=Celery Beat {{ project_namespace }} Service 4 | After=network.target 5 | 6 | [Service] 7 | User={{ celery_user }} 8 | RuntimeDirectory={{ celery_runtime_dir }} 9 | Group={{ celery_group }} 10 | Restart=always 11 | WorkingDirectory={{ project_path }} 12 | ExecStart={{ venv_path }}/bin/celery -A {{ project_name }} beat -l {{ celery_log_level }} \ 13 | --logfile={{ celerybeat_log_file }} --pidfile={{ celerybeat_pid_file }} --schedule={{ celerybeat_schedule_file}} 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | Alias=celerybeat-{{ project_namespace }}.service 18 | {%endraw%} 19 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git://github.com/pre-commit/pre-commit-hooks 3 | rev: v1.4.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - id: check-case-conflict 8 | - id: check-merge-conflict 9 | - id: check-yaml 10 | args: ['--unsafe'] 11 | - id: detect-private-key 12 | - id: forbid-new-submodules 13 | - id: check-json 14 | - id: pretty-format-json 15 | - id: check-added-large-files 16 | - id: flake8 17 | 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.6.4 20 | hooks: 21 | - id: isort 22 | 23 | - repo: https://github.com/psf/black 24 | rev: stable 25 | hooks: 26 | - id: black 27 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/templates/django.asgi.ini.j2: -------------------------------------------------------------------------------- 1 | {% raw %}[Unit] 2 | Description={{ project_namespace }} gunicorn daemon 3 | After=network.target 4 | 5 | [Service] 6 | Environment=LC_ALL=en_US.utf-8 7 | Environment=LANG=en_US.utf-8 8 | StandardOutput=syslog 9 | StandardError=syslog 10 | SyslogIdentifier=gunicorn 11 | User={{ asgi_user }} 12 | Group={{ asgi_group }} 13 | WorkingDirectory={{ project_path }} 14 | ExecStart={{ venv_path }}/bin/gunicorn -w {{ asgi_workers }} --bind unix://{{ asgi_socket }} --access-logfile {{project_log_dir}}/asgi.log --capture-output --error-logfile {{project_log_dir}}/asgi-errors.log -k uvicorn.workers.UvicornWorker asgi:application 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | {% endraw %} 19 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart nginx 3 | debug: msg="checking config first" 4 | changed_when: True 5 | notify: 6 | - check nginx configuration 7 | - restart nginx - after config check 8 | 9 | - name: reload nginx 10 | debug: msg="checking config first" 11 | changed_when: True 12 | notify: 13 | - check nginx configuration 14 | - reload nginx - after config check 15 | 16 | - name: check nginx configuration 17 | command: "nginx -t" 18 | register: result 19 | changed_when: "result.rc != 0" 20 | check_mode: no 21 | 22 | - name: restart nginx - after config check 23 | service: name=nginx state=restarted 24 | 25 | - name: reload nginx - after config check 26 | service: name=nginx state=reloaded 27 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/api/2-current-user.md: -------------------------------------------------------------------------------- 1 | # Current User 2 | 3 | ## Get profile 4 | ``` 5 | GET /api/me (requires authentication) 6 | ``` 7 | 8 | __Response__ 9 | 10 | ```json 11 | { 12 | "id": "629b1e03-53f0-43ef-9a03-17164cf782ac", 13 | "first_name": "John", 14 | "last_name": "Hawley", 15 | "email": "john@localhost.com" 16 | } 17 | ``` 18 | 19 | ## Update profile 20 | ``` 21 | PATCH /api/me (requires authentication) 22 | ``` 23 | 24 | __Example__ 25 | ```json 26 | { 27 | "first_name": "James", 28 | "last_name": "Warner" 29 | } 30 | ``` 31 | 32 | __Response__ 33 | 34 | ```json 35 | { 36 | "id": "629b1e03-53f0-43ef-9a03-17164cf782ac", 37 | "first_name": "James", 38 | "last_name": "Warner", 39 | "email": "john@localhost.com", 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/utils.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.db.models import signals 3 | 4 | 5 | def signals_switch(): 6 | pre_save = signals.pre_save.receivers 7 | post_save = signals.post_save.receivers 8 | 9 | def disconnect(): 10 | signals.pre_save.receivers = [] 11 | signals.post_save.receivers = [] 12 | 13 | def reconnect(): 14 | signals.pre_save.receivers = pre_save 15 | signals.post_save.receivers = post_save 16 | 17 | return disconnect, reconnect 18 | 19 | 20 | disconnect_signals, reconnect_signals = signals_switch() 21 | 22 | 23 | def get_dict_from_list_where(my_list, key, value): 24 | """see: http://stackoverflow.com/a/7079297/782901""" 25 | return next((item for item in my_list if item[key] == value), None) 26 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/migrations/0002_auto_20171024_1200.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-10-24 12:00 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.citext 6 | from django.contrib.postgres.operations import CITextExtension 7 | from django.db import migrations 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [("users", "0001_initial")] 13 | 14 | operations = [ 15 | CITextExtension(), 16 | migrations.AlterField( 17 | model_name="user", 18 | name="email", 19 | field=django.contrib.postgres.fields.citext.CIEmailField( 20 | db_index=True, max_length=254, unique=True, verbose_name="email address" 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/defaults/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | # roles/common/defaults/main.yml 3 | common_timezone: {% endraw %}{{ cookiecutter.timezone }}{% raw %} 4 | 5 | lc_lang: "en_US.UTF-8" 6 | lc_all: "en_US.utf8" 7 | lc_ctype: "en_US.utf8" 8 | lc_collate: "en_US.utf8" 9 | 10 | base_ubuntu: 11 | common: 12 | apt_packages: 13 | - build-essential 14 | - git 15 | - htop 16 | - curl 17 | - python-dev 18 | - python3.9 19 | - python3.9-dev 20 | - python-setuptools 21 | - python3-venv 22 | - python3-pip 23 | - sysstat 24 | - vim 25 | - fail2ban 26 | - libjpeg-dev 27 | - libtiff5-dev 28 | - zlib1g-dev 29 | - libfreetype6-dev 30 | - liblcms2-dev 31 | - postgresql-client 32 | - libpq-dev 33 | - libreadline-dev 34 | - ca-certificates 35 | {% endraw %} 36 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/factories.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helpers to create dynamic model instances for testing purposes. 3 | 4 | Usages: 5 | >>> from tests import factories as f 6 | >>> 7 | >>> user = f.create_user(first_name="Robert", last_name="Downey") # creates single instance of user 8 | >>> users = f.create_user(n=5, is_active=False) # creates 5 instances of user 9 | 10 | There is a bit of magic going on behind the scenes with `G` method from https://django-dynamic-fixture.readthedocs.io/ 11 | """ 12 | 13 | # Third Party Stuff 14 | from django.apps import apps 15 | from django.conf import settings 16 | from django_dynamic_fixture import G 17 | 18 | 19 | def create_user(**kwargs): 20 | """Create an user along with their dependencies.""" 21 | User = apps.get_model(settings.AUTH_USER_MODEL) 22 | user = G(User, **kwargs) 23 | user.set_password(kwargs.get("password", "test")) 24 | user.save() 25 | return user 26 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/api_urls.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework.routers import DefaultRouter 3 | 4 | # {{ cookiecutter.main_module }} Stuff 5 | from {{cookiecutter.main_module}}.base.api.routers import SingletonRouter 6 | from {{cookiecutter.main_module}}.users.api import CurrentUserViewSet 7 | from {{cookiecutter.main_module}}.users.auth.api import AuthViewSet 8 | 9 | default_router = DefaultRouter(trailing_slash=False) 10 | singleton_router = SingletonRouter(trailing_slash=False) 11 | 12 | # Register all the django rest framework viewsets below. 13 | default_router.register("auth", AuthViewSet, basename="auth") 14 | singleton_router.register("me", CurrentUserViewSet, basename="me") 15 | 16 | # Combine urls from both default and singleton routers and expose as 17 | # 'urlpatterns' which django can pick up from this module. 18 | urlpatterns = default_router.urls + singleton_router.urls 19 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/integration/test_site_pages.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | import pytest 3 | from django.urls import reverse 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_root_txt_files(client): 9 | files = ["robots.txt", "humans.txt"] 10 | for filename in files: 11 | url = reverse("root-txt-files", kwargs={"filename": filename}) 12 | response = client.get(url) 13 | assert response.status_code == 200 14 | assert response["Content-Type"] == "text/plain" 15 | 16 | 17 | def test_landing_pages(client): 18 | # Test that these urls are rendered properly and doesn't required authorization 19 | urls = ["/about/", "/"] 20 | for url in urls: 21 | response = client.get(url) 22 | assert response.status_code == 200 23 | assert response["Content-Type"] == "text/html; charset=utf-8" 24 | assert "" in response.content.decode("utf-8") 25 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/templatetags/urls_extra.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django import template 3 | 4 | # {{ cookiecutter.main_module }} Stuff 5 | from {{cookiecutter.main_module}}.base.utils.urls import resolve_frontend_url as _resolve_frontend_url 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.simple_tag 11 | def resolve_frontend_url(name, **kwargs): 12 | """Templatetag to render absolute urls for frontend app via it's name. 13 | 14 | It makes use of the mapping in FRONTEND_URLS in settings, combined with 15 | FRONTEND_SITE_SCHEME and FRONTEND_SITE_DOMAIN and configuration. 16 | 17 | Usages: 18 | ``` 19 | {% raw %}{% load resolve_frontend_url from urls_extra %} 20 | 21 | {% resolve_frontend_url "home" %} 22 | {% resolve_frontend_url "password-reset" token=token %}{% endraw %} 23 | ``` 24 | """ 25 | return _resolve_frontend_url(name, **kwargs) 26 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/middleware.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.main_module}}.users.auth.tokens import get_user_for_token 2 | from {{cookiecutter.main_module}}.users.auth.utils import get_http_authorization 3 | 4 | 5 | def _authenticate(request): 6 | is_anonymous = not hasattr(request, "user") or request.user.is_anonymous 7 | return is_anonymous and get_http_authorization(request) is not None 8 | 9 | 10 | class JSONWebTokenMiddleware: 11 | def __init__(self): 12 | self.cached_allow_any = set() 13 | 14 | def resolve(self, next, root, info, **kwargs): 15 | context = info.context 16 | 17 | if _authenticate(context): 18 | 19 | token = get_http_authorization(context) 20 | user = get_user_for_token(token, "authentication") 21 | if user is not None: 22 | context.user = user 23 | 24 | return next(root, info, **kwargs) 25 | -------------------------------------------------------------------------------- /run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup for returning a non-zero exit code if any of the command fails. 4 | err=0 5 | trap 'err=1' ERR 6 | 7 | # Clean 8 | if [ ! $CI ]; then 9 | if psql -lqt | cut -d \| -f 1 | grep -qw hello_world ; then 10 | read -p "Database 'hello_world' required for running the tests already exist. Do you want to delete it (y)?" yn 11 | if echo "$yn" | grep -iq "^n" ;then 12 | exit 13 | else 14 | dropdb hello_world 15 | fi 16 | fi 17 | fi 18 | 19 | rm -rf hello-world-backend/; 20 | 21 | # Generate new code, (it also creates db, migrate and install dependencies) 22 | yes 'y' | cookiecutter . --no-input --config-file cookiecutter-test-config.yaml 23 | 24 | # Run the tests present inside generate project 25 | cd hello-world-backend; 26 | make test ARGS="--cov" 27 | 28 | # Cleanup 29 | test ! $CI && dropdb hello_world 30 | 31 | test $err = 0 # Return non-zero if any command failed 32 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/404.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load i18n %} 3 | {% get_current_language as LANGUAGE_CODE %} 4 | 5 | {% block title %}{% trans 'Page Not found' %}{% endblock %} 6 | {% block head_extras %} 7 | 8 | {% endblock head_extras %} 9 | {% block body_classes %}{{ block.super }} page-error page-404 {% endblock body_classes %} 10 | 11 | {% block content %} 12 |

{% trans 'Not Found' %}

13 |

{% blocktrans %}The requested URL {{ request_path }} was not found on this server.{% endblocktrans %}

14 | 15 | 16 | 20 | 21 | {% endblock content %}{% endraw %} 22 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/services.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.contrib.auth import authenticate, get_user_model 3 | 4 | # {{ cookiecutter.main_module }} Stuff 5 | from {{cookiecutter.main_module}}.base import exceptions as exc 6 | 7 | 8 | def get_and_authenticate_user(email, password): 9 | user = authenticate(username=email, password=password) 10 | if user is None: 11 | raise exc.WrongArguments("Invalid username/password. Please try again!") 12 | 13 | return user 14 | 15 | 16 | def create_user_account(email, password, first_name="", last_name=""): 17 | user = get_user_model().objects.create_user( 18 | email=email, password=password, first_name=first_name, last_name=last_name 19 | ) 20 | return user 21 | 22 | 23 | def get_user_by_email(email: str): 24 | return get_user_model().objects.filter(email__iexact=email).first() 25 | 26 | 27 | def get_active_user_by_id(user_id): 28 | return get_user_model().objects.filter(id=user_id, is_active=True).first() 29 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.test import TestCase 3 | 4 | # {{ cookiecutter.main_module }} Stuff 5 | from {{cookiecutter.main_module}}.users.models import User 6 | 7 | 8 | class UserModelTestCase(TestCase): 9 | def test_create_user(self): 10 | u = User.objects.create_user( 11 | email="f@F.com", password="abc", first_name="F", last_name="B" 12 | ) 13 | assert u.is_active is True 14 | assert u.is_staff is False 15 | assert u.is_superuser is False 16 | assert u.email == "f@f.com" 17 | assert u.get_full_name() == "F B" 18 | assert u.get_short_name() == "F" 19 | assert str(u) == str(u.id) 20 | 21 | def test_create_super_user(self): 22 | u = User.objects.create_superuser(email="f@f.com", password="abc") 23 | assert u.is_active is True 24 | assert u.is_staff is True 25 | assert u.is_superuser is True 26 | assert str(u) == str(u.id) 27 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/templates/celery.service.j2: -------------------------------------------------------------------------------- 1 | {%raw%} 2 | [Unit] 3 | Description=Celery {{ project_namespace }} Service 4 | After=network.target 5 | 6 | [Service] 7 | User={{ celery_user }} 8 | Group={{ celery_group }} 9 | Type=forking 10 | Restart=always 11 | WorkingDirectory={{ project_path }} 12 | ExecStart={{ venv_path }}/bin/celery -A {{ project_name }} multi start worker-{{ project_namespace }} -l {{ celery_log_level }} \ 13 | --logfile={{ celery_log_file }} --pidfile={{ celery_pid_file }} --schedule={{ celerybeat_schedule_file}} 14 | ExecStop={{ venv_path }}/bin/celery multi stopwait worker-{{ project_namespace }} --pidfile={{ celery_pid_file }} 15 | ExecReload={{ venv_path }}/bin/celery -A {{ project_name }} multi restart worker-{{ project_namespace }} -l {{ celery_log_level }} \ 16 | --logfile={{ celery_log_file }} --pidfile={{ celery_pid_file }} --schedule={{ celerybeat_schedule_file}} 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | Alias=celery-{{ project_namespace }}.service 21 | {%endraw%} 22 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/api.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework import viewsets 3 | 4 | # {{ cookiecutter.main_module }} Stuff 5 | from {{cookiecutter.main_module}}.base import response 6 | 7 | from . import models, serializers 8 | 9 | 10 | class CurrentUserViewSet(viewsets.GenericViewSet): 11 | serializer_class = serializers.UserSerializer 12 | queryset = models.User.objects.filter(is_active=True) 13 | 14 | def get_object(self): 15 | return self.request.user 16 | 17 | def list(self, request): 18 | """Get logged in user profile""" 19 | serializer = self.get_serializer(self.get_object()) 20 | return response.Ok(serializer.data) 21 | 22 | def partial_update(self, request): 23 | """Update logged in user profile""" 24 | instance = self.get_object() 25 | serializer = self.get_serializer(instance, data=request.data, partial=True) 26 | serializer.is_valid(raise_exception=True) 27 | serializer.save() 28 | return response.Ok(serializer.data) 29 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/tasks/asgi-setup.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | - name: apt_get install asgi packages 3 | apt: pkg={{ item }} state=present 4 | with_items: 5 | - uuid-dev 6 | - libcap-dev 7 | - libpcre3-dev 8 | tags: ["configure"] 9 | 10 | - name: make sure project directory is owned by asgi group 11 | file: path={{ project_path }} state=directory owner={{user}} group={{asgi_group}} recurse=yes 12 | tags: ["configure"] 13 | 14 | - name: copy django-asgi logrotate 15 | template: src=django.logrotate.j2 16 | dest=/etc/logrotate.d/asgi-{{ deploy_environment}}-{{project_name}}-django 17 | mode=644 18 | tags: ["configure"] 19 | 20 | - name: make sure log directory exists 21 | file: path={{ project_log_dir }} state=directory owner={{asgi_user}} group={{asgi_group}} mode=751 recurse=yes 22 | tags: ["configure"] 23 | 24 | - name: copy Django asgi service to systemd 25 | template: src=django.asgi.ini.j2 26 | dest=/etc/systemd/system/asgi-{{project_namespace}}.service 27 | mode=644 28 | tags: ["deploy"] 29 | {% endraw %} 30 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Hello World" 3 | , "project_description": "Add a short project description here." 4 | , "main_module": "{{ cookiecutter.project_name|lower|replace(' ', '_')|replace('-', '_') }}" 5 | , "github_username": "Fueled" 6 | , "github_repository": "{{ cookiecutter.main_module|replace('_', '-') }}-backend" 7 | , "timezone": "UTC" 8 | , "default_from_email": "{{ cookiecutter.project_name }} " 9 | , "version": "0.0.0" 10 | , "add_newrelic" : "y" 11 | , "add_postgis": "n" 12 | , "add_heroku": "n" 13 | , "add_fly": "n" 14 | , "enable_whitenoise": "n" 15 | , "add_ansible": "y" 16 | , "letsencrypt": "y" 17 | , "letsencrypt_email": "backend+{{ cookiecutter.main_module|replace('_', '-') }}@fueled.com" 18 | , "have_password_protected_docs": "n" 19 | , "add_sentry": "y" 20 | , "add_django_auth_wall": "y" 21 | , "add_celery": "n" 22 | , "add_graphql": "n" 23 | , "add_asgi": "n" 24 | , "add_pre_commit": "y" 25 | , "add_docker": "y" 26 | , "pagination": ["LimitOffsetPagination", "CursorPagination"] 27 | } 28 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/local/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | # N.B. If only .env files supported variable expansion... 9 | export CELERY_BROKER_URL="${REDIS_URL}" 10 | 11 | if [ -z "${POSTGRES_USER}" ]; then 12 | base_postgres_image_default_user='postgres' 13 | export POSTGRES_USER="${base_postgres_image_default_user}" 14 | fi 15 | export DATABASE_URL="{% if cookiecutter.add_postgis.lower() == 'y' %}postgis{% else %}postgres{% endif %}://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 16 | 17 | postgres_ready() { 18 | python << END 19 | import sys 20 | import psycopg2 21 | try: 22 | psycopg2.connect( 23 | dbname="${POSTGRES_DB}", 24 | user="${POSTGRES_USER}", 25 | password="${POSTGRES_PASSWORD}", 26 | host="${POSTGRES_HOST}", 27 | port="${POSTGRES_PORT}", 28 | ) 29 | except psycopg2.OperationalError: 30 | sys.exit(-1) 31 | sys.exit(0) 32 | END 33 | } 34 | until postgres_ready; do 35 | >&2 echo 'Waiting for PostgreSQL to become available...' 36 | sleep 1 37 | done 38 | >&2 echo 'PostgreSQL is available' 39 | 40 | exec "$@" 41 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/django/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | # N.B. If only .env files supported variable expansion... 9 | export CELERY_BROKER_URL="${REDIS_URL}" 10 | 11 | if [ -z "${POSTGRES_USER}" ]; then 12 | base_postgres_image_default_user='postgres' 13 | export POSTGRES_USER="${base_postgres_image_default_user}" 14 | fi 15 | export DATABASE_URL="{% if cookiecutter.add_postgis.lower() == 'y' %}postgis{% else %}postgres{% endif %}://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 16 | 17 | postgres_ready() { 18 | python << END 19 | import sys 20 | 21 | import psycopg2 22 | 23 | try: 24 | psycopg2.connect( 25 | dbname="${POSTGRES_DB}", 26 | user="${POSTGRES_USER}", 27 | password="${POSTGRES_PASSWORD}", 28 | host="${POSTGRES_HOST}", 29 | port="${POSTGRES_PORT}", 30 | ) 31 | except psycopg2.OperationalError: 32 | sys.exit(-1) 33 | sys.exit(0) 34 | 35 | END 36 | } 37 | until postgres_ready; do 38 | >&2 echo 'Waiting for PostgreSQL to become available...' 39 | sleep 1 40 | done 41 | >&2 echo 'PostgreSQL is available' 42 | 43 | exec "$@" 44 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Run tests for Django-init 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | 9 | services: 10 | postgres: 11 | image: postgres:13 12 | env: 13 | POSTGRES_USER: postgres 14 | POSTGRES_PASSWORD: postgres 15 | POSTGRES_DB: hello_world 16 | ports: 17 | - 5432:5432 18 | # needed because the postgres container does not provide a healthcheck 19 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 20 | 21 | env: 22 | DATABASE_URL: postgres://postgres:postgres@localhost:5432/hello_world 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Python 3.9 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: "3.9" 30 | cache: "pip" 31 | - name: Install cookiecutter 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip3 install cookiecutter==1.7.3 35 | - name: Install poetry 36 | run: | 37 | pip3 install poetry==1.2.0 38 | - name: Run tests 39 | run: | 40 | ./run_test.sh 41 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 21 | 22 | 23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 25 | exit 1 26 | fi 27 | 28 | export PGHOST="${POSTGRES_HOST}" 29 | export PGPORT="${POSTGRES_PORT}" 30 | export PGUSER="${POSTGRES_USER}" 31 | export PGPASSWORD="${POSTGRES_PASSWORD}" 32 | export PGDATABASE="${POSTGRES_DB}" 33 | 34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 36 | 37 | 38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 39 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/users/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | from graphene_django.types import DjangoObjectType 4 | 5 | from {{cookiecutter.main_module}}.graphql.utils import CountableConnectionBase 6 | from {{cookiecutter.main_module}}.users.auth import tokens 7 | from {{cookiecutter.main_module}}.users.models import User 8 | 9 | 10 | class CurrentUser(DjangoObjectType): 11 | class Meta: 12 | model = User 13 | fields = ["id", "first_name", "last_name", "email"] 14 | interfaces = [relay.Node] 15 | 16 | 17 | class AuthenticatedUser(DjangoObjectType): 18 | auth_token = graphene.String() 19 | 20 | class Meta: 21 | model = User 22 | fields = ["id", "first_name", "last_name", "email"] 23 | interfaces = [relay.Node] 24 | 25 | def resolve_auth_token(self, info): 26 | return tokens.get_token_for_user(self, "authentication") 27 | 28 | 29 | class UserConnection(DjangoObjectType): 30 | class Meta: 31 | model = User 32 | fields = ["id", "first_name", "last_name"] 33 | filter_fields = {"id": ["exact"]} 34 | interfaces = (relay.Node,) 35 | connection_class = CountableConnectionBase 36 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/email/password_reset_mail.tpl: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "mail_templated/base.tpl" %} 2 | {% load i18n %} 3 | {% load resolve_frontend_url from urls_extra %} 4 | 5 | {# ======== Subject of email #} 6 | {% block subject %}Reset your Password!{% endblock %} 7 | 8 | {% block body %} 9 | {# ======== plain text version of email body #} 10 | {% blocktrans %}You're receiving this email because you requested a password reset 11 | for your user account.{% endblocktrans %} 12 | 13 | {% trans "Please go to the following page and choose a new password:" %} 14 | 15 | {% resolve_frontend_url "password-confirm" token=token %} 16 | 17 | {% trans "Thanks for using our site!" %} 18 | {% endblock body %} 19 | 20 | 21 | {% block html %} 22 | {# ======== html version of email body #} 23 |

{% blocktrans %}You're receiving this email because you requested a password reset 24 | for your user account.{% endblocktrans %}

25 | 26 |

{% trans "Please go to the following page and choose a new password:" %} 27 | {% trans "Reset Password" %} 28 |

29 | 30 |

{% trans "Thanks for using our site!" %}

31 | {% endblock html %}{% endraw %} 32 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from {{cookiecutter.main_module}}.base import exceptions 3 | 4 | try: 5 | from graphql.execution.execute import GraphQLResolveInfo 6 | except ImportError: 7 | from graphql.execution.base import ResolveInfo as GraphQLResolveInfo # type: ignore 8 | 9 | 10 | def context(f): 11 | def decorator(func): 12 | def wrapper(*args, **kwargs): 13 | info = next(arg for arg in args if isinstance(arg, GraphQLResolveInfo)) 14 | return func(info.context, *args, **kwargs) 15 | 16 | return wrapper 17 | 18 | return decorator 19 | 20 | 21 | def user_passes_test(test_func, exc=exceptions.PermissionDenied): 22 | def decorator(f): 23 | @wraps(f) 24 | @context(f) 25 | def wrapper(context, *args, **kwargs): 26 | if test_func(context.user): 27 | return f(*args, **kwargs) 28 | raise exc 29 | 30 | return wrapper 31 | 32 | return decorator 33 | 34 | 35 | login_required = user_passes_test(lambda u: u.is_authenticated) 36 | staff_member_required = user_passes_test(lambda u: u.is_staff) 37 | superuser_required = user_passes_test(lambda u: u.is_superuser) 38 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/defaults/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | pg_hstore: False 3 | pg_db: "{{ project_namespace }}" 4 | pg_user: dev 5 | pg_password: password 6 | django_requirements_file: requirements.txt 7 | {% endraw %} 8 | 9 | {%- if cookiecutter.add_asgi.lower() == 'y' %} 10 | # asgi related variables 11 | asgi_user: www-data 12 | asgi_group: www-data 13 | asgi_workers: 2 14 | {% raw %} 15 | asgi_socket: /tmp/django-{{ domain_name }}-asgi.sock 16 | {% endraw %} 17 | asgi_user: www-data 18 | asgi_group: www-data 19 | asgi_workers: 2 20 | {% else %} 21 | # uwsgi related variables 22 | uwsgi_user: www-data 23 | uwsgi_group: www-data 24 | uwsgi_min_workers: 2 25 | uwsgi_initial_workers: 4 26 | uwsgi_max_workers: 10 27 | uwsgi_workers_spawn_rate: 2 28 | uwsgi_worker_connections: 5000 29 | uwsgi_debug: False 30 | uwsgi_timeout: 30 31 | uwsgi_keepalive: 2 32 | uwsgi_loglevel: info 33 | uwsgi_conf_path: /etc/uwsgi-emperor/vassals 34 | uwsgi_emperor_pid_file: /run/uwsgi-emperor.pid 35 | {% raw %} 36 | uwsgi_socket: "/tmp/uwsgi-{{ project_namespace }}.sock" 37 | uwsgi_pid_file: "/tmp/uwsgi-{{ project_namespace }}.pid" 38 | 39 | uwsgi_log_dir: /var/log/uwsgi 40 | uwsgi_log_file: "{{ uwsgi_log_dir }}/{{ project_namespace }}.log" 41 | {% endraw %} 42 | {% endif %} 43 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/graphql/test_current_user_api.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import json 3 | 4 | # Third Party Stuff 5 | import pytest 6 | 7 | from tests import factories as f 8 | 9 | pytestmark = pytest.mark.django_db 10 | 11 | 12 | def test_get_current_user_api(client): 13 | graphql_query = """ 14 | query{ 15 | me{ 16 | email, 17 | firstName, 18 | lastName 19 | } 20 | } 21 | """ 22 | 23 | user = f.create_user(email="test@example.com") 24 | 25 | response = client.post_graphql(graphql_query) 26 | assert response.status_code == 200 27 | 28 | # should return an error without auth 29 | response_data = json.loads(response.content) 30 | assert "errors" in response_data.keys() 31 | 32 | client.login(user) 33 | response = client.post_graphql(graphql_query) 34 | assert response.status_code == 200 35 | 36 | # should return user 37 | response_data = json.loads(response.content) 38 | expected_keys = ["email", "firstName", "lastName"] 39 | assert "errors" not in response_data.keys() 40 | assert set(expected_keys).issubset(response_data["data"]["me"].keys()) 41 | assert response_data["data"]["me"]["email"] == "test@example.com" 42 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/postgresql/templates/pg_hba.conf.j2: -------------------------------------------------------------------------------- 1 | {% raw %}# PostgreSQL Client Authentication Configuration File 2 | # 3 | # Generated by Ansible 4 | # All handmade changes can be rewited! 5 | # 6 | # Allow any user on the local system to connect to any database with 7 | # any database user name using Unix-domain sockets (the default for local 8 | # connections). 9 | # 10 | # TYPE DATABASE USER ADDRESS METHOD 11 | 12 | # Default: 13 | {% for connection in pg_cfg_pg_hba_default %} 14 | # {{ connection.comment }} 15 | {{ connection.type }} {{ connection.database }} {{ connection.user }} {{ connection.address }} {{ connection.method }} 16 | {% endfor %} 17 | 18 | # Passwored hosts 19 | {% for host in pg_cfg_pg_hba_passwd_hosts %} 20 | # {{ connection.comment }} 21 | host all all {{ host }} password 22 | {% endfor %} 23 | 24 | # Trusted hosts 25 | {% for host in pg_cfg_pg_hba_trust_hosts %} 26 | # {{ connection.comment }} 27 | host all all {{ host }} trust 28 | {% endfor %} 29 | 30 | # User custom 31 | {% for connection in pg_cfg_pg_hba_custom %} 32 | # {{ connection.comment }} 33 | {{ connection.type }} {{ connection.database }} {{ connection.user }} {{ connection.address }} {{ connection.method }} 34 | {% endfor %}{% endraw %} 35 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Standard Library 3 | import re 4 | from uuid import UUID 5 | 6 | # Third Party Stuff 7 | from django.utils.encoding import force_bytes, force_str 8 | from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode 9 | 10 | 11 | def encode_uuid_to_base64(uuid_) -> str: 12 | """Returns a urlsafe based64 encoded representation of a UUID object or UUID like string.""" 13 | return urlsafe_base64_encode(force_bytes(uuid_)) 14 | 15 | 16 | def decode_uuid_from_base64(uuid_value: str): 17 | """Given a base64 encoded string, try to decode it to a valid UUID object. 18 | 19 | Returns a valid UUID value or None 20 | """ 21 | try: 22 | return UUID(force_str(urlsafe_base64_decode(uuid_value))) 23 | except (ValueError, OverflowError, TypeError): 24 | return None 25 | 26 | 27 | def get_http_authorization(request): 28 | auth_rx = re.compile(r"^Bearer (.+)$") 29 | if request is None or "HTTP_AUTHORIZATION" not in request.META: 30 | return None 31 | 32 | token_rx_match = auth_rx.search(request.META["HTTP_AUTHORIZATION"]) 33 | if not token_rx_match: 34 | return None 35 | 36 | token = token_rx_match.group(1) 37 | return token 38 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = {{ cookiecutter.version }} 3 | tag = True 4 | commit = True 5 | commit_args = --no-verify 6 | 7 | [bumpversion:file:{{ cookiecutter.main_module }}/__init__.py] 8 | 9 | [bumpversion:file:{{ cookiecutter.main_module }}/templates/rest_framework/api.html] 10 | 11 | [bumpversion:file:docs/index.md] 12 | 13 | [bumpversion:file:README.md] 14 | 15 | [flake8] 16 | ignore = E203, E266, E501, W503 17 | max-line-length = 100 18 | select = B,C,E,F,W,T4,B9 19 | exclude = .tox,.git,*/migrations/*,*/static/*,docs,venv,.venv,node_modules 20 | 21 | [tool:pytest] 22 | addopts = --failed-first --new-first 23 | DJANGO_SETTINGS_MODULE = settings.testing 24 | norecursedirs = .tox .git */migrations/* */static/* docs venv node_modules 25 | 26 | [mypy] 27 | ignore_missing_imports = True 28 | allow_untyped_globals = True 29 | allow_redefinition = True 30 | show_error_codes = True 31 | plugins = 32 | mypy_django_plugin.main 33 | 34 | [mypy.plugins.django-stubs] 35 | django_settings_module = settings.development 36 | 37 | [coverage:run] 38 | source = {{ cookiecutter.main_module }}/ 39 | omit = 40 | *tests* 41 | *commands* 42 | *migrations* 43 | *admin* 44 | *wsgi* 45 | *conftest* 46 | 47 | [coverage:report] 48 | show_missing = True 49 | skip_covered = True 50 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/dev.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | dev_postgres_data: {} 3 | dev_postgres_data_backups: {} 4 | 5 | services: 6 | django: &django 7 | build: 8 | context: . 9 | dockerfile: ./compose/dev/django/Dockerfile 10 | image: {{ cookiecutter.main_module }}_dev_django 11 | depends_on: 12 | - postgres 13 | - redis 14 | env_file: 15 | - ./.envs/.dev/.env 16 | command: /start 17 | ports: ["8000:5000"] 18 | 19 | postgres: 20 | build: 21 | context: . 22 | dockerfile: ./compose/dev/postgres/Dockerfile 23 | image: {{ cookiecutter.main_module }}_dev_postgres 24 | volumes: 25 | - dev_postgres_data:/var/lib/postgresql/data:Z 26 | - dev_postgres_data_backups:/backups:z 27 | env_file: 28 | - ./.envs/.dev/.env 29 | 30 | redis: 31 | image: redis:7.0 32 | 33 | celeryworker: 34 | <<: *django 35 | image: {{ cookiecutter.main_module }}_dev_celeryworker 36 | command: /start-celeryworker 37 | ports: [] 38 | 39 | celerybeat: 40 | <<: *django 41 | image: {{ cookiecutter.main_module }}_dev_celerybeat 42 | command: /start-celerybeat 43 | ports: [] 44 | 45 | flower: 46 | <<: *django 47 | image: {{ cookiecutter.main_module }}_dev_flower 48 | command: /start-flower 49 | ports: [] 50 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/templates/django.uwsgi.ini.j2: -------------------------------------------------------------------------------- 1 | {% raw %}[uwsgi] 2 | chdir={{project_path}} 3 | module=wsgi:application 4 | # master=True 5 | pidfile={{uwsgi_pid_file}} 6 | uid={{uwsgi_user}} 7 | gid={{uwsgi_group}} 8 | vacuum=True 9 | 10 | # minimum number of workers to keep at all times 11 | cheaper = {{ uwsgi_min_workers }} 12 | 13 | # number of workers to spawn at startup 14 | cheaper-initial = {{ uwsgi_initial_workers }} 15 | 16 | # maximum number of workers that can be spawned 17 | workers = {{ uwsgi_max_workers }} 18 | 19 | # how many workers should be spawned at a time 20 | cheaper-step = {{ uwsgi_workers_spawn_rate }} 21 | 22 | max-requests={{uwsgi_worker_connections}} 23 | 24 | buffer-size=8192 25 | home={{venv_path}} 26 | 27 | socket={{uwsgi_socket}} 28 | # daemonize=/var/log/uwsgi/fueled.log 29 | log-x-forwarded-for=true 30 | plugins=python{{ python_version | replace(".", "") }} 31 | 32 | # for newrelic support 33 | enable-threads=true 34 | 35 | # default params 36 | autoload=true 37 | no-orphans=true 38 | chmod-socket=660 39 | log-date=true 40 | logto={{ uwsgi_log_file }}{% endraw %} 41 | 42 | # os write errors 43 | # https://github.com/getsentry/raven-python/issues/732#issuecomment-176854438 44 | ignore-sigpipe = true 45 | ignore-write-errors = true 46 | disable-write-exception = true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Project Specific 2 | hello-world-backend/ 3 | 4 | ## VirtualEnv 5 | .venv/ 6 | venv/ 7 | 8 | ### OSX ### 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | 13 | # Icon must ends with two \r. 14 | Icon 15 | 16 | 17 | # Thumbnails 18 | ._* 19 | 20 | # Files that might appear on external disk 21 | .Spotlight-V100 22 | .Trashes 23 | 24 | 25 | ### Python ### 26 | # Byte-compiled / optimized / DLL files 27 | __pycache__/ 28 | *.py[cod] 29 | .mypy_cache/ 30 | 31 | # C extensions 32 | *.so 33 | 34 | # Distribution / packaging 35 | .Python 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | .tox/ 46 | .coverage 47 | .cache 48 | .pytest_cache/ 49 | nosetests.xml 50 | coverage.xml 51 | 52 | # Translations 53 | *.mo 54 | 55 | # Mr Developer 56 | .mr.developer.cfg 57 | .project 58 | .pydevproject 59 | 60 | # Rope 61 | .ropeproject 62 | 63 | # Django stuff: 64 | *.log 65 | *.pot 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | 71 | ### Linux ### 72 | *~ 73 | 74 | ### Project Generated Files 75 | djcookiecutter-test/ 76 | 77 | ### Vim ### 78 | [._]*.s[a-w][a-z] 79 | [._]s[a-w][a-z] 80 | *.un~ 81 | Session.vim 82 | .netrwhist 83 | 84 | # Pycharm project modules 85 | .idea/ 86 | 87 | 88 | ### VSCode 89 | .vscode/ -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/templates/uwsgi-emperor-init.d.j2: -------------------------------------------------------------------------------- 1 | {% raw %}#!/bin/sh 2 | # kFreeBSD do not accept scripts as interpreters, using #!/bin/sh and sourcing. 3 | if [ true != "$INIT_D_SCRIPT_SOURCED" ]; then 4 | set "$0" "$@" 5 | INIT_D_SCRIPT_SOURCED=true . /lib/init/init-d-script 6 | fi 7 | ### BEGIN INIT INFO 8 | # Provides: uwsgi-emperor 9 | # Required-Start: $local_fs $remote_fs $network 10 | # Required-Stop: $local_fs $remote_fs $network 11 | # Default-Start: 2 3 4 5 12 | # Default-Stop: 0 1 6 13 | # Short-Description: Start/stop uWSGI server instance(s) 14 | # Description: This script manages uWSGI Emperor server instance(s). 15 | ### END INIT INFO 16 | 17 | # Author: Jonas Smedegaard 18 | 19 | DESC="uWSGI Emperor server" 20 | DAEMON=/usr/bin/uwsgi 21 | PIDFILE={{ uwsgi_emperor_pid_file }} 22 | LOGFILE=/var/log/uwsgi/emperor.log 23 | # TODO: drop die-on-term with 2.1+ (see bug#799971) 24 | DAEMON_ARGS="--ini /etc/uwsgi-emperor/emperor.ini --die-on-term --pidfile $PIDFILE --daemonize $LOGFILE" 25 | SCRIPTNAME="/etc/init.d/uwsgi-emperor" 26 | 27 | alias do_reload=do_reload_sigusr1 28 | 29 | do_start_prepare() { 30 | # Create with correct permissions in advance as uwsgi with --daemonize creates it world write otherwise 31 | touch "$PIDFILE" 32 | }{% endraw %} 33 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/celery/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | - name: copy celery.log logrotate 3 | template: src=celery.log.j2 4 | dest=/etc/logrotate.d/celery-{{ project_namespace }} 5 | mode=644 6 | tags: ['configure', 'celery'] 7 | 8 | - name: make sure log directory exists 9 | file: path={{ celery_log_dir }} state=directory owner={{celery_user}} group={{celery_group}} mode=751 recurse=yes 10 | tags: ['configure', 'celery'] 11 | 12 | - name: ensure python virtualenv exist 13 | command: python3 -m venv {{ venv_path }} creates={{ venv_path }} 14 | become: false 15 | tags: ['celery'] 16 | 17 | - name: ensure celery package is installed 18 | pip: name=celery state=present executable={{ venv_path }}/bin/pip 19 | become: false 20 | tags: ['celery'] 21 | 22 | - name: copy celery service 23 | template: src=celery.service.j2 dest=/etc/systemd/system/celery-{{ project_namespace }}.service 24 | tags: ['celery'] 25 | 26 | - name: copy celery beat service 27 | template: src=celerybeat.service.j2 dest=/etc/systemd/system/celerybeat-{{ project_namespace }}.service 28 | tags: ['celery'] 29 | 30 | - name: start celery service 31 | shell: service celery-{{ project_namespace }} start 32 | tags: ['celery'] 33 | 34 | - name: start celery service 35 | shell: service celerybeat-{{ project_namespace }} start 36 | tags: ['celery'] 37 | {% endraw %} 38 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/hosts: -------------------------------------------------------------------------------- 1 | [all:vars] 2 | vm=0 3 | user=ubuntu 4 | project_namespace={% raw %}{{ project_name }}-{{ deploy_env }}{% endraw %} 5 | project_path=/home/ubuntu/{% raw %}{{ deploy_env }}{% endraw %}/{{ cookiecutter.github_repository }} 6 | venv_path={% raw %}{{ project_path }}/venv{% endraw %} 7 | use_letsencrypt={{ 'True' if cookiecutter.letsencrypt.lower() == 'y' else 'False' }} 8 | letsencrypt_email={{ cookiecutter.letsencrypt_email }} 9 | django_requirements_file=requirements.txt 10 | django_settings="settings.production" 11 | 12 | [vagrant] 13 | 192.168.33.12 14 | 15 | [vagrant:vars] 16 | vm=1 17 | deploy_env=vagrant 18 | user=vagrant 19 | project_path=/home/vagrant/{{ cookiecutter.github_repository }} 20 | venv_path=/home/vagrant/venv 21 | django_requirements_file=requirements_dev.txt 22 | django_settings="settings.development" 23 | use_letsencrypt=False 24 | pg_db={{ cookiecutter.main_module }} 25 | pg_user=vagrant 26 | pg_password=vagrant 27 | domain_name=vagrant.{{ cookiecutter.main_module }}.com 28 | 29 | [dev] 30 | dev.{{ cookiecutter.main_module }}.com 31 | 32 | [dev:vars] 33 | deploy_env=dev 34 | domain_name=dev.{{ cookiecutter.main_module }}.com 35 | 36 | [qa] 37 | qa.{{ cookiecutter.main_module }}.com 38 | 39 | [qa:vars] 40 | deploy_env=qa 41 | domain_name=qa.{{ cookiecutter.main_module }}.com 42 | 43 | [production] 44 | {{ cookiecutter.main_module }}.com 45 | 46 | [production:vars] 47 | deploy_env=prod 48 | domain_name={{ cookiecutter.main_module }}.com 49 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/utils/urls.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | import django_sites as sites 3 | from django.conf import settings 4 | from django.urls import reverse as django_reverse 5 | 6 | URL_TEMPLATE = "{scheme}://{domain}/{path}" 7 | 8 | 9 | def build_url(path, scheme="http", domain="localhost"): 10 | return URL_TEMPLATE.format(scheme=scheme, domain=domain, path=path.lstrip("/")) 11 | 12 | 13 | def is_absolute_url(path): 14 | """Test wether or not `path` is absolute url.""" 15 | return path.startswith("http") 16 | 17 | 18 | def get_absolute_url(path, site_id=None): 19 | """Return a path as an absolute url.""" 20 | if is_absolute_url(path): 21 | return path 22 | if site_id: 23 | site = sites.get_by_id(site_id) 24 | else: 25 | site = sites.get_current() 26 | return build_url(path, scheme=site.scheme, domain=site.domain) 27 | 28 | 29 | def resolve_frontend_url(name, site_id="frontend", **kwargs): 30 | """Returns the absolute url for the frontend site 31 | resolve_front_urls('password-confirm', token="xyz", uuid="abc") 32 | """ 33 | urls = settings.FRONTEND_URLS 34 | path = urls[name].format(**kwargs) 35 | return get_absolute_url(path=path, site_id=site_id) 36 | 37 | 38 | def reverse(viewname, *args, **kwargs): 39 | """Same behavior as django's reverse but uses django_sites to compute absolute url.""" 40 | return get_absolute_url(django_reverse(viewname, *args, **kwargs)) 41 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/models.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import uuid 3 | 4 | # Third Party Stuff 5 | from django.db import models 6 | from uuid_upload_path import upload_to 7 | from versatileimagefield.fields import PPOIField, VersatileImageField 8 | 9 | 10 | class UUIDModel(models.Model): 11 | """An abstract base class model that makes primary key `id` as UUID 12 | instead of default auto incremented number. 13 | """ 14 | 15 | id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4) 16 | 17 | class Meta: 18 | abstract = True 19 | 20 | 21 | class TimeStampedUUIDModel(UUIDModel): 22 | """An abstract base class model that provides self-updating 23 | ``created`` and ``modified`` fields with UUID as primary_key field. 24 | """ 25 | 26 | created_at = models.DateTimeField(auto_now_add=True, editable=False) 27 | modified_at = models.DateTimeField(auto_now=True, editable=False) 28 | 29 | class Meta: 30 | abstract = True 31 | 32 | 33 | class ImageMixin(models.Model): 34 | """An abstract base class model that provides a VersatileImageField Image with POI""" 35 | 36 | image = VersatileImageField( 37 | upload_to=upload_to, 38 | blank=True, 39 | null=True, 40 | ppoi_field="image_poi", 41 | verbose_name="image", 42 | ) 43 | image_poi = PPOIField(verbose_name="image's Point of Interest") # point of interest 44 | 45 | class Meta: 46 | abstract = True 47 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/tasks/letsencrypt.yml: -------------------------------------------------------------------------------- 1 | {%raw%}--- 2 | - name: update the apt cache 3 | apt: 4 | update_cache: yes 5 | cache_valid_time: 7200 6 | become: yes 7 | 8 | - name: Install Certbot 9 | apt: 10 | pkg: 11 | - certbot 12 | - python3-certbot-nginx 13 | state: present 14 | cache_valid_time: 300 15 | 16 | - name: create certbot folder 17 | become: yes 18 | file: path={{ letsencrypt_challenge_root }} state=directory owner={{ user }} group={{ nginx_user }} mode="u=rwx,g=rwxs,o=r" 19 | 20 | - name: upgrade ca-certificates 21 | apt: 22 | name: ca-certificates 23 | state: latest 24 | cache_valid_time: 300 25 | 26 | - name: Check if certificate already exists. 27 | stat: 28 | path: /etc/letsencrypt/live/{{ domain_name | first | replace('*.', '') }}/cert.pem 29 | register: letsencrypt_cert 30 | 31 | - name: Reload nginx 32 | service: name=nginx state=reloaded 33 | 34 | - name: request cert 35 | command: "certbot certonly --nginx --domains {{ domain_name }} --email {{ letsencrypt_email }} --agree-tos --non-interactive" 36 | args: 37 | creates: "{{ letsencrypt_ssl_cert_dir }}" 38 | become: yes 39 | when: not letsencrypt_cert.stat.exists 40 | 41 | - name: remove any cert diretory if exist 42 | file: path={{ ssl_cert_dir }}/{{ domain_name }} state=absent 43 | 44 | - name: symblink certificates 45 | file: src={{ letsencrypt_ssl_cert_dir }} dest={{ ssl_cert_dir }}/{{ domain_name }} owner={{ user }} group=www-data state=link 46 | {%endraw%} 47 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/api/routers.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework import routers 3 | 4 | 5 | class SingletonRouter(routers.SimpleRouter): 6 | """Same as default router but without detail route and GET, POST, PUT, PATCH and 7 | DELETE maps to same url as list. 8 | 9 | See CurrentUserViewset for usages. This allows GenericViewSet to be used against 10 | a singleton resource. If `/me` endpoint represents currently logged in user 11 | you are able to `GET /me`, `PUT /me`, `DELETE /me` and can also add any list_routes like 12 | `POST /me/change-avatar`. 13 | """ 14 | 15 | routes = [ 16 | # Mapping for list, create, update, partial_update and delete function to http verb. 17 | routers.Route( 18 | url=r"^{prefix}{trailing_slash}$", 19 | mapping={ 20 | "get": "list", 21 | "post": "create", 22 | "patch": "partial_update", 23 | "put": "update", 24 | "delete": "destroy", 25 | }, 26 | name="{basename}", 27 | detail=False, 28 | initkwargs={"suffix": ""}, 29 | ), 30 | # Dynamically generated list routes. 31 | # Generated using @action decorator 32 | # on methods of the viewset. 33 | routers.DynamicRoute( 34 | url=r"^{prefix}/{url_path}$", 35 | name="{basename}-{url_name}", 36 | detail=False, 37 | initkwargs={}, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/backends.py: -------------------------------------------------------------------------------- 1 | """ 2 | Authentication backends for rest framework. 3 | 4 | This module exposes two backends: session and token. 5 | 6 | The first (session) is a modified version of standard 7 | session authentication backend of restframework with2 8 | csrf token disabled. 9 | 10 | And the second (token) implements own version of oauth2 11 | like authentiacation but with selfcontained tokens. Thats 12 | makes authentication totally stateles. 13 | 14 | It uses django signing framework for create new 15 | selfcontained tokens. This trust tokes from external 16 | fraudulent modifications. 17 | """ 18 | # Third Party Stuff 19 | from rest_framework.authentication import BaseAuthentication 20 | 21 | from .tokens import get_user_for_token 22 | from .utils import get_http_authorization 23 | 24 | 25 | class JWTAuthenticationMixin: 26 | def authenticate(self, request): 27 | token = get_http_authorization(request) 28 | if not token: 29 | return None 30 | 31 | user = get_user_for_token(token, "authentication") 32 | 33 | return (user, token) 34 | 35 | def authenticate_header(self, request): 36 | return 'Bearer realm="api"' 37 | 38 | 39 | class RestJWTAuthentication(JWTAuthenticationMixin, BaseAuthentication): 40 | """Self-contained stateles authentication implementation that work similar to OAuth2. 41 | 42 | It uses json web tokens (https://github.com/jpadilla/pyjwt) for trust 43 | data stored in the token. 44 | """ 45 | pass 46 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/403_csrf.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %}{% trans '403 Forbidden' %}{% endblock %} 4 | {% block head_extras %} 5 | 6 | {% endblock head_extras %} 7 | {% block body_classes %}{{ block.super }} page-error page-403 page-403-csrf {% endblock body_classes %} 8 | 9 | {% block content %} 10 | 11 |
12 |

{% trans "Forbidden" %} (403)

13 |

{% trans "CSRF verification failed. Request aborted." %}

14 | {% if no_referer %} 15 |

{% trans "You are seeing this message because this HTTPS site requires a 'Referer header' to be sent by your Web browser, but none was sent. This header is required for security reasons, to ensure that your browser is not being hijacked by third parties." %}

16 |

{% trans "If you have configured your browser to disable 'Referer' headers, please re-enable them, at least for this site, or for HTTPS connections, or for 'same-origin' requests." %}

17 | {% endif %} 18 | {% if no_cookie %} 19 |

{% trans "You are seeing this message because this site requires a CSRF cookie when submitting forms. This cookie is required for security reasons, to ensure that your browser is not being hijacked by third parties." %}

20 |

{% trans "If you have configured your browser to disable cookies, please re-enable them, at least for this site, or for 'same-origin' requests." %}

21 | {% endif %} 22 |
23 | {% endblock content %}{% endraw %} 24 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/backend/api_mixins.md: -------------------------------------------------------------------------------- 1 | # DRF helpers 2 | 3 | ## Permissions Per Action Helper 4 | 5 | DRF allows us to set permissions on an entire viewset. 6 | 7 | ``` 8 | class FooViewSet(viewsets.ModelViewSet): 9 | permission_classes = [permissions.IsAuthenticated] 10 | ``` 11 | 12 | `PermissionPerAction` mixin available in the `base` module allows us to set permission classes for each of the actions (`list`, `retrieve`, `create`, `partial_update`, `update` or any other custom actions) on the viewset. 13 | 14 | ``` 15 | import project_name.base.api.mixins import PermissionPerAction 16 | 17 | class FooViewSet(PermissionPerAction, viewsets.ModelViewSet): 18 | permissions_per_action = { 19 | "create": [permissions.IsAuthenticated], 20 | "list": [permissions.AllowAny] 21 | } 22 | ``` 23 | 24 | ## Multiple Serializers for a viewset 25 | 26 | Similiar to permission classes, DRF allows us to only set one serializer per viewset. 27 | 28 | ``` 29 | class FooViewSet(viewsets.ModelViewSet): 30 | serializer_class = serializers.FooSerializer 31 | ``` 32 | 33 | `MultipleSerializerMixin` available in the `base` module allows us to set different serializer classes for each of the actions (`list`, `retrieve`, `create`, `partial_update`, `update` or any other custom actions) on the viewset. 34 | 35 | ``` 36 | import project_name.base.api.mixins import MultipleSerializerMixin 37 | 38 | class FooViewSet(MultipleSerializerMixin, viewsets.ModelViewSet): 39 | serializer_classes = { 40 | "create": serializers.CreateFooSerializer, 41 | "list": serializers.ReadFooSerializer 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must ends with two \r. 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear on external disk 13 | .Spotlight-V100 14 | .Trashes 15 | 16 | 17 | ### Python ### 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | .tox/ 37 | .coverage 38 | .cache 39 | .pytest_cache/ 40 | nosetests.xml 41 | coverage.xml 42 | htmlcov/ 43 | 44 | # Translations 45 | *.mo 46 | 47 | # Mr Developer 48 | .mr.developer.cfg 49 | .project 50 | .pydevproject 51 | 52 | # Rope 53 | .ropeproject 54 | 55 | # Django stuff: 56 | *.log 57 | *.pot 58 | .staticfiles/ 59 | .media/ 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # npm 65 | node_modules/ 66 | 67 | # Campass 68 | .sass-cache 69 | 70 | # Celery 71 | celerybeat-schedule 72 | 73 | # Vagrant 74 | .vagrant 75 | 76 | # Redis 77 | dump.rdb 78 | 79 | # python-dotenv 80 | .env 81 | 82 | # {{ cookiecutter.project_name }} 83 | _docs_html/ 84 | media/ 85 | provisioner/site.retry 86 | 87 | # DB Schema as it gets generated on server 88 | database-schema.svg 89 | 90 | # Virtualenv 91 | venv/ 92 | .venv/ 93 | 94 | ### Vim ### 95 | [._]*.s[a-w][a-z] 96 | [._]s[a-w][a-z] 97 | *.un~ 98 | Session.vim 99 | .netrwhist 100 | 101 | # Pycharm 102 | .idea/ 103 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.env.sample: -------------------------------------------------------------------------------- 1 | # Core Stuff 2 | # ============================== 3 | # DJANGO_SETTINGS_MODULE=settings.production 4 | # DJANGO_DEBUG=False 5 | # DJANGO_ADMIN_URL='admin' 6 | # DJANGO_SECRET_KEY='change_with_50+_long_random_string' 7 | # DJANGO_ADMINS='Admin 1, Admin2 ' 8 | 9 | # SITE_SCHEME=https 10 | # SITE_DOMAIN=youdomain.com 11 | # SITE_NAME='your site name' 12 | # ALLOWED_HOSTS=youdomain.com 13 | # DJANGO_SECURE_HSTS_SECONDS=60 14 | # FRONTEND_SITE_DOMAIN='frontendapp.com' 15 | # FRONTEND_SITE_SCHEME='https' 16 | 17 | # Databases 18 | # ============================== 19 | # DATABASE_URL=postgres://username:password@yourhost.com:5432/database_name 20 | # REDIS_URL='redis://localhost:6379/' 21 | 22 | # File Storage 23 | # ============================== 24 | # ENABLE_MEDIA_UPLOAD_TO_S3=False 25 | # DJANGO_AWS_ACCESS_KEY_ID='' 26 | # DJANGO_AWS_SECRET_ACCESS_KEY='' 27 | # DJANGO_AWS_STORAGE_BUCKET_NAME='' 28 | # DJANGO_AWS_S3_HOST='' 29 | # DJANGO_AWS_S3_REGION_NAME='' 30 | 31 | # APIs 32 | # ============================== 33 | # API_DEBUG=False 34 | 35 | # Emails 36 | # ============================== 37 | # DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 38 | # DEFAULT_FROM_EMAIL='' 39 | # EMAIL_HOST='' 40 | # EMAIL_HOST_PASSWORD='' 41 | # EMAIL_HOST_USER='' 42 | # EMAIL_PORT=587 43 | # EMAIL_SUBJECT_PREFIX='' 44 | # EMAIL_USE_TLS='' 45 | 46 | # CORS 47 | # ============================== 48 | # CORS_ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:9000,https://example.com 49 | 50 | {% if cookiecutter.add_sentry == "y" -%} 51 | # SENTRY 52 | # ============================== 53 | # SENTRY_DSN='' 54 | # SENTRY_ENVIRONMENT=production 55 | {%- endif -%} 56 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/templates/site.80.conf.j2: -------------------------------------------------------------------------------- 1 | {%raw%}# {{ ansible_managed }} 2 | server { 3 | listen 80; 4 | listen [::]:80; 5 | server_name {% if vm %}_{% else %}{{ domain_name }}{% endif %}; 6 | 7 | {% if use_letsencrypt %} 8 | location /.well-known/acme-challenge/ { 9 | root {{ letsencrypt_challenge_root }}; 10 | try_files $uri =404; 11 | } 12 | {% endif %} 13 | 14 | {% if vm and (nginx_cert.stat.exists == false or nginx_key.stat.exists == false) %} 15 | location / {{% endraw %} 16 | {%- if cookiecutter.add_asgi.lower() == 'y' %} 17 | {%raw%}proxy_pass unix://{{ asgi_socket }};{% endraw %} 18 | {%- else %} 19 | {%raw%}uwsgi_pass unix:///tmp/uwsgi-{{ project_namespace }}.sock;{% endraw %} 20 | {%- endif %} 21 | {% raw %} 22 | include /etc/nginx/uwsgi_params; 23 | 24 | # set correct scheme 25 | uwsgi_param UWSGI_SCHEME $http_x_forwarded_proto; 26 | } 27 | 28 | location /docs { 29 | # mkdocs html generated via "product_data" role 30 | alias {{ project_path }}/_docs_html/;{% endraw %} 31 | {%- if cookiecutter.have_password_protected_docs.lower() == 'y' %} 32 | {%raw%}auth_basic "Restricted Content"; 33 | auth_basic_user_file {{ htpasswd_file_path }};{% endraw %} 34 | {%- endif -%}{%raw%} 35 | } 36 | 37 | location /static/ { 38 | alias {{ project_path }}/.staticfiles/; 39 | } 40 | 41 | location /media/ { 42 | alias {{ project_path }}/.media/; 43 | } 44 | 45 | {% else %} 46 | location / { 47 | return 301 https://$server_name$request_uri; 48 | } 49 | {% endif %} 50 | }{%endraw%} 51 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/users/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | from graphene_django.filter import DjangoFilterConnectionField 4 | 5 | from {{cookiecutter.main_module}}.graphql.decorators import login_required, superuser_required 6 | from {{cookiecutter.main_module}}.graphql.utils import filter_objects 7 | from {{cookiecutter.main_module}}.users.models import User 8 | from .mutations import ( 9 | Login, 10 | PasswordChange, 11 | PasswordResetConfirm, 12 | RequestPasswordReset, 13 | SignUp, 14 | ) 15 | from .resolvers import get_all_users 16 | from .types import CurrentUser, UserConnection 17 | 18 | 19 | class UserQueries(graphene.ObjectType): 20 | me = graphene.Field( 21 | CurrentUser, description="Return the currently authenticated user" 22 | ) 23 | users = DjangoFilterConnectionField( 24 | UserConnection, description="Return list of all Users" 25 | ) 26 | user_details = graphene.Field(UserConnection, user_id=graphene.ID()) 27 | 28 | @login_required 29 | def resolve_me(self, info): 30 | return info.context.user 31 | 32 | @superuser_required 33 | def resolve_users(self, info, **kwargs): 34 | qs = get_all_users(info) 35 | # add filters 36 | return qs 37 | 38 | @superuser_required 39 | def resolve_user_details(self, info, user_id): 40 | return filter_objects( 41 | User, user_id 42 | ).first() 43 | 44 | 45 | class UserMutations(graphene.ObjectType): 46 | signup = SignUp.Field() 47 | login = Login.Field() 48 | password_change = PasswordChange.Field() 49 | password_reset = RequestPasswordReset.Field() 50 | password_reset_confirm = PasswordResetConfirm.Field() 51 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: vagrant 3 | vars_files: 4 | - vars.yml 5 | gather_facts: true 6 | become: true 7 | become_method: sudo 8 | 9 | roles: 10 | - common 11 | - nginx 12 | - postgresql 13 | - project_data 14 | {%- if cookiecutter.add_celery.lower() == 'y' %} 15 | - redis 16 | - celery 17 | {%- else %} 18 | # - redis 19 | # - celery 20 | {%- endif %} 21 | 22 | #= Dev 23 | #=================================================== 24 | - hosts: dev 25 | vars_files: 26 | - vars.yml 27 | gather_facts: true 28 | become: true 29 | become_method: sudo 30 | 31 | roles: 32 | - common 33 | - nginx 34 | - postgresql 35 | - project_data 36 | {%- if cookiecutter.add_celery.lower() == 'y' %} 37 | - redis 38 | - celery 39 | {%- else %} 40 | # - redis 41 | # - celery 42 | {%- endif %} 43 | 44 | #= QA 45 | #=================================================== 46 | - hosts: qa 47 | vars_files: 48 | - vars.yml 49 | gather_facts: true 50 | become: true 51 | become_method: sudo 52 | 53 | roles: 54 | - common 55 | - nginx 56 | - project_data 57 | - postgresql 58 | {%- if cookiecutter.add_celery.lower() == 'y' %} 59 | - redis 60 | - celery 61 | {%- else %} 62 | # - redis 63 | # - celery 64 | {%- endif %} 65 | 66 | #= Production 67 | #=================================================== 68 | - hosts: production 69 | vars_files: 70 | - vars.yml 71 | gather_facts: true 72 | become: true 73 | become_method: sudo 74 | 75 | roles: 76 | - common 77 | - nginx 78 | - project_data 79 | {%- if cookiecutter.add_celery.lower() == 'y' %} 80 | - redis 81 | - celery 82 | {%- else %} 83 | # - redis 84 | # - celery 85 | {%- endif %} 86 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/graphql/2-users.md: -------------------------------------------------------------------------------- 1 | ## Current User 2 | (requires authentication) 3 | 4 | __Request__ 5 | ``` 6 | query { 7 | me { 8 | id 9 | firstName 10 | lastName 11 | email 12 | } 13 | } 14 | ``` 15 | 16 | __Response__ 17 | 18 | ```json 19 | { 20 | "data": { 21 | "me": { 22 | "id": "Q3VycmVudFVzZXI6M2MzYjVhMmUtMWM0MC00MTQzLTk1N2ItYjVlYTAzOWU0NzVi", 23 | "first_name": "John", 24 | "last_name": "Hawley", 25 | "email": "john@localhost.com" 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 32 | ## All Users 33 | (requires authentication and superuser privilege) 34 | 35 | __Request__ 36 | ``` 37 | query { 38 | users { 39 | totalCount, 40 | edgeCount, 41 | edges { 42 | node { 43 | id, 44 | firstName, 45 | lastName 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | __Response__ 53 | 54 | ```json 55 | { 56 | "data": { 57 | "users": { 58 | "totalCount": 2, 59 | "edgeCount": 2, 60 | "edges": [ 61 | { 62 | "node": { 63 | "id": "VXNlckNvbm5lY3Rpb246M2MzYjVhMmUtMWM0MC00MTQzLTk1N2ItYjVlYTAzOWU0NzVi", 64 | "firstName": "first name", 65 | "lastName": "last name" 66 | } 67 | }, 68 | { 69 | "node": { 70 | "id": "VXNlckNvbm5lY3Rpb246ZjU4N2IyY2EtNThmMS00NTE3LTgyMTEtYzczODA3YTI1ZTU1", 71 | "firstName": "fueled", 72 | "lastName": "user" 73 | } 74 | } 75 | ] 76 | } 77 | } 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/integration/test_current_user_api.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import json 3 | 4 | # Third Party Stuff 5 | import pytest 6 | from django.urls import reverse 7 | 8 | from tests import factories as f 9 | 10 | pytestmark = pytest.mark.django_db 11 | 12 | 13 | def test_get_current_user_api(client): 14 | url = reverse("me") 15 | user = f.create_user(email="test@example.com") 16 | 17 | # should require auth 18 | response = client.get(url) 19 | assert response.status_code == 401 20 | 21 | client.login(user) 22 | response = client.get(url) 23 | 24 | # assert response is None 25 | assert response.status_code == 200 26 | expected_keys = ["id", "email", "first_name", "last_name"] 27 | assert set(expected_keys).issubset(response.data.keys()) 28 | assert response.data["id"] == str(user.id) 29 | 30 | 31 | def test_patch_current_user_api(client): 32 | url = reverse("me") 33 | user = f.create_user(email="test@example.com", first_name="test", last_name="test") 34 | 35 | data = { 36 | "first_name": "modified_test", 37 | "last_name": "modified_test", 38 | "email": "modified_test@example.com", 39 | } 40 | 41 | # should require auth 42 | response = client.json.patch(url, json.dumps(data)) 43 | assert response.status_code == 401 44 | 45 | client.login(user) 46 | response = client.json.patch(url, json.dumps(data)) 47 | # assert response is None 48 | assert response.status_code == 200 49 | expected_keys = ["id", "email", "first_name", "last_name"] 50 | assert set(expected_keys).issubset(response.data.keys()) 51 | 52 | assert response.data["first_name"] == "modified_test" 53 | assert response.data["last_name"] == "modified_test" 54 | assert response.data["email"] == "modified_test@example.com" 55 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI config for project. 2 | 3 | This module contains the WSGI application used by Django's development server 4 | and any production WSGI deployments. It should expose a module-level variable 5 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 6 | this application via the ``WSGI_APPLICATION`` setting. 7 | 8 | Usually you will have the standard Django WSGI application here, but it also 9 | might make sense to replace the whole Django WSGI application with a custom one 10 | that later delegates to the Django one. For example, you could introduce WSGI 11 | middleware here, or combine a Django application with an application of another 12 | framework. 13 | """ 14 | 15 | # Standard Library 16 | import os 17 | 18 | # Third Party Stuff 19 | from django.core.wsgi import get_wsgi_application 20 | from dotenv import load_dotenv 21 | 22 | # Read .env file and set key/value inside it as environement variables 23 | # see: http://github.com/theskumar/python-dotenv 24 | load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) 25 | 26 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 27 | # if running multiple sites in the same mod_wsgi process. To fix this, use 28 | # mod_wsgi daemon mode with each site in its own daemon process, or use 29 | # os.environ['DJANGO_SETTINGS_MODULE'] = '.settings' 30 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.production") 31 | 32 | # This application object is used by any WSGI server configured to use this 33 | # file. This includes Django's development server, if the WSGI_APPLICATION 34 | # setting points here. 35 | application = get_wsgi_application() 36 | 37 | # Apply WSGI middleware here. 38 | # from helloworld.wsgi import HelloWorldApplication 39 | # application = HelloWorldApplication(application) 40 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-20.04 9 | 10 | services: 11 | postgres: 12 | image: {% if cookiecutter.add_postgis.lower() == 'y' %}postgis/postgis:13-master{% else %}postgres:13{% endif %} 13 | env: 14 | POSTGRES_USER: postgres 15 | POSTGRES_PASSWORD: postgres 16 | POSTGRES_DB: postgres 17 | ports: 18 | - 5432:5432 19 | # needed because the postgres container does not provide a healthcheck 20 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 21 | 22 | env: 23 | DATABASE_URL: {% if cookiecutter.add_postgis.lower() == 'y' %}postgis{% else %}postgres{% endif %}://postgres:postgres@localhost:5432/{{cookiecutter.main_module}} 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | {%- if cookiecutter.add_postgis.lower() == 'y' %} 28 | - name: Install gdal 29 | run: | 30 | sudo apt-get install gdal-bin 31 | sudo apt-get install libgdal-dev 32 | # Update is essential for fetching postgis-3 scripts 33 | sudo apt-get update 34 | sudo apt-get install postgresql-13-postgis-3-scripts 35 | {%- endif %} 36 | - name: Set up Python 3.9 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: '3.9' 40 | cache: 'pip' 41 | - name: Install poetry 42 | run: | 43 | pip3 install poetry==1.2.0 44 | - name: Install requirements 45 | run: | 46 | python -m pip install --upgrade pip 47 | poetry install --with dev 48 | - name: Run tests 49 | run: | 50 | poetry run pytest --cov -v --tb=native 51 | - name: Linting 52 | run: | 53 | make lint 54 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/views.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import sys 3 | 4 | # Third Party Stuff 5 | from django import http 6 | from django.conf import settings 7 | from django.shortcuts import render 8 | from django.views.defaults import server_error as default_server_error 9 | 10 | 11 | def server_error(request, *args, **kwargs): 12 | """JSON aware server 500 error response. 13 | 14 | As we don't want to return html response for a json request. 15 | """ 16 | if ( 17 | not settings.DEBUG 18 | and request.META.get("CONTENT_TYPE", None) == "application/json" 19 | ): 20 | exc_type, exc_obj, exc_tb = sys.exc_info() 21 | response_dict = { 22 | "error_type": exc_type.__name__ if exc_type else "ServerError", 23 | "errors": [{"message": "Server application error"}], 24 | } 25 | return http.JsonResponse(data=response_dict, status=500) 26 | 27 | return default_server_error(request, *args, **kwargs) 28 | 29 | 30 | def csrf_failure(request, reason="", template_name="403_csrf.html"): 31 | """Custom view used when request fails CSRF protection. 32 | 33 | Custom view is used because Django compressor complains if trying to 34 | use the in-built default view due to use `render` function. It keeps the 35 | logic but make things less magical. 36 | """ 37 | from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER 38 | 39 | ctx = { 40 | "reason": reason, 41 | "no_referer": reason == REASON_NO_REFERER, 42 | "no_cookie": reason == REASON_NO_CSRF_COOKIE, 43 | "DEBUG": settings.DEBUG, 44 | } 45 | return render(request, template_name=template_name, context=ctx, status=403) 46 | 47 | 48 | def root_txt_files(request, filename): 49 | return render(request, filename, {}, content_type="text/plain") 50 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/celery.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import os 3 | 4 | # Third Party Stuff 5 | {%- if cookiecutter.add_sentry.lower() == 'y' %} 6 | import raven 7 | {%- endif %} 8 | from celery import Celery 9 | from django.conf import settings 10 | from dotenv import load_dotenv 11 | {%- if cookiecutter.add_sentry.lower() == 'y'%} 12 | from raven.contrib.celery import register_logger_signal, register_signal 13 | {%- endif %} 14 | 15 | # Set the default Django settings module for the 'celery' program. 16 | load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env")) 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.development") 18 | {%- if cookiecutter.add_sentry.lower() == 'y' %} 19 | 20 | 21 | class CeleryCustomised(Celery): 22 | 23 | def on_configure(self): 24 | client = raven.Client( 25 | os.getenv("SENTRY_DSN"), environment=os.getenv("SENTRY_ENVIRONMENT") 26 | ) 27 | 28 | # Always ensure you import register_logger_signal, register_signal and not their parent modules 29 | # register a custom filter to filter out duplicate logs 30 | register_logger_signal(client) 31 | 32 | # hook into the Celery error handler 33 | register_signal(client) 34 | 35 | 36 | app = CeleryCustomised("{{ cookiecutter.main_module }}") 37 | {%- else %} 38 | 39 | 40 | app = Celery("{{ cookiecutter.main_module }}") 41 | {%- endif %} 42 | 43 | # Using a string here means the worker don't have to serialize 44 | # the configuration object to child processes. 45 | # - namespace='CELERY' means all celery-related configuration keys 46 | # should have a `CELERY_` prefix. 47 | app.config_from_object("django.conf:settings", namespace="CELERY") 48 | 49 | # Load task modules from all registered Django app configs. 50 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 51 | 52 | app.conf.beat_schedule = {} 53 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/postgres/maintenance/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Restore database from a backup. 5 | ### 6 | ### Parameters: 7 | ### <1> filename of an existing backup. 8 | ### 9 | ### Usage: 10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> 11 | 12 | 13 | set -o errexit 14 | set -o pipefail 15 | set -o nounset 16 | 17 | 18 | working_dir="$(dirname ${0})" 19 | source "${working_dir}/_sourced/constants.sh" 20 | source "${working_dir}/_sourced/messages.sh" 21 | 22 | 23 | if [[ -z ${1+x} ]]; then 24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 25 | exit 1 26 | fi 27 | backup_filename="${BACKUP_DIR_PATH}/${1}" 28 | if [[ ! -f "${backup_filename}" ]]; then 29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." 30 | exit 1 31 | fi 32 | 33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." 34 | 35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 37 | exit 1 38 | fi 39 | 40 | export PGHOST="${POSTGRES_HOST}" 41 | export PGPORT="${POSTGRES_PORT}" 42 | export PGUSER="${POSTGRES_USER}" 43 | export PGPASSWORD="${POSTGRES_PASSWORD}" 44 | export PGDATABASE="${POSTGRES_DB}" 45 | 46 | message_info "Dropping the database..." 47 | dropdb "${PGDATABASE}" 48 | 49 | message_info "Creating a new database..." 50 | createdb --owner="${POSTGRES_USER}" 51 | 52 | message_info "Applying the backup to the new database..." 53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" 54 | 55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." 56 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | All contributions are much welcome and greatly appreciated! Expect to be credited for you effort. 3 | 4 | ## General 5 | Generally try to limit the scope of any Pull Request to an atomic update if possible. This way, it's much easier to assess and review your changes. 6 | 7 | You should expect a considerably faster turn around if you submit two or more PRs instead of baking them all into one major PR. 8 | 9 | ## Pull Request Guidelines 10 | Before you submit a pull request, check that it meets these guidelines: 11 | 12 | 1. All the pull requests are made against `master` branch. 13 | 14 | 2. The pull request should include tests. 15 | 16 | 3. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.md. 17 | 18 | 19 | ## Coding conventions 20 | 21 | - Read and pay attention to current code in the repository 22 | - For the Python part, we follow [black](https://pypi.org/project/black/) for formating code. We use modified configuration of [flake8][flake8] to check for linting errors that complies formating standards of `black`. Once you're ready to commit changes, format your code with `black` and check your code with `flake8`. Optionally, setup `pre-commit` with `pre-install --install` to do it automatically before commit. 23 | - Install a plugin for [EditorConfig][editorconfig] and let it handle some of the formating issues for you. 24 | - For the Django part, we follow standard [Django coding style][django-coding style]. 25 | - And always remember the Zen. 26 | - Prefer absolute imports for outside/external modules, and relative imports within the same module. 27 | 28 | [editorconfig]: http://editorconfig.org/ 29 | [flake8]: http://flake8.readthedocs.org/en/latest/ 30 | [django-coding style]: https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/ 31 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | 9 | config.vm.box = "bento/ubuntu-16.04" 10 | 11 | # Make virtualbox use 1GB RAM. Compilation of libraries like lxml may fail for 12 | # less than 1GB RAM. 13 | config.vm.provider "virtualbox" do |v| 14 | v.memory = 1024 15 | end 16 | 17 | # Create a forwarded port mapping which allows access to a specific port 18 | # within the machine from a port on the host machine. In the example below, 19 | # accessing "localhost:8080" will access port 80 on the guest machine. 20 | config.vm.network :forwarded_port, guest: 80, host: 8080 21 | 22 | # Create a private network, which allows host-only access to the machine 23 | # using a specific IP. 24 | config.vm.network :private_network, ip: "192.168.33.12" 25 | 26 | # Create a public network, which generally matched to bridged network. 27 | # Bridged networks make the machine appear as another physical device on 28 | # your network. 29 | # config.vm.network :public_network 30 | 31 | # If true, then any SSH connections made will enable agent forwarding. 32 | # Default value: false 33 | # config.ssh.forward_agent = true 34 | 35 | # Share an additional folder to the guest VM. The first argument is 36 | # the path on the host to the actual folder. The second argument is 37 | # the path on the guest to mount the folder. And the optional third 38 | # argument is a set of non-required options. 39 | config.vm.synced_folder "./", "/home/vagrant/{{ cookiecutter.github_repository }}/" 40 | 41 | config.vm.provision "ansible" do |ansible| 42 | ansible.playbook = "provisioner/site.yml" 43 | ansible.host_key_checking = "False" 44 | ansible.inventory_path = "provisioner/hosts" 45 | ansible.verbose = "v" 46 | ansible.limit = "vagrant" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Fueled 2 | All rights reserved. 3 | 4 | This software consists of voluntary contributions made by many individuals. For 5 | exact contribution history, see the revision history available at 6 | https://github.com/Fueled/django-init 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, this 15 | list of conditions and the following disclaimer in the documentation and/or 16 | other materials provided with the distribution. 17 | 18 | * Neither the name of "django-init" nor the names of its contributors may 19 | be used to endorse or promote products derived from this software without 20 | specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 26 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 30 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 31 | OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ==== 34 | 35 | All files located in the node_modules, venv and external directories are 36 | externally maintained libraries used by this software which have their 37 | own licenses; we recommend you read them, as their terms may differ from 38 | the terms above. 39 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | local_postgres_data: {} 5 | local_postgres_data_backups: {} 6 | 7 | services: 8 | django: &django 9 | build: 10 | context: . 11 | dockerfile: ./compose/local/Dockerfile 12 | image: {{ cookiecutter.main_module }}_local_django 13 | container_name: {{ cookiecutter.main_module }}-django 14 | depends_on: 15 | - postgres 16 | volumes: 17 | - .:/app:z 18 | env_file: 19 | - ./.env 20 | # - ./.envs/.local/.django 21 | # - ./.envs/.local/.postgres 22 | ports: 23 | - "8000:8000" 24 | command: /start 25 | 26 | postgres: 27 | build: 28 | context: . 29 | dockerfile: ./compose/dev/postgres/Dockerfile 30 | image: {{ cookiecutter.main_module }}_local_postgres 31 | container_name: {{ cookiecutter.main_module }}-postgres 32 | volumes: 33 | - local_postgres_data:/var/lib/postgresql/data:Z 34 | - local_postgres_data_backups:/backups:z 35 | env_file: 36 | - ./.env 37 | # - ./.envs/.local/.postgres 38 | 39 | redis: 40 | image: redis:alpine 41 | container_name: {{ cookiecutter.main_module }}-redis 42 | restart: unless-stopped 43 | 44 | celeryworker: 45 | <<: *django 46 | image: {{ cookiecutter.main_module }}_local_celeryworker 47 | container_name: {{ cookiecutter.main_module }}-celeryworker 48 | depends_on: 49 | - redis 50 | - postgres 51 | ports: [] 52 | command: /start-celeryworker 53 | 54 | celerybeat: 55 | <<: *django 56 | image: {{ cookiecutter.main_module }}_local_celerybeat 57 | container_name: {{ cookiecutter.main_module }}-celerybeat 58 | depends_on: 59 | - redis 60 | - postgres 61 | ports: [] 62 | command: /start-celerybeat 63 | 64 | flower: 65 | <<: *django 66 | image: {{ cookiecutter.main_module }}_local_flower 67 | container_name: {{ cookiecutter.main_module }}-flower 68 | ports: 69 | - "5555:5555" 70 | command: /start-flower 71 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/fly/django/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-buster 2 | 3 | # define an alias for the specfic python version used in this file. 4 | FROM python:${PYTHON_VERSION} as python 5 | 6 | ENV POETRY_VERSION=1.3.2 7 | 8 | ARG BUILD_ENVIRONMENT=dev 9 | ARG APP_HOME=/app 10 | 11 | ENV PYTHONUNBUFFERED 1 12 | ENV PYTHONDONTWRITEBYTECODE 1 13 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 14 | 15 | WORKDIR ${APP_HOME} 16 | 17 | RUN addgroup --system django \ 18 | && adduser --system --ingroup django django 19 | 20 | # Install required system dependencies 21 | RUN apt-get update && apt-get install --no-install-recommends -y \ 22 | # dependencies for building Python packages 23 | build-essential \ 24 | # psycopg2 dependencies 25 | libpq-dev \ 26 | # Translations dependencies 27 | gettext \ 28 | # Versatile image field & pillow \ 29 | libmagic1 \ 30 | libmagic-dev \ 31 | 32 | # cleaning up unused files 33 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 34 | && rm -rf /var/lib/apt/lists/* 35 | 36 | # Install Poetry 37 | RUN pip install --no-cache-dir poetry==${POETRY_VERSION} 38 | 39 | COPY poetry.lock pyproject.toml ${APP_HOME}/ 40 | 41 | # Project initialization: 42 | RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi 43 | 44 | COPY --chown=django:django ./compose/dev/django/celery/worker/start /start-celeryworker 45 | RUN chmod +x /start-celeryworker 46 | 47 | 48 | COPY --chown=django:django ./compose/dev/django/celery/beat/start /start-celerybeat 49 | RUN chmod +x /start-celerybeat 50 | 51 | 52 | COPY ./compose/dev/django/celery/flower/start /start-flower 53 | RUN chmod +x /start-flower 54 | 55 | COPY --chown=django:django . ${APP_HOME} 56 | 57 | # make django owner of the WORKDIR directory as well. 58 | RUN chown django:django ${APP_HOME} 59 | 60 | RUN python manage.py collectstatic --noinput 61 | 62 | EXPOSE 8000 63 | 64 | CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "wsgi:application"] 65 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/admin.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.contrib import admin 3 | from django.contrib.auth.admin import UserAdmin as AuthUserAdmin 4 | from django.contrib.auth.forms import UserChangeForm as DjangoUserChangeForm 5 | from django.contrib.auth.forms import UserCreationForm as DjangoUserCreationForm 6 | 7 | from .models import User 8 | 9 | 10 | # Forms 11 | # ---------------------------------------------------------------------------- 12 | class MyUserCreationForm(DjangoUserCreationForm): 13 | class Meta: 14 | model = User 15 | fields = ("email",) 16 | 17 | 18 | class MyUserChangeForm(DjangoUserChangeForm): 19 | class Meta: 20 | model = User 21 | fields = "__all__" 22 | 23 | 24 | # ModelAdmins 25 | # ---------------------------------------------------------------------------- 26 | @admin.register(User) 27 | class UserAdmin(AuthUserAdmin): 28 | add_form_template = "admin/auth/user/add_form.html" 29 | model = User 30 | fieldsets = ( 31 | (None, {"fields": ("email", "password")}), 32 | ("Personal info", {"fields": ("first_name", "last_name")}), 33 | ( 34 | "Permissions", 35 | { 36 | "fields": ( 37 | "is_active", 38 | "is_staff", 39 | "is_superuser", 40 | "groups", 41 | "user_permissions", 42 | ) 43 | }, 44 | ), 45 | ("Important dates", {"fields": ("last_login", "date_joined")}), 46 | ) 47 | add_fieldsets = ( 48 | (None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}), 49 | ) 50 | readonly_fields = ("date_joined", "last_login") 51 | form = MyUserChangeForm 52 | add_form = MyUserCreationForm 53 | list_display = ("email", "first_name", "last_name", "is_active") 54 | list_filter = ("is_superuser", "is_active") 55 | search_fields = ("first_name", "last_name", "email") 56 | ordering = ("email",) 57 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/api/errors.md: -------------------------------------------------------------------------------- 1 | # Errors 2 | 3 | ## Generic Errors 4 | 5 | The `status_code` in response header complies with the standard http response headers. We expect developers to make use of them to display appropriate error dialogs. 6 | 7 | For json requests, the API will return the error in the following format: 8 | 9 | ```json 10 | { 11 | "error_type": "WrongArguments", 12 | "errors": [ 13 | { 14 | "message": "Unable to login with provided credentials." 15 | } 16 | ] 17 | } 18 | ``` 19 | 20 | ## Validation Errors 21 | 22 | For `POST`, `PATCH`, `PUT` & `DELETE` methods, validation error may be raised. 23 | 24 | It will return (Status: `400 Bad Request`). 25 | 26 | The response body will contain json in the format same as Generic Error with `field` key. For example, if you try to register with `email` which already exists. The error response body will be of the format: 27 | 28 | ```json 29 | { 30 | "error_type": "ValidationError", 31 | "errors": [ 32 | { 33 | "field": "email", 34 | "message": "User with this email address already exists." 35 | }, 36 | { 37 | "field": "email", 38 | "message": "Some other error related to email." 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | For nested errors: 45 | 46 | The response body will contain json in the format same as Validation error with `errors` as key. For example: 47 | 48 | ```json 49 | { 50 | "error_type": "ValidationError", 51 | "errors": [ 52 | { 53 | "field": "profile", 54 | "message": null, 55 | "errors": [ 56 | { 57 | "field": "email", 58 | "message": "User with this email address already exists." 59 | }, 60 | { 61 | "field": "custom_data", 62 | "message": null, 63 | "errors": [ 64 | { 65 | "field": "age", 66 | "message": "Person too young. Age should be more than 18 years." 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | ``` 75 | 76 | __NOTE__: The copy for most of these error messages can be changed by backend developers. 77 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/local/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-buster 2 | 3 | # define an alias for the specfic python version used in this file. 4 | FROM python:${PYTHON_VERSION} as python 5 | 6 | ARG BUILD_ENVIRONMENT=local 7 | ARG APP_HOME=/app 8 | 9 | ENV POETRY_VERSION=1.3.2 10 | ENV PYTHONUNBUFFERED 1 11 | ENV PYTHONDONTWRITEBYTECODE 1 12 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 13 | 14 | WORKDIR ${APP_HOME} 15 | 16 | # Cache our requirements and only reinstall them when pyproject.toml or poetry.lock files change 17 | # COPY poetry.lock pyproject.toml ${APP_HOME} 18 | 19 | # Install required system dependencies 20 | RUN apt-get update && apt-get install --no-install-recommends -y \ 21 | # dependencies for building Python packages 22 | build-essential \ 23 | # psycopg2 dependencies 24 | libpq-dev \ 25 | # Translations dependencies 26 | gettext \ 27 | # Versatile image field & pillow \ 28 | libmagic1 \ 29 | libmagic-dev \ 30 | {% if cookiecutter.add_postgis.lower() == "y" %}# GDAL postgres requirements 31 | gdal-bin \ 32 | libgdal-dev \{% endif %} 33 | # cleaning up unused files 34 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 35 | && rm -rf /var/lib/apt/lists/* 36 | 37 | # Install Poetry 38 | RUN pip install poetry==${POETRY_VERSION} 39 | 40 | # Requirements are installed here to ensure they will be cached. 41 | COPY poetry.lock pyproject.toml ${APP_HOME}/ 42 | 43 | # Project initialization: 44 | RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi 45 | 46 | COPY ./compose/local/entrypoint /entrypoint 47 | RUN chmod +x /entrypoint 48 | 49 | COPY ./compose/local/start /start 50 | RUN chmod +x /start 51 | 52 | COPY ./compose/dev/django/celery/worker/start /start-celeryworker 53 | RUN chmod +x /start-celeryworker 54 | 55 | COPY ./compose/dev/django/celery/beat/start /start-celerybeat 56 | RUN chmod +x /start-celerybeat 57 | 58 | COPY ./compose/dev/django/celery/flower/start /start-flower 59 | RUN chmod +x /start-flower 60 | 61 | # copy application code to WORKDIR 62 | COPY . ${APP_HOME} 63 | 64 | ENTRYPOINT ["/entrypoint"] 65 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/tests/graphql/test_users_list_api.py: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | import json 3 | 4 | # Third Party Stuff 5 | import pytest 6 | 7 | from tests import factories as f 8 | 9 | pytestmark = pytest.mark.django_db 10 | 11 | 12 | def test_get_current_user_api(client): 13 | graphql_query = """ 14 | query users($first: Int = 1, $after: String = ""){ 15 | users(first: $first, after: $after){ 16 | totalCount, 17 | edgeCount, 18 | edges { 19 | node { 20 | id, 21 | firstName, 22 | lastName 23 | } 24 | cursor 25 | }, 26 | pageInfo{ 27 | startCursor, 28 | endCursor, 29 | hasNextPage, 30 | hasPreviousPage 31 | } 32 | } 33 | } 34 | """ 35 | 36 | user = f.create_user(email="test@example.com") 37 | f.create_user(email="test2@example.com") 38 | 39 | # should return an error without auth 40 | response = client.post_graphql(graphql_query) 41 | assert response.status_code == 200 42 | response_data = json.loads(response.content) 43 | assert "errors" in response_data.keys() 44 | 45 | client.login(user) 46 | 47 | # should return permission issue error 48 | response = client.post_graphql(graphql_query) 49 | response_data = json.loads(response.content) 50 | assert "errors" in response_data.keys() 51 | assert ( 52 | "You do not have permission to perform this action." 53 | == response_data["errors"][0]["message"] 54 | ) 55 | 56 | user.is_superuser = True 57 | user.save() 58 | 59 | # should return user list 60 | response = client.post_graphql(graphql_query) 61 | response_data = json.loads(response.content) 62 | assert response.status_code == 200 63 | data = response_data["data"] 64 | assert data["users"]["totalCount"] == 2 65 | assert data["users"]["edgeCount"] == 1 66 | assert data["users"]["pageInfo"]["hasNextPage"] is True 67 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/postgresql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | - name: Add postgreSQL repository apt-key 3 | apt_key: 4 | id: "ACCC4CF8" 5 | url: "https://www.postgresql.org/media/keys/ACCC4CF8.asc" 6 | state: present 7 | 8 | - name: Add postgreSQL repository 9 | apt_repository: 10 | repo: "deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main {{ postgresql_version }}" 11 | state: present 12 | update_cache: yes 13 | 14 | - name: Install postgres packages for Ubuntu 15 | apt: 16 | pkg: 17 | - "postgresql-{{ postgresql_version }}" 18 | - "postgresql-client-{{ postgresql_version }}" 19 | - "postgresql-contrib-{{ postgresql_version }}" 20 | - "python3-psycopg2" 21 | state: present 22 | cache_valid_time: 300 23 | 24 | - name: Install postgis packages for Ubuntu 25 | apt: 26 | pkg: 27 | - "postgresql-{{ postgresql_version }}-postgis-{{ postgis_version }}" 28 | - gdal-bin 29 | state: present 30 | cache_valid_time: 300 31 | when: pg_gis 32 | 33 | - name: setup database 34 | become_user: postgres 35 | postgresql_db: 36 | db: "{{ pg_db }}" 37 | encoding: 'UTF-8' 38 | lc_collate: 'en_US.UTF-8' 39 | lc_ctype: 'en_US.UTF-8' 40 | template: 'template0' 41 | state: present 42 | 43 | - name: add hstore 44 | become_user: postgres 45 | shell: psql {{ pg_db }} -c "CREATE EXTENSION IF NOT EXISTS hstore;" 46 | when: pg_hstore 47 | 48 | - name: add postgis 49 | become_user: postgres 50 | shell: psql {{ pg_db }} -c "CREATE EXTENSION IF NOT EXISTS postgis;" 51 | when: pg_gis 52 | 53 | - name: add postgis topology 54 | become_user: postgres 55 | shell: psql {{ pg_db }} -c "CREATE EXTENSION IF NOT EXISTS postgis_topology;" 56 | when: pg_gis 57 | 58 | - name: grant access for database to user 59 | become_user: postgres 60 | postgresql_user: 61 | db: "{{ pg_db }}" 62 | user: "{{ pg_user }}" 63 | password: "{{ pg_password }}" 64 | role_attr_flags: "CREATEDB,SUPERUSER" 65 | state: present 66 | encrypted: yes 67 | 68 | - name: ensure postgres is running 69 | service: name=postgresql state=started enabled=yes{% endraw %} 70 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/tasks/uwsgi-setup.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | - name: apt_get install uwsgi packages 3 | apt: 4 | pkg: ['uwsgi-emperor', 'uwsgi', 'uwsgi-src', 'uuid-dev', 'libcap-dev', 'libpcre3-dev'] 5 | state: present 6 | tags: ['configure'] 7 | 8 | # compile python36 plugin for uwsgi 9 | # https://dev.to/pauloxnet/how-to-use-uwsgi-with-ptyhon36-in-ubuntu 10 | - name: python3 plugin | create build dir 11 | file: path=/usr/src/uwsgi/plugins/python state=directory owner={{user}} group={{uwsgi_group}} recurse=yes 12 | tags: ['configure'] 13 | 14 | - name: Compile python{{ python_version }} plugin for uwsgi 15 | shell: > 16 | PYTHON=python{{ python_version }} uwsgi --build-plugin "/usr/src/uwsgi/plugins/python python{{python_version | replace(".", "")}}" && 17 | mv python{{python_version|replace(".", "")}}_plugin.so /usr/lib/uwsgi/plugins/python{{python_version|replace(".", "")}}_plugin.so && 18 | chmod 644 /usr/lib/uwsgi/plugins/python{{python_version|replace(".", "")}}_plugin.so 19 | 20 | 21 | - name: make sure project directory is owned by uwsgi group 22 | file: path={{ project_path }} state=directory owner={{user}} group={{uwsgi_group}} recurse=yes 23 | tags: ['configure'] 24 | 25 | - name: copy django-uwsgi logrotate 26 | template: src=django.logrotate.j2 27 | dest=/etc/logrotate.d/uwsgi-{{ project_namespace }} 28 | mode=644 29 | tags: ['configure'] 30 | 31 | - name: make sure uwsgi log directory exists 32 | file: path={{ uwsgi_log_dir }} state=directory owner={{uwsgi_user}} group={{uwsgi_group}} mode=751 recurse=yes 33 | tags: ['configure'] 34 | 35 | - name: update uwsgi-emperor init file 36 | template: src=uwsgi-emperor-init.d.j2 37 | dest=/etc/init.d/uwsgi-emperor 38 | mode=755 39 | register: uwsgiconf 40 | 41 | - name: ensure uwsgi emperor pid file as correct permissions 42 | file: 43 | path: '{{ uwsgi_emperor_pid_file }}' 44 | mode: '0644' 45 | 46 | - name: ensure django app config is added as uwsgi vassal 47 | template: src=django.uwsgi.ini.j2 48 | dest={{ uwsgi_conf_path }}/{{project_namespace }}.ini 49 | mode=644 50 | tags: ['deploy'] 51 | register: uwsgiconf 52 | {% endraw %} 53 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/defaults/main.yml: -------------------------------------------------------------------------------- 1 | ngnix_listen: '443 http2 ssl' 2 | nginx_user: 'www-data' 3 | nginx_worker_processes: auto 4 | nginx_worker_connections: 1024 5 | nginx_client_max_body_size: '10M' 6 | 7 | {% raw %} 8 | nginx_conf_file_name: '{{ project_namespace }}' 9 | 10 | # SSL common configurations 11 | ssl_cert_dir: '/etc/nginx/ssl' 12 | ssl_forward_secrecy_key_path: /etc/nginx/dhparam.pem 13 | ssl_forward_secrecy_key_length: 2048 14 | letsencrypt_ssl_cert_dir: '/etc/letsencrypt/live/{{ domain_name }}' 15 | letsencrypt_challenge_root: '/app/certbot' 16 | letsencrypt_email: 'webmaster@{{ domain_name }}' 17 | ssl_certificate: '{{ ssl_cert_dir }}/{{ domain_name }}/fullchain.pem' 18 | ssl_certificate_key: '{{ ssl_cert_dir }}/{{ domain_name }}/privkey.pem' 19 | ssl_dhparam: '{{ ssl_forward_secrecy_key_path }}' 20 | 21 | # Oldest compatible clients: Firefox 1, Chrome 1, IE 7, Opera 5, Safari 1, Windows XP IE8, Android 2.3, Java 7 22 | ssl_protocols: 'TLSv1 TLSv1.1 TLSv1.2' 23 | ssl_ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS' 24 | ssl_session_timeout: '1d' 25 | ssl_session_cache: 'shared:SSL:50m' 26 | ssl_trusted_certificate: '{{ ssl_cert_dir }}/{{ domain_name }}/chain.pem' 27 | access_log_file: '/var/log/nginx/{{ domain_name }}.access.log' 28 | error_log_file: '/var/log/nginx/{{ domain_name }}.error.log' 29 | 30 | # htpasswsd 31 | # docs username, password 32 | htpasswd_file_path: '/etc/nginx/.htpasswd-{{ project_namespace }}'{% endraw %} 33 | nginx_docs_username: '{{ cookiecutter.main_module|truncate(3, True, '') }}' 34 | nginx_docs_password: '{{ cookiecutter.main_module|title|truncate(3, True, '') }}1234!' 35 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/compose/dev/django/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-buster 2 | 3 | # define an alias for the specfic python version used in this file. 4 | FROM python:${PYTHON_VERSION} as python 5 | 6 | ENV POETRY_VERSION=1.3.2 7 | 8 | ARG BUILD_ENVIRONMENT=dev 9 | ARG APP_HOME=/app 10 | 11 | ENV PYTHONUNBUFFERED 1 12 | ENV PYTHONDONTWRITEBYTECODE 1 13 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 14 | 15 | WORKDIR ${APP_HOME} 16 | 17 | RUN addgroup --system django \ 18 | && adduser --system --ingroup django django 19 | 20 | # Install required system dependencies 21 | RUN apt-get update && apt-get install --no-install-recommends -y \ 22 | # dependencies for building Python packages 23 | build-essential \ 24 | # psycopg2 dependencies 25 | libpq-dev \ 26 | # Translations dependencies 27 | gettext \ 28 | # Versatile image field & pillow \ 29 | libmagic1 \ 30 | libmagic-dev \ 31 | {% if cookiecutter.add_postgis.lower() == "y" %}# GDAL postgres requirements 32 | gdal-bin \ 33 | libgdal-dev \{% endif %} 34 | # cleaning up unused files 35 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 36 | && rm -rf /var/lib/apt/lists/* 37 | 38 | # Install Poetry 39 | RUN pip install --no-cache-dir poetry==${POETRY_VERSION} 40 | 41 | COPY poetry.lock pyproject.toml ${APP_HOME}/ 42 | 43 | # Project initialization: 44 | RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi 45 | 46 | COPY --chown=django:django ./compose/dev/django/entrypoint /entrypoint 47 | RUN chmod +x /entrypoint 48 | 49 | 50 | COPY --chown=django:django ./compose/dev/django/start /start 51 | RUN chmod +x /start 52 | 53 | 54 | COPY --chown=django:django ./compose/dev/django/celery/worker/start /start-celeryworker 55 | RUN chmod +x /start-celeryworker 56 | 57 | 58 | COPY --chown=django:django ./compose/dev/django/celery/beat/start /start-celerybeat 59 | RUN chmod +x /start-celerybeat 60 | 61 | 62 | COPY ./compose/dev/django/celery/flower/start /start-flower 63 | RUN chmod +x /start-flower 64 | COPY --chown=django:django . ${APP_HOME} 65 | 66 | # make django owner of the WORKDIR directory as well. 67 | RUN chown django:django ${APP_HOME} 68 | 69 | USER django 70 | 71 | ENTRYPOINT ["/entrypoint"] 72 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/graphql/utils.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django.core.exceptions import ValidationError 3 | from graphene import relay 4 | from graphql.error import GraphQLError 5 | from graphql_relay import from_global_id 6 | 7 | 8 | def filter_objects(object_name, relay_ids, otherwise=None): 9 | if not isinstance(relay_ids, list): 10 | relay_ids = [relay_ids] 11 | try: 12 | object_ids = [from_global_id(relay_id)[1] for relay_id in relay_ids] 13 | return object_name.objects.filter(id__in=object_ids) 14 | except: # noqa 15 | return otherwise 16 | 17 | 18 | class CountableConnectionBase(relay.Connection): 19 | """ 20 | Extend connection class to display 21 | total count and edges count in paginated results 22 | """ 23 | 24 | class Meta: 25 | abstract = True 26 | 27 | total_count = graphene.Int() 28 | edge_count = graphene.Int() 29 | 30 | @classmethod 31 | def resolve_total_count(cls, root, info, **kwargs): 32 | return root.length 33 | 34 | @classmethod 35 | def resolve_edge_count(cls, root, info, **kwargs): 36 | return len(root.edges) 37 | 38 | 39 | def validate_one_of_args_is_in_query(*args): 40 | # split args into a list with 2-element tuples: 41 | # [(arg1_name, arg1_value), (arg2_name, arg2_value), ...] 42 | splitted_args = [args[i : i + 2] for i in range(0, len(args), 2)] # noqa: E203 43 | # filter trueish values from each tuple 44 | filter_args = list(filter(lambda item: bool(item[1]) is True, splitted_args)) 45 | 46 | if len(filter_args) > 1: 47 | rest_args = ", ".join([f"'{item[0]}'" for item in filter_args[1:]]) 48 | raise GraphQLError( 49 | f"Argument '{filter_args[0][0]}' cannot be combined with {rest_args}" 50 | ) 51 | 52 | if not filter_args: 53 | required_args = ", ".join([f"'{item[0]}'" for item in splitted_args]) 54 | raise GraphQLError(f"At least one of arguments is required: {required_args}.") 55 | 56 | 57 | def validate_one_of_args_is_in_mutation(error_class, *args): 58 | try: 59 | validate_one_of_args_is_in_query(*args) 60 | except GraphQLError as e: 61 | raise ValidationError(str(e), code=error_class.GRAPHQL_ERROR) 62 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/templates/base.html: -------------------------------------------------------------------------------- 1 | {%- if cookiecutter.add_sentry.lower() == 'y' %} 2 | {%- raw %}{% load raven %}{% endraw %} 3 | {%- endif %} 4 | {% raw %}{% load static i18n %} 5 | {% get_current_language as LANGUAGE_CODE %} 6 | 7 | 8 | 9 | 10 | {% block title %}{% endraw %}{{ cookiecutter.project_name }}{% raw %}{% endblock title %} 11 | 12 | 13 | 14 | {% endraw %}{%- if cookiecutter.add_sentry.lower() == 'y' %} 15 | {% raw %}{% if site_info.IS_RAVEN_INSTALLED %} 16 | 17 | {% endif %}{% endraw %} 18 | {%- endif %}{% raw %} 19 | {% block css %} 20 | 21 | 22 | {% endblock css %} 23 | {% block head_extras %}{% endblock %} 24 | 25 | 26 | 27 | 30 | 31 | {% block header %} 32 |

{% endraw %}{{ cookiecutter.project_name }}{% raw %}

33 | {% endblock header %} 34 | 35 | {% block content %} 36 |

Coming soon!

37 | {% endblock content %} 38 | 39 | {% block footer %} {% endblock footer %} 40 | 41 | 43 | {# Placed at the end of the document so the pages load faster #} 44 | {% block js %} 45 | 46 | 47 | {% endblock js %} 48 | 49 | {% endraw %} 50 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.envs/.local/.env.sample: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | 6 | # Redis 7 | # ------------------------------------------------------------------------------ 8 | REDIS_URL=redis://redis:6379/0 9 | 10 | # Celery 11 | # ------------------------------------------------------------------------------ 12 | 13 | # Flower 14 | CELERY_FLOWER_USER=dev 15 | CELERY_FLOWER_PASSWORD="{{ cookiecutter.main_module|title|truncate(3, True, '') }}1234!" 16 | 17 | # PostgreSQL 18 | # ------------------------------------------------------------------------------ 19 | POSTGRES_HOST=postgres 20 | POSTGRES_PORT=5432 21 | POSTGRES_DB={{ cookiecutter.main_module }} 22 | POSTGRES_USER=dev 23 | POSTGRES_PASSWORD=password 24 | 25 | # Core Stuff 26 | # ============================== 27 | # DJANGO_SETTINGS_MODULE=settings.production 28 | # DJANGO_DEBUG=False 29 | # DJANGO_ADMIN_URL='admin' 30 | # DJANGO_SECRET_KEY='change_with_50+_long_random_string' 31 | # DJANGO_ADMINS='Admin 1, Admin2 ' 32 | 33 | # SITE_SCHEME=https 34 | # SITE_DOMAIN=youdomain.com 35 | # SITE_NAME='your site name' 36 | # ALLOWED_HOSTS=youdomain.com 37 | # DJANGO_SECURE_HSTS_SECONDS=60 38 | # FRONTEND_SITE_DOMAIN='frontendapp.com' 39 | # FRONTEND_SITE_SCHEME='https' 40 | 41 | # File Storage 42 | # ============================== 43 | # ENABLE_MEDIA_UPLOAD_TO_S3=False 44 | # DJANGO_AWS_ACCESS_KEY_ID='' 45 | # DJANGO_AWS_SECRET_ACCESS_KEY='' 46 | # DJANGO_AWS_STORAGE_BUCKET_NAME='' 47 | # DJANGO_AWS_S3_HOST='' 48 | # DJANGO_AWS_S3_REGION_NAME='' 49 | 50 | # Django Rest Framework 51 | # ============================== 52 | # API_DEBUG=False 53 | 54 | # Emails 55 | # ============================== 56 | # DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 57 | # DEFAULT_FROM_EMAIL='' 58 | # EMAIL_HOST='' 59 | # EMAIL_HOST_PASSWORD='' 60 | # EMAIL_HOST_USER='' 61 | # EMAIL_PORT=587 62 | # EMAIL_SUBJECT_PREFIX='' 63 | # EMAIL_USE_TLS='' 64 | 65 | # CORS 66 | # ============================== 67 | # CORS_ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:9000,https://example.com 68 | 69 | # SENTRY 70 | # ============================== 71 | # SENTRY_DSN='' 72 | # SENTRY_ENVIRONMENT=production 73 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/graphql/0-overview.md: -------------------------------------------------------------------------------- 1 | # GraphQL 2 | 3 | ## Authentication 4 | 5 | For all auth related requests (`login`, `register` etc), clients need to refer to [docs mentioned here](1-auth.md). 6 | For clients to make authenticated requests, the `auth_token` value (received from the `login` endpoint) should be included in the `Authorization` HTTP header. The value should be prefixed by the string literal `Bearer`, with whitespace separating the two strings. 7 | 8 | ## API Endpoint 9 | 10 | ``` 11 | POST /graphql 12 | ``` 13 | 14 | All the queries and mutations will be a POST request to the above endpoint. We've documented a sample header and payload to be sent with the request. 15 | 16 | __Headers__ 17 | 18 | ```json 19 | { 20 | "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2F1dGhlbnRpY2F0aW9uX2lkIjoiNzY1MjE3YTgtNzU5OS00ZTI1LTljMjQtYjdjOTJlODc4MjAxIn0.972Irua8Ql0NRf_KxgYI7q1imPBkf2XJG25L94JM8Hw" 21 | } 22 | ``` 23 | 24 | __Payload__ 25 | 26 | ```json 27 | { 28 | "query": "query MyInfo { me { id firstName lastName email } }", 29 | "variables":null, 30 | "operationName":"MyInfo" 31 | } 32 | ``` 33 | 34 | ## Pagination 35 | 36 | Pagination is required in most queries that return lists of items in the GraphQL API. It limits the number of results returned by the server to a more manageable size and avoids data flow disruptions. 37 | 38 | There are two types of lists in GraphQL: 39 | 40 | - `[Foo]` is a simple list. It is used to query a list containing several items. An excellent example of a simple list could be a query for product variants which returns a list with a manageable number of results. 41 | - `FooConnection` represents a more complex list. When queried, it will return an unknown or large number of results. 42 | 43 | Pagination is used to help you handle large amounts of items returned by the connection list type. 44 | 45 | The pagination model is based on the [GraphQL Connection Specification](https://relay.dev/graphql/connections.htm). Its schema looks like this: 46 | 47 | ``` 48 | type FooConnection { 49 | pageInfo: PageInfo! 50 | edges: [FooEdge!]! 51 | } 52 | 53 | type PageInfo { 54 | hasNextPage: Boolean! 55 | hasPreviousPage: Boolean! 56 | startCursor: String 57 | endCursor: String 58 | } 59 | 60 | type FooEdge { 61 | node: Foo! 62 | cursor: String! 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # See: https://github.com/mkdocs/mkdocs/blob/master/docs/user-guide/configuration.md 2 | site_name: {{ cookiecutter.project_name }} 3 | site_description: {{ cookiecutter.project_description }} 4 | site_author: {{ cookiecutter.github_username }} 5 | repo_url: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.github_repository }} 6 | 7 | nav: 8 | - Introduction: index.md 9 | {%- if cookiecutter.add_graphql == "y" %} 10 | - Graphql API: 11 | - Overview: graphql/0-overview.md 12 | - Authentication: graphql/1-auth.md 13 | - Current User: graphql/2-users.md 14 | - Errors: graphql/errors.md 15 | - Error Handling: graphql/errors_handling.md 16 | {%- endif %} 17 | - REST API: 18 | - Overview: api/0-overview.md 19 | - Authentication: api/1-auth.md 20 | - Current User: api/2-current-user.md 21 | - Errors: api/errors.md 22 | - Technical Specs: 23 | - Server Config: backend/server_config.md 24 | - Coding Rules: backend/coding_rules.md 25 | - Database Schema: backend/database-schema.svg 26 | - Releases: 27 | - Changelog: releases.md 28 | 29 | theme: 30 | name: 'material' 31 | features: 32 | - tabs 33 | - instant 34 | 35 | 36 | plugins: 37 | - git-revision-date-localized 38 | - search: 39 | prebuild_index: true 40 | 41 | 42 | copyright: '' 43 | 44 | # directory to output HTML build 45 | site_dir: _docs_html 46 | 47 | # Determines if a broken link to a page within the documentation is considered 48 | # a warning or an error (link to a page not listed in the pages setting). Set to 49 | # true to halt processing when a broken link is found, false prints a warning. 50 | strict: true 51 | 52 | # Determines the address used when running mkdocs serve. 53 | # Set it to 8001, so that it doesn't conflict with django's 8000 port 54 | dev_addr: '127.0.0.1:8001' 55 | 56 | markdown_extensions: 57 | - markdown.extensions.meta 58 | - markdown.extensions.admonition 59 | - markdown.extensions.smarty 60 | - markdown.extensions.sane_lists 61 | - markdown.extensions.codehilite: 62 | guess_lang: false 63 | - markdown.extensions.toc: 64 | permalink: true 65 | - pymdownx.superfences: 66 | preserve_tabs: true 67 | - pymdownx.inlinehilite 68 | - pymdownx.smartsymbols 69 | - pymdownx.mark 70 | - pymdownx.magiclink 71 | - pymdownx.smartsymbols 72 | - markdown_include.include: 73 | base_path: docs 74 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/.envs/.dev/.env.sample: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | 6 | # Redis 7 | # ------------------------------------------------------------------------------ 8 | REDIS_URL=redis://redis:6379/0 9 | 10 | # Celery 11 | # ------------------------------------------------------------------------------ 12 | 13 | # Flower 14 | CELERY_FLOWER_USER=dev 15 | CELERY_FLOWER_PASSWORD="{{ cookiecutter.main_module|title|truncate(3, True, '') }}1234!" 16 | 17 | 18 | # Databases 19 | # ============================== 20 | # DATABASE_URL=postgres://username:password@yourhost.com:5432/database_name 21 | 22 | # PostgreSQL 23 | # ------------------------------------------------------------------------------ 24 | POSTGRES_HOST=postgres 25 | POSTGRES_PORT=5432 26 | POSTGRES_DB={{ cookiecutter.main_module }} 27 | POSTGRES_USER=dev 28 | POSTGRES_PASSWORD=password 29 | 30 | # Core Stuff 31 | # ============================== 32 | # DJANGO_SETTINGS_MODULE=settings.production 33 | # DJANGO_DEBUG=False 34 | # DJANGO_ADMIN_URL='admin' 35 | # DJANGO_SECRET_KEY='change_with_50+_long_random_string' 36 | # DJANGO_ADMINS='Admin 1, Admin2 ' 37 | 38 | # SITE_SCHEME=https 39 | # SITE_DOMAIN=youdomain.com 40 | # SITE_NAME='your site name' 41 | # ALLOWED_HOSTS=youdomain.com 42 | # DJANGO_SECURE_HSTS_SECONDS=60 43 | # FRONTEND_SITE_DOMAIN='frontendapp.com' 44 | # FRONTEND_SITE_SCHEME='https' 45 | 46 | # File Storage 47 | # ============================== 48 | # ENABLE_MEDIA_UPLOAD_TO_S3=False 49 | # DJANGO_AWS_ACCESS_KEY_ID='' 50 | # DJANGO_AWS_SECRET_ACCESS_KEY='' 51 | # DJANGO_AWS_STORAGE_BUCKET_NAME='' 52 | # DJANGO_AWS_S3_HOST='' 53 | # DJANGO_AWS_S3_REGION_NAME='' 54 | 55 | # Django Rest Framework 56 | # ============================== 57 | # API_DEBUG=False 58 | 59 | # Emails 60 | # ============================== 61 | # DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 62 | # DEFAULT_FROM_EMAIL='' 63 | # EMAIL_HOST='' 64 | # EMAIL_HOST_PASSWORD='' 65 | # EMAIL_HOST_USER='' 66 | # EMAIL_PORT=587 67 | # EMAIL_SUBJECT_PREFIX='' 68 | # EMAIL_USE_TLS='' 69 | 70 | # CORS 71 | # ============================== 72 | # CORS_ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:9000,https://example.com 73 | 74 | # SENTRY 75 | # ============================== 76 | # SENTRY_DSN='' 77 | # SENTRY_ENVIRONMENT=production 78 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | 3 | # Uncomment following if ipv6 is not available or not tunelled through ipv4 properly 4 | # Disable ipv6 when running in VM(vagrant) 5 | #- name: Disable ipv6 for all interfaces 6 | # sysctl: name="net.ipv6.conf.all.disable_ipv6" value=1 state=present 7 | # when: vm == 1 8 | # 9 | #- name: Disable ipv6 for default interface 10 | # sysctl: name="net.ipv6.conf.default.disable_ipv6" value=1 state=present 11 | # when: vm == 1 12 | # 13 | #- name: Disable ipv6 for local interface 14 | # sysctl: name="net.ipv6.conf.lo.disable_ipv6" value=1 state=present 15 | # when: vm == 1 16 | - name: Set hostname 17 | action: shell hostnamectl set-hostname {{ domain_name }} 18 | when: vm == 0 19 | 20 | - name: set system locale 21 | command: update-locale LC_ALL={{ lc_all }} LANG={{ lc_lang }} LC_CTYPE={{ lc_ctype }} LC_COLLATE={{ lc_collate }} 22 | 23 | - name: set locale file 24 | become: yes 25 | template: src=locale.j2 dest=/etc/default/locale 26 | 27 | - name: set timezone 28 | file: src=/usr/share/zoneinfo/{{ common_timezone }} 29 | dest=/etc/localtime 30 | force=yes 31 | state=link 32 | 33 | - name: 'Add python3.9 (ppa:deadsnakes/ppa) repository' 34 | apt_repository: 35 | repo: 'ppa:deadsnakes/ppa' 36 | update_cache: yes 37 | 38 | - name: update apt cache 39 | shell: apt update 40 | become: yes 41 | register: apt_updated 42 | 43 | - name: ensure a default bashrc and inputrc file 44 | copy: src={{ item.src }} dest=/home/{{ user }}/{{ item.dest }} owner={{ user }} group={{ user }} 45 | with_items: 46 | - { src: 'bashrc', dest: '.bashrc' } 47 | - { src: 'inputrc', dest: '.inputrc' } 48 | 49 | - name: ensure .pip directory exists 50 | file: 51 | path: /home/{{ user }}/.pip 52 | state: directory 53 | owner: "{{ user }}" 54 | group: "{{ user }}" 55 | mode: 0775 56 | 57 | - name: ensure default pypi mirror for easy_install is pypi.python.org 58 | copy: src={{ item.src }} dest=/home/{{ user }}/{{ item.dest }} owner={{ user }} group={{ user }} 59 | with_items: 60 | - { src: 'pydistutils.cfg', dest: '.pydistutils.cfg' } 61 | - { src: 'pip.conf', dest: '.pip/pip.conf' } 62 | 63 | - name: apt_get install common packages 64 | apt: pkg={{base_ubuntu.common.apt_packages}} state=present 65 | 66 | - name: ensure there is a group www-data 67 | group: name=www-data state=present system=yes{% endraw %} 68 | 69 | - name: ensure there is a user www-data 70 | user: name=www-data group=www-data system=yes 71 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/api/mixins.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | 5 | class MultipleSerializerMixin(object): 6 | def get_serializer_class(self): 7 | """ 8 | Look for serializer class in self.serializer_classes, which 9 | should be a dict mapping action name (key) to serializer class (value), 10 | i.e.: 11 | 12 | class MyViewSet(MultipleSerializerMixin, ViewSet): 13 | serializer_class = MyDefaultSerializer 14 | serializer_classes = { 15 | 'list': MyListSerializer, 16 | 'my_action': MyActionSerializer, 17 | } 18 | 19 | @list_route 20 | def my_action: 21 | ... 22 | 23 | If there's no serializer available for that action than 24 | the default serializer class will be returned as fallback. 25 | """ 26 | if not isinstance(self.serializer_classes, dict): 27 | raise ImproperlyConfigured("serializer_classes should be a dict mapping.") 28 | 29 | if self.action in self.serializer_classes.keys(): 30 | return self.serializer_classes[self.action] 31 | return super().get_serializer_class() 32 | 33 | 34 | class PermissionPerActionMixin: 35 | """ 36 | Look for permission classes in self.permissions_per_action, which 37 | should be a dict mapping action name (key) to permission classes (value), 38 | i.e.: 39 | 40 | class MyViewSet(PermissionPerActionMixin, ViewSet): 41 | permission_classes = [permissions.IsAuthenticated] 42 | permissions_per_action = { 43 | 'list': [DRFPermissions | CustomPermissions], 44 | 'my_action': [DRFPermissions | CustomPermissions], 45 | } 46 | 47 | @list_route 48 | def my_action: 49 | ... 50 | 51 | If there are no permission classes available for that action than 52 | the default permission classes will be returned as fallback. 53 | """ 54 | 55 | attr_name = "permissions_per_action" 56 | 57 | def get_permissions(self): 58 | action_permissions = getattr(self, self.attr_name) 59 | 60 | if not isinstance(action_permissions, dict): 61 | raise ImproperlyConfigured(f"{self.attr_name} should be a dict mapping.") 62 | 63 | if self.action in action_permissions.keys(): 64 | self.permission_classes = action_permissions[self.action] 65 | 66 | return super().get_permissions() 67 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {%raw%}--- 2 | - name: Add official Nginx repo 3 | apt_repository: 4 | repo: 'ppa:nginx/stable' 5 | update_cache: yes 6 | 7 | - name: install NGINX Server. 8 | apt: pkg=nginx-full state=present 9 | 10 | - name: make sure ssl directory exists 11 | file: path={{ ssl_cert_dir }} state=directory 12 | when: vm == 0 and use_letsencrypt 13 | 14 | - name: check {{ letsencrypt_ssl_cert_dir }} exists 15 | stat: path={{ letsencrypt_ssl_cert_dir }} 16 | register: letsencrypt_dir 17 | 18 | - import_tasks: htpasswd.yml 19 | 20 | - import_tasks: letsencrypt.yml 21 | when: vm == 0 and use_letsencrypt and letsencrypt_dir.stat.exists == false 22 | 23 | - name: check ssl/nginx.crt exists 24 | stat: path={{ ssl_certificate }} 25 | register: nginx_cert 26 | 27 | - name: check ssl/nginx.key exists 28 | stat: path={{ ssl_certificate_key }} 29 | register: nginx_key 30 | 31 | - fail: msg="Whoops! ssl certificate doesn't exist" 32 | when: (vm == 0 and use_letsencrypt) == true and (nginx_cert.stat.exists == false or nginx_key.stat.exists == false) 33 | 34 | - name: generate ssl forward secrecy key 35 | command: openssl dhparam -out {{ ssl_forward_secrecy_key_path }} {{ ssl_forward_secrecy_key_length }} creates={{ ssl_forward_secrecy_key_path }} 36 | when: vm == 0 37 | 38 | - name: copy base nginx configuration. 39 | template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf 40 | notify: reload nginx 41 | 42 | - name: delete default vhost. 43 | action: file path=/etc/nginx/sites-enabled/default state=absent 44 | notify: reload nginx 45 | 46 | - name: copy site host port 80 configuration. 47 | template: src=site.80.conf.j2 dest=/etc/nginx/sites-available/{{ nginx_conf_file_name }}.80.conf 48 | notify: reload nginx 49 | 50 | - name: copy site host port 443 configuration. 51 | template: src=site.443.conf.j2 dest=/etc/nginx/sites-available/{{ nginx_conf_file_name }}.443.conf 52 | when: nginx_cert.stat.exists == true and nginx_key.stat.exists == true 53 | notify: reload nginx 54 | 55 | - name: put host port 80 configuration in sites-enabled 56 | file: src=/etc/nginx/sites-available/{{ nginx_conf_file_name }}.80.conf dest=/etc/nginx/sites-enabled/{{ nginx_conf_file_name }}.80 state=link 57 | notify: reload nginx 58 | 59 | - name: put host port 443 configuration in sites-enabled 60 | file: src=/etc/nginx/sites-available/{{ nginx_conf_file_name }}.443.conf dest=/etc/nginx/sites-enabled/{{ nginx_conf_file_name }}.443 state=link 61 | when: nginx_cert.stat.exists == true and nginx_key.stat.exists == true 62 | notify: reload nginx 63 | 64 | - name: ensure nginx is running 65 | service: name=nginx state=started{%endraw%} 66 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/tokens.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | import jwt 3 | from django.conf import settings 4 | from django.contrib.auth import get_user_model 5 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 6 | 7 | # {{ cookiecutter.main_module }} Stuff 8 | from {{cookiecutter.main_module}}.base import exceptions as exc 9 | 10 | from .utils import decode_uuid_from_base64, encode_uuid_to_base64 11 | 12 | HS256_ALGORITHM = "HS256" 13 | 14 | 15 | def get_token_for_user(user, scope: str) -> str: 16 | """Generate a new signed token containing a specified user limited for a scope (identified as a string).""" 17 | data = {"user_%s_id" % (scope): str(user.id)} 18 | return jwt.encode(data, settings.SECRET_KEY, algorithm=HS256_ALGORITHM) 19 | 20 | 21 | def get_user_for_token(token: str, scope: str): 22 | """ 23 | Given a selfcontained token and a scope try to parse and 24 | unsign it. 25 | 26 | If max_age is specified it checks token expiration. 27 | 28 | If token passes a validation, returns 29 | a user instance corresponding with user_id stored 30 | in the incoming token. 31 | """ 32 | try: 33 | data = jwt.decode(token, settings.SECRET_KEY, algorithms=[HS256_ALGORITHM]) 34 | except jwt.DecodeError: 35 | raise exc.NotAuthenticated("Invalid token") 36 | 37 | model_cls = get_user_model() 38 | 39 | try: 40 | user = model_cls.objects.get(pk=data["user_%s_id" % (scope)]) 41 | except (model_cls.DoesNotExist, KeyError): 42 | raise exc.NotAuthenticated("Invalid token") 43 | else: 44 | return user 45 | 46 | 47 | def get_token_for_password_reset(user): 48 | return "{}::{}".format( 49 | encode_uuid_to_base64(user.pk), PasswordResetTokenGenerator().make_token(user) 50 | ) 51 | 52 | 53 | def get_user_for_password_reset_token(token): 54 | default_error_messages = { 55 | "invalid_token": "Invalid token or the token has expired", 56 | "user_not_found": "No user exists for given token", 57 | } 58 | try: 59 | uidb64, reset_token = token.split("::") 60 | except ValueError: 61 | raise exc.RequestValidationError(default_error_messages["invalid_token"]) 62 | 63 | user_id = decode_uuid_from_base64(uidb64) 64 | if not user_id: 65 | raise exc.RequestValidationError(default_error_messages["invalid_token"]) 66 | 67 | user = get_user_model().objects.filter(id=user_id).first() 68 | 69 | if not user: 70 | raise exc.RequestValidationError(default_error_messages["user_not_found"]) 71 | 72 | if not PasswordResetTokenGenerator().check_token(user, reset_token): 73 | raise exc.RequestValidationError(default_error_messages["invalid_token"]) 74 | 75 | return user 76 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/graphql/1-auth.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | !!!info 4 | For API overview and usages, check out [this page](0-overview.md). 5 | 6 | 7 | ## Register 8 | 9 | __Request__ 10 | ``` 11 | mutation { 12 | signup ( 13 | input: { 14 | email: "test@example.com", 15 | firstName: "a", 16 | lastName: "b", 17 | password: "password" 18 | } 19 | ) { 20 | user { 21 | id 22 | email 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | __Response__ 29 | ```json 30 | { 31 | "data": { 32 | "signup": { 33 | "user": { 34 | "id": "f1b234c8-8bdf-4a33-bfae-a1929c2e8ca0", 35 | "email": "test@example.com" 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | 43 | ## Login 44 | __Request__ 45 | ``` 46 | mutation { 47 | login ( 48 | input: { 49 | email: "test@example.com", 50 | password: "password" 51 | } 52 | ) { 53 | user { 54 | id, email, firstName, lastName, authToken 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | __Response__ 61 | 62 | ```json 63 | { 64 | "data": { 65 | "login": { 66 | "user": { 67 | "id": "f1b234c8-8bdf-4a33-bfae-a1929c2e8ca0", 68 | "email": "test@example.com", 69 | "firstName": "Dave", 70 | "lastName": "", 71 | "authToken": "eyJhbGciO..." 72 | } 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | 79 | ## Request Password Reset 80 | 81 | __Request__ 82 | ``` 83 | mutation RequestPasswordReset { 84 | passwordReset ( 85 | input: { 86 | email: "test@example.com" 87 | } 88 | ) { 89 | message 90 | } 91 | } 92 | ``` 93 | 94 | __Response__ 95 | 96 | ```json 97 | { 98 | "data": { 99 | "passwordReset": { 100 | "message": "Further instructions will be sent to the email if it exists" 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | 107 | ## Password Change 108 | (requires authentication) 109 | 110 | __Request__ 111 | ``` 112 | mutation PasswordChange { 113 | passwordChange ( 114 | input: { 115 | currentPassword: "password", newPassword:"newpassword" 116 | } 117 | ) { 118 | user { 119 | email 120 | firstName 121 | lastName 122 | authToken 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | __Response__ 129 | 130 | ```json 131 | { 132 | "data": { 133 | "login": { 134 | "user": { 135 | "id": "f1b234c8-8bdf-4a33-bfae-a1929c2e8ca0", 136 | "email": "test@example.com", 137 | "firstName": "Dave", 138 | "lastName": "", 139 | "authToken": "eyJhbGciO..." 140 | } 141 | } 142 | } 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/backend/coding_rules.md: -------------------------------------------------------------------------------- 1 | # Conventions 2 | 3 | - Read and pay attention to current code in the repository 4 | - For the Python part, we follow pep8 in most cases. We use [flake8][flake8] to check for linting errors. Once you're ready to commit changes, check your code with `flake8`. 5 | - Install a plugin for [EditorConfig][editorconfig] and let it handle some of the formating issues for you. 6 | - For the Django part, we follow standard [Django coding style][django-coding style]. 7 | - And always remember the Zen. 8 | 9 | [editorconfig]: http://editorconfig.org/ 10 | [flake8]: http://flake8.readthedocs.org/en/latest/ 11 | [django-coding style]: https://docs.djangoproject.com/en/1.7/internals/contributing/writing-code/coding-style/ 12 | 13 | 14 | # Coding Rules 15 | 16 | ## Django models 17 | 18 | * All model names in singular and PascalCase. 19 | * All models have a `Meta` with at least: 20 | - `verbose_name` and `verbose_name_plural`: unicode strings, lowercase, with spaces. 21 | - `ordering`: return a consistent order, using pk if no other unique field or combination exists. 22 | * All models have `__str__` method, returning a human-readable, descriptive, short text. 23 | * All fields have `verbose_name`. Also `help_text` if needed to fully explain the field meaning. 24 | * All fields have explicit `blank` and `null` parameters. Use only those combinations, unless there a documented need of other thing: 25 | 26 | __Normal fields__ (IntegerField, DateField, ForeignKey, FileField...) 27 | 28 | - (optional) `null = True`, `blank = True` 29 | - (required) `null = False`, `blank = False` 30 | 31 | __Text fields__ (CharField, TextField, URLField...) 32 | 33 | - (optional) `null = False`, `blank = True` 34 | - (required) `null = False`, `blank = False` 35 | 36 | __Boolean fields__: 37 | 38 | - (two values, T/F) `null = False`, `blank = True` 39 | - (three values, T/F/Null) `null = True`, `blank = True` 40 | 41 | * Don't create text fields with `null = True`, unless you need to distinguish between empty string and `None`. 42 | * Don't create boolean fields with `blank = False`, otherwise they could only be `True`. 43 | 44 | Example: 45 | 46 | ```python 47 | class SomeClass(models.Model): 48 | name = models.CharField(max_length=100, null = False, blank = False, unique=True, 49 | verbose_name = _('name')) 50 | slug = models.SlugField(max_length=100, null = False, blank = False, unique=True, 51 | verbose_name = _('slug'), 52 | help_text = ('Identifier of this object. Only letters, digits and underscore "_" allowed.')) 53 | text = models.TextField(null = False, blank = True, 54 | verbose_name = _('text')) 55 | 56 | class Meta: 57 | verbose_name = _('some class') 58 | verbose_name_plural = _('some classes') 59 | ordering = ['name'] 60 | 61 | def __str__(self): 62 | return self.name 63 | ``` 64 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/serializers.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.contrib.auth import password_validation 3 | from rest_framework import serializers 4 | 5 | # {{ cookiecutter.main_module }} Stuff 6 | from {{cookiecutter.main_module}}.users import services as user_services 7 | from {{cookiecutter.main_module}}.users.models import UserManager 8 | from {{cookiecutter.main_module}}.users.serializers import UserSerializer 9 | 10 | from . import tokens 11 | 12 | 13 | class EmptySerializer(serializers.Serializer): 14 | pass 15 | 16 | 17 | class LoginSerializer(serializers.Serializer): 18 | email = serializers.CharField(max_length=300, required=True) 19 | password = serializers.CharField(required=True) 20 | 21 | 22 | class RegisterSerializer(serializers.Serializer): 23 | email = serializers.EmailField(required=True) 24 | password = serializers.CharField(required=True) 25 | first_name = serializers.CharField(required=False, allow_blank=True, max_length=120) 26 | last_name = serializers.CharField(required=False, allow_blank=True, max_length=120) 27 | 28 | def validate_password(self, value): 29 | password_validation.validate_password(value) 30 | return value 31 | 32 | def validate_email(self, value): 33 | user = user_services.get_user_by_email(email=value) 34 | if user: 35 | raise serializers.ValidationError("Email is already taken.") 36 | return UserManager.normalize_email(value) 37 | 38 | 39 | class AuthUserSerializer(UserSerializer): 40 | auth_token = serializers.SerializerMethodField() 41 | 42 | class Meta(UserSerializer.Meta): 43 | fields = UserSerializer.Meta.fields + ["auth_token"] 44 | 45 | def get_auth_token(self, obj): 46 | return tokens.get_token_for_user(obj, "authentication") 47 | 48 | 49 | class PasswordChangeSerializer(serializers.Serializer): 50 | current_password = serializers.CharField(required=True) 51 | new_password = serializers.CharField(required=True) 52 | 53 | default_error_messages = {"invalid_password": "Current password does not match"} 54 | 55 | def validate_current_password(self, value): 56 | if not self.context["request"].user.check_password(value): 57 | raise serializers.ValidationError( 58 | self.default_error_messages["invalid_password"] 59 | ) 60 | return value 61 | 62 | def validate_new_password(self, value): 63 | # https://docs.djangoproject.com/en/2.0/topics/auth/passwords/#django.contrib.auth.password_validation.validate_password 64 | password_validation.validate_password(value) 65 | return value 66 | 67 | 68 | class PasswordResetSerializer(serializers.Serializer): 69 | email = serializers.EmailField(required=True) 70 | 71 | 72 | class PasswordResetConfirmSerializer(serializers.Serializer): 73 | new_password = serializers.CharField(required=True) 74 | token = serializers.CharField(required=True) 75 | 76 | def validate_new_password(self, value): 77 | password_validation.validate_password(value) 78 | return value 79 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/models.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.contrib.auth.models import ( 3 | AbstractBaseUser, 4 | BaseUserManager, 5 | PermissionsMixin, 6 | ) 7 | from django.contrib.postgres.fields import CIEmailField 8 | from django.db import models 9 | from django.utils import timezone 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | # {{ cookiecutter.main_module }} Stuff 13 | from {{cookiecutter.main_module}}.base.models import UUIDModel 14 | 15 | 16 | class UserManager(BaseUserManager): 17 | use_in_migrations = True 18 | 19 | def _create_user( 20 | self, 21 | email: str, 22 | password: str, 23 | is_staff: bool, 24 | is_superuser: bool, 25 | **extra_fields, 26 | ): 27 | email = self.normalize_email(email) 28 | user = self.model( 29 | email=email, 30 | is_staff=is_staff, 31 | is_active=True, 32 | is_superuser=is_superuser, 33 | **extra_fields, 34 | ) 35 | user.set_password(password) 36 | user.save(using=self._db) 37 | return user 38 | 39 | def create_user(self, email: str, password=None, **extra_fields): 40 | """Creates and saves a User with the given email and password.""" 41 | return self._create_user(email, password, False, False, **extra_fields) 42 | 43 | def create_superuser(self, email: str, password: str, **extra_fields): 44 | return self._create_user(email, password, True, True, **extra_fields) 45 | 46 | 47 | class User(AbstractBaseUser, UUIDModel, PermissionsMixin): 48 | first_name = models.CharField(_("First Name"), max_length=120, blank=True) 49 | last_name = models.CharField(_("Last Name"), max_length=120, blank=True) 50 | # https://docs.djangoproject.com/en/1.11/ref/contrib/postgres/fields/#citext-fields 51 | email = CIEmailField(_("email address"), unique=True, db_index=True) 52 | is_staff = models.BooleanField( 53 | _("staff status"), 54 | default=False, 55 | help_text="Designates whether the user can log into this admin site.", 56 | ) 57 | 58 | is_active = models.BooleanField( 59 | "active", 60 | default=True, 61 | help_text="Designates whether this user should be treated as " 62 | "active. Unselect this instead of deleting accounts.", 63 | ) 64 | date_joined = models.DateTimeField(_("date joined"), default=timezone.now) 65 | 66 | USERNAME_FIELD = "email" 67 | objects = UserManager() 68 | 69 | class Meta: 70 | verbose_name = _("user") 71 | verbose_name_plural = _("users") 72 | ordering = ("-date_joined",) 73 | 74 | def __str__(self): 75 | return str(self.id) 76 | 77 | def get_full_name(self) -> str: 78 | """Returns the first_name plus the last_name, with a space in between.""" 79 | full_name = "{} {}".format(self.first_name, self.last_name) 80 | return full_name.strip() 81 | 82 | def get_short_name(self) -> str: 83 | """Returns the short name for the user.""" 84 | return self.first_name.strip() 85 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/project_data/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %}--- 2 | - name: Download latest code from {{ project_repo_url }}, branch {{ repo_version }} 3 | git: repo={{ project_repo_url }} dest={{ project_path }} version={{ repo_version }} accept_hostkey=true 4 | become: false 5 | register: gitresult 6 | when: vm == 0 7 | tags: ['always'] 8 | 9 | - debug: msg="Git SHA-1 before={{ gitresult.before }} after={{ gitresult.after }}" 10 | when: vm == 0 11 | tags: ['always'] 12 | 13 | - name: Ensure python virtualenv folder exist 14 | command: python3 -m venv {{ venv_path }} creates={{ venv_path }} 15 | become: false 16 | tags: ['always'] 17 | 18 | - name: Install binaries for postgres postgis 19 | apt: 20 | pkg: 21 | - gdal-bin 22 | state: present 23 | cache_valid_time: 300 24 | when: pg_gis 25 | 26 | - name: Install python dependencies 27 | pip: requirements={{ project_path }}/{{ django_requirements_file }} executable={{ venv_path }}/bin/pip 28 | become: false 29 | tags: ['deploy'] 30 | 31 | - name: Copy env configuration, if it doesn't exist 32 | template: src=env.env.j2 dest={{ project_path }}/.env force=no 33 | become: false 34 | tags: ['configure'] 35 | 36 | - name: Ensure gettext package is installed for translations 37 | apt: 38 | pkg: gettext 39 | state: present 40 | 41 | - name: Run collect static 42 | django_manage: command=collectstatic app_path={{ project_path }} virtualenv={{ venv_path }} 43 | become: false 44 | tags: ['deploy'] 45 | 46 | - name: Run database migrations 47 | django_manage: command=migrate app_path={{ project_path }} virtualenv={{ venv_path }} 48 | become: false 49 | tags: ['deploy'] 50 | 51 | - name: Run compilemessages for static translations 52 | django_manage: command=compilemessages app_path={{ project_path }} virtualenv={{ venv_path }} 53 | become: false 54 | tags: ['deploy'] 55 | 56 | {% endraw %} 57 | {%- if cookiecutter.add_asgi.lower() == 'y' %} 58 | - import_tasks: asgi-setup.yml 59 | 60 | - name: Reload asgi processes 61 | {% raw %} 62 | systemd: state=restarted name=asgi-{{ project_namespace }} 63 | {% endraw %} 64 | {%- else %} 65 | - import_tasks: uwsgi-setup.yml 66 | 67 | {% raw %} 68 | - name: Reload uwsgi processes 69 | command: uwsgi --reload {{ uwsgi_pid_file }} 70 | become: true 71 | when: not uwsgiconf.changed 72 | {% endraw %} 73 | {%- endif %} 74 | tags: ['deploy'] 75 | {%- if cookiecutter.add_celery.lower() == 'y' %} 76 | notify: reload celery # reload celery everytime uwsgi conf changes 77 | {%- endif %} 78 | 79 | {% raw %}- name: apt_get install graphviz for db schema generation 80 | apt: pkg=graphviz state=present 81 | 82 | - name: Generate DB Schema. 83 | shell: "source {{ venv_path }}/bin/activate && python bin/generate_db_schema.py" 84 | args: 85 | chdir: "{{ project_path }}" 86 | executable: /bin/bash 87 | become: false 88 | tags: ['deploy', 'documentation'] 89 | 90 | - name: Build documentation for "/docs" url. 91 | command: "{{ venv_path }}/bin/mkdocs build" 92 | args: 93 | chdir: "{{ project_path }}" 94 | become: false 95 | tags: ['deploy', 'documentation']{% endraw %} 96 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/templates/site.443.conf.j2: -------------------------------------------------------------------------------- 1 | {%raw%}# {{ ansible_managed }} 2 | 3 | server { 4 | listen {{ ngnix_listen }}; 5 | listen [::]:{{ ngnix_listen }}; 6 | server_name {{ domain_name }}; 7 | 8 | ssl_certificate {{ ssl_certificate }}; 9 | ssl_certificate_key {{ ssl_certificate_key }}; 10 | ssl_dhparam {{ ssl_dhparam }}; 11 | 12 | 13 | ssl_protocols {{ ssl_protocols }}; 14 | ssl_ciphers '{{ ssl_ciphers }}'; 15 | ssl_prefer_server_ciphers on; 16 | 17 | ssl_session_timeout {{ ssl_session_timeout }}; 18 | ssl_session_cache {{ ssl_session_cache }}; 19 | ssl_session_tickets off; 20 | 21 | # OCSP Stapling --- 22 | # fetch OCSP records from URL in ssl_certificate and cache them 23 | ssl_stapling on; 24 | ssl_stapling_verify on; 25 | 26 | ## verify chain of trust of OCSP response using Root CA and Intermediate certs 27 | ssl_trusted_certificate {{ ssl_trusted_certificate }}; 28 | 29 | 30 | access_log {{ access_log_file }}; 31 | error_log {{ error_log_file }}; 32 | 33 | 34 | client_max_body_size {{ nginx_client_max_body_size }}; 35 | 36 | {% if use_letsencrypt %} 37 | location /.well-known/acme-challenge { 38 | root {{ letsencrypt_challenge_root }}; 39 | try_files $uri =404; 40 | } 41 | {% endif %} 42 | 43 | location /docs { 44 | # mkdocs html generated via "product_data" role 45 | alias {{ project_path }}/_docs_html/;{% endraw %} 46 | {%- if cookiecutter.have_password_protected_docs.lower() == 'y' %} 47 | {%raw%}auth_basic "Restricted Content"; 48 | auth_basic_user_file {{ htpasswd_file_path }};{% endraw %} 49 | {%- endif -%}{%raw%} 50 | } 51 | 52 | # Setup named location for Django requests and handle proxy details 53 | location / { 54 | {%- if cookiecutter.add_asgi.lower() == 'y' %} 55 | proxy_set_header Host $http_host; 56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 57 | proxy_set_header X-Forwarded-Proto $scheme; 58 | proxy_set_header Upgrade $http_upgrade; 59 | proxy_set_header Connection $connection_upgrade; 60 | proxy_redirect off; 61 | proxy_buffering off; 62 | proxy_pass http://uvicorn; 63 | 64 | {%- else %} 65 | uwsgi_pass unix:///tmp/uwsgi-{{ project_namespace }}.sock; 66 | include /etc/nginx/uwsgi_params; 67 | 68 | # set correct scheme 69 | uwsgi_param UWSGI_SCHEME $http_x_forwarded_proto; 70 | {%- endif %} 71 | } 72 | {% endraw %} 73 | {%- if cookiecutter.enable_whitenoise.lower() == 'n' %} 74 | {%raw%}location /static/ { 75 | alias {{ project_path }}/.staticfiles/; 76 | } 77 | 78 | location /media/ { 79 | alias {{ project_path }}/.media/; 80 | }{% endraw %} 81 | {%- endif %} 82 | } 83 | 84 | {%- if cookiecutter.add_asgi.lower() == 'y' %} 85 | upstream uvicorn { 86 | {% raw %}server unix://{{ asgi_socket }};{% endraw %} 87 | } 88 | 89 | map $http_upgrade $connection_upgrade { 90 | default upgrade; 91 | '' close; 92 | } 93 | {%- endif %} 94 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/settings/development.py: -------------------------------------------------------------------------------- 1 | """Development Settings 2 | 3 | Adds sensible defaults for developement of project 4 | - Enable DEBUG 5 | - Log outgoing emails to console 6 | - Enable Django Extensions 7 | - Enable Django Debug Toolbar 8 | - Use local caches 9 | - Enable livereloading 10 | """ 11 | 12 | from .common import * # noqa F405 13 | from .common import INSTALLED_APPS, TEMPLATES, env 14 | 15 | # DEBUG 16 | # ------------------------------------------------------------------------------ 17 | DEBUG = env.bool("DJANGO_DEBUG", default=True) 18 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # type: ignore 19 | 20 | INTERNAL_IPS = ("127.0.0.1", "192.168.33.12") 21 | 22 | ALLOWED_HOSTS = ["*"] 23 | 24 | {%- if cookiecutter.enable_whitenoise.lower() == "y" %} 25 | # Staticfiles 26 | # ------------------------------------------------------------------------------ 27 | # Disable Django's static file handling and allow WhiteNoise to take over. This 28 | # helps in minimizing dev/prod differences when serving static files. 29 | INSTALLED_APPS = ["whitenoise.runserver_nostatic", ] + INSTALLED_APPS 30 | {%- endif %} 31 | 32 | 33 | # SECRET CONFIGURATION 34 | # ------------------------------------------------------------------------------ 35 | # A secret key for this particular Django installation. Used in secret-key 36 | # hashing algorithms. Set this in your settings, or Django will complain 37 | # loudly. 38 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 39 | # Note: This key only used for development and testing. 40 | SECRET_KEY = env("DJANGO_SECRET_KEY", default="CHANGEME!!!") 41 | 42 | # cors 43 | # -------------------------------------------------------------------------- 44 | CORS_ALLOWED_ORIGINS = env.list( 45 | "CORS_ALLOWED_ORIGINS", default=["http://localhost", "http://localhost:8000"] 46 | ) 47 | 48 | # Mail settings 49 | # ------------------------------------------------------------------------------ 50 | EMAIL_HOST = "localhost" 51 | EMAIL_PORT = 1025 52 | EMAIL_BACKEND = env( 53 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 54 | ) 55 | 56 | # CACHES 57 | # ------------------------------------------------------------------------------ 58 | CACHES = { 59 | "default": { 60 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 61 | "LOCATION": "", 62 | } 63 | } 64 | 65 | # django-debug-toolbar 66 | # ------------------------------------------------------------------------------ 67 | MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405 68 | INSTALLED_APPS += [ 69 | "debug_toolbar", 70 | ] 71 | DEBUG_TOOLBAR_CONFIG = { 72 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 73 | "SHOW_TEMPLATE_CONTEXT": True, 74 | } 75 | 76 | # This will expose all browsable api urls. For dev the default value is true 77 | API_DEBUG = env.bool("API_DEBUG", default=True) 78 | 79 | # MEDIA CONFIGURATION 80 | # ------------------------------------------------------------------------------ 81 | 82 | # Media configuration to support deployment of media files while is debug=True or development. 83 | MEDIA_URL = env("MEDIA_URL", default="/media/") 84 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/graphql/errors_handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | There are several error types in the GraphQL API, and you may come across different ones depending on the operations you are trying to perform. 4 | 5 | The GraphQL API handles the following three types of errors: 6 | 7 | ## Query-level errors 8 | 9 | This error occurs if you provide wrong or unrecognized input data while performing a specified operation. GraphQL checks the syntax as you write, and if you are trying to execute an unknown operation, the editor you are using will notify you. If you proceed with sending the request, you will get a syntax error. 10 | 11 | Below is an example of an error triggered by the wrong syntax. The following query tries to fetch the fullName field, which doesn't exist on the User type: 12 | 13 | 14 | ``` 15 | query { 16 | me { 17 | fullName 18 | } 19 | } 20 | ``` 21 | 22 | Sending this query to the server would result in the following syntax error: 23 | 24 | ```json 25 | { 26 | "error": { 27 | "errors": [ 28 | { 29 | "message": "Cannot query field \"fullName\" on type \"User\". Did you mean \"firstName\" or \"lastName\"?", 30 | "locations": [ 31 | { 32 | "line": 3, 33 | "column": 5 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | } 40 | ``` 41 | 42 | ## Data-level errors 43 | 44 | This error occurs when the user passes invalid data as the mutation input. For example, using an email address that is associated with a user account to create a secondary account will throw a validation error since the email should be unique within a user account. 45 | 46 | Validation errors are part of the schema, meaning we need to include them in the query to get them explicitly. In all mutations, for example, you can obtain them through the `errors` field. 47 | 48 | Below is an example of an error triggered by validation issues: 49 | 50 | ``` 51 | mutation { 52 | accountRegister( 53 | input: { 54 | email: "customer@example.com" 55 | password: "" 56 | redirectUrl: "http://example.com/reset-password/" 57 | } 58 | ) { 59 | user { 60 | email 61 | } 62 | errors { 63 | field 64 | code 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | Validation errors are returned in a dedicated error field inside mutation results: 71 | 72 | ```json 73 | { 74 | "data": { 75 | "accountRegister": { 76 | "user": null, 77 | "errors": [ 78 | { 79 | "field": "email", 80 | "code": "UNIQUE" 81 | } 82 | ] 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | ## Permission errors 89 | 90 | This error occurs when you are trying to perform a specific operation but are not authorized to do so; in other words, you have no sufficient permissions assigned. 91 | 92 | Below is an example of an error triggered by insufficient authorization. The `users` query requires appropriate admin permissions: 93 | 94 | ``` 95 | query { 96 | users(first: 20) { 97 | edges { 98 | node { 99 | id 100 | } 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | ```json 107 | { 108 | "errors": [ 109 | { 110 | "message": "You do not have permission to perform this action", 111 | "locations": [ 112 | { 113 | "line": 33, 114 | "column": 3 115 | } 116 | ], 117 | "path": [ 118 | "users" 119 | ] 120 | } 121 | ], 122 | "data": { 123 | "users": null 124 | } 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/users/auth/api.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from django.contrib.auth import logout 3 | from rest_framework import viewsets 4 | from rest_framework.decorators import action 5 | from rest_framework.permissions import AllowAny, IsAuthenticated 6 | 7 | # {{ cookiecutter.main_module }} Stuff 8 | from {{cookiecutter.main_module}}.base import response 9 | from {{cookiecutter.main_module}}.base.api.mixins import MultipleSerializerMixin 10 | from {{cookiecutter.main_module}}.users import services as user_services 11 | 12 | from . import serializers, services, tokens 13 | 14 | 15 | class AuthViewSet(MultipleSerializerMixin, viewsets.GenericViewSet): 16 | 17 | permission_classes = [AllowAny] 18 | serializer_classes = { 19 | "login": serializers.LoginSerializer, 20 | "register": serializers.RegisterSerializer, 21 | "logout": serializers.EmptySerializer, 22 | "password_change": serializers.PasswordChangeSerializer, 23 | "password_reset": serializers.PasswordResetSerializer, 24 | "password_reset_confirm": serializers.PasswordResetConfirmSerializer, 25 | } 26 | 27 | @action(methods=["POST"], detail=False) 28 | def login(self, request): 29 | serializer = self.get_serializer(data=request.data) 30 | serializer.is_valid(raise_exception=True) 31 | user = user_services.get_and_authenticate_user(**serializer.validated_data) 32 | data = serializers.AuthUserSerializer(user).data 33 | return response.Ok(data) 34 | 35 | @action(methods=["POST"], detail=False) 36 | def register(self, request): 37 | serializer = self.get_serializer(data=request.data) 38 | serializer.is_valid(raise_exception=True) 39 | user = user_services.create_user_account(**serializer.validated_data) 40 | data = serializers.AuthUserSerializer(user).data 41 | return response.Created(data) 42 | 43 | @action(methods=["POST"], detail=False) 44 | def logout(self, request): 45 | """ 46 | Calls Django logout method; Does not work for UserTokenAuth. 47 | """ 48 | logout(request) 49 | return response.Ok({"success": "Successfully logged out."}) 50 | 51 | @action(methods=["POST"], detail=False, permission_classes=[IsAuthenticated]) 52 | def password_change(self, request): 53 | serializer = self.get_serializer(data=request.data) 54 | serializer.is_valid(raise_exception=True) 55 | request.user.set_password(serializer.validated_data["new_password"]) 56 | request.user.save() 57 | return response.NoContent() 58 | 59 | @action(methods=["POST"], detail=False) 60 | def password_reset(self, request): 61 | serializer = self.get_serializer(data=request.data) 62 | serializer.is_valid(raise_exception=True) 63 | user = user_services.get_user_by_email(serializer.data["email"]) 64 | if user: 65 | services.send_password_reset_mail(user) 66 | return response.Ok( 67 | {"message": "Further instructions will be sent to the email if it exists"} 68 | ) 69 | 70 | @action(methods=["POST"], detail=False) 71 | def password_reset_confirm(self, request): 72 | serializer = self.get_serializer(data=request.data) 73 | serializer.is_valid(raise_exception=True) 74 | user = tokens.get_user_for_password_reset_token( 75 | serializer.validated_data["token"] 76 | ) 77 | user.set_password(serializer.validated_data["new_password"]) 78 | user.save() 79 | return response.NoContent() 80 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/urls.py: -------------------------------------------------------------------------------- 1 | """Root url routing file. 2 | 3 | You should put the url config in their respective app putting only a 4 | reference to them here. 5 | """ 6 | # Standard Library 7 | from typing import TYPE_CHECKING, List, Union 8 | 9 | # Third Party Stuff 10 | from django.conf import settings 11 | from django.conf.urls.static import static 12 | from django.contrib import admin 13 | from django.urls import include, path, re_path 14 | from django.views.generic import TemplateView 15 | 16 | {%- if cookiecutter.add_graphql == "y" %} 17 | from django.views.decorators.csrf import csrf_exempt 18 | from graphene_django.views import GraphQLView 19 | {%- endif %} 20 | 21 | from . import api_urls 22 | from .base import views as base_views 23 | from .base.api import schemas as api_schemas 24 | 25 | if TYPE_CHECKING: 26 | # Third Party Stuff 27 | from django.urls import URLPattern, URLResolver 28 | 29 | URL = Union[URLPattern, URLResolver] 30 | URLList = List[URL] 31 | 32 | admin.site.site_title = admin.site.site_header = "{{ cookiecutter.project_name }} Administration" 33 | handler500 = base_views.server_error 34 | 35 | # Top Level Pages 36 | # ============================================================================== 37 | urlpatterns: "URLList" = [ 38 | path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), 39 | path( 40 | "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 41 | ), 42 | # Your stuff: custom urls go here 43 | ] 44 | 45 | urlpatterns += [ 46 | re_path( 47 | r"^(?P(robots.txt)|(humans.txt))$", 48 | base_views.root_txt_files, 49 | name="root-txt-files", 50 | ), 51 | # Rest API 52 | path("api/", include(api_urls)), 53 | {%- if cookiecutter.add_graphql == "y" %} 54 | path( 55 | "graphql/", 56 | csrf_exempt(GraphQLView.as_view(graphiql=settings.API_DEBUG)), 57 | name="graphql", 58 | ), 59 | {% endif %} 60 | # Django Admin 61 | path("{}/".format(settings.DJANGO_ADMIN_URL), admin.site.urls), 62 | ] 63 | 64 | if settings.API_DEBUG: 65 | urlpatterns += [ 66 | # Browsable API 67 | path("api/schema/", api_schemas.schema_view.as_view(), name="schema"), 68 | path("api-playground/", api_schemas.swagger_schema_view, name="api-playground"), 69 | path("api/auth-n/", include("rest_framework.urls", namespace="rest_framework")), 70 | ] 71 | 72 | if settings.DEBUG: 73 | # Third Party Stuff 74 | from django.urls import get_callable 75 | from django.views import defaults as dj_default_views 76 | 77 | urlpatterns += [ 78 | path( 79 | "400/", 80 | dj_default_views.bad_request, 81 | kwargs={"exception": Exception("Bad Request!")}, 82 | ), 83 | path( 84 | "403/", 85 | dj_default_views.permission_denied, 86 | kwargs={"exception": Exception("Permission Denied!")}, 87 | ), 88 | path("403_csrf/", get_callable(settings.CSRF_FAILURE_VIEW)), 89 | path( 90 | "404/", 91 | dj_default_views.page_not_found, 92 | kwargs={"exception": Exception("Not Found!")}, 93 | ), 94 | path("500/", handler500), 95 | ] 96 | 97 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 98 | 99 | # Django Debug Toolbar 100 | if "debug_toolbar" in settings.INSTALLED_APPS: 101 | # Third Party Stuff 102 | import debug_toolbar 103 | 104 | urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] 105 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/common/files/bashrc: -------------------------------------------------------------------------------- 1 | # ~/.bashrc: executed by bash(1) for non-login shells. 2 | # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) 3 | # for examples 4 | 5 | # If not running interactively, don't do anything 6 | [ -z "$PS1" ] && return 7 | 8 | # don't put duplicate lines in the history. See bash(1) for more options 9 | # ... or force ignoredups and ignorespace 10 | HISTCONTROL=ignoredups:ignorespace 11 | 12 | # append to the history file, don't overwrite it 13 | shopt -s histappend 14 | 15 | # for setting history length see HISTSIZE and HISTFILESIZE in bash(1) 16 | HISTSIZE=1000 17 | HISTFILESIZE=2000 18 | 19 | # check the window size after each command and, if necessary, 20 | # update the values of LINES and COLUMNS. 21 | shopt -s checkwinsize 22 | 23 | # make less more friendly for non-text input files, see lesspipe(1) 24 | [ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" 25 | 26 | # set variable identifying the chroot you work in (used in the prompt below) 27 | if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then 28 | debian_chroot=$(cat /etc/debian_chroot) 29 | fi 30 | 31 | # set a fancy prompt (non-color, unless we know we "want" color) 32 | case "$TERM" in 33 | xterm-color) color_prompt=yes;; 34 | esac 35 | 36 | # uncomment for a colored prompt, if the terminal has the capability; turned 37 | # off by default to not distract the user: the focus in a terminal window 38 | # should be on the output of commands, not on the prompt 39 | force_color_prompt=yes 40 | 41 | if [ -n "$force_color_prompt" ]; then 42 | if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then 43 | # We have color support; assume it's compliant with Ecma-48 44 | # (ISO/IEC-6429). (Lack of such support is extremely rare, and such 45 | # a case would tend to support setf rather than setaf.) 46 | color_prompt=yes 47 | else 48 | color_prompt= 49 | fi 50 | fi 51 | 52 | if [ "$color_prompt" = yes ]; then 53 | PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@$(hostname -f)\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' 54 | else 55 | PS1='${debian_chroot:+($debian_chroot)}\u@$(hostname -f):\w\$ ' 56 | fi 57 | unset color_prompt force_color_prompt 58 | 59 | # If this is an xterm set the title to user@host:dir 60 | case "$TERM" in 61 | xterm*|rxvt*) 62 | PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" 63 | ;; 64 | *) 65 | ;; 66 | esac 67 | 68 | # enable color support of ls and also add handy aliases 69 | if [ -x /usr/bin/dircolors ]; then 70 | test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" 71 | alias ls='ls --color=auto' 72 | #alias dir='dir --color=auto' 73 | #alias vdir='vdir --color=auto' 74 | 75 | alias grep='grep --color=auto' 76 | alias fgrep='fgrep --color=auto' 77 | alias egrep='egrep --color=auto' 78 | fi 79 | 80 | # some more ls aliases 81 | alias ll='ls -alF' 82 | alias la='ls -A' 83 | alias l='ls -CF' 84 | alias g='git' 85 | 86 | # Alias definitions. 87 | # You may want to put all your additions into a separate file like 88 | # ~/.bash_aliases, instead of adding them here directly. 89 | # See /usr/share/doc/bash-doc/examples in the bash-doc package. 90 | 91 | if [ -f ~/.bash_aliases ]; then 92 | . ~/.bash_aliases 93 | fi 94 | 95 | # enable programmable completion features (you don't need to enable 96 | # this, if it's already enabled in /etc/bash.bashrc and /etc/profile 97 | # sources /etc/bash.bashrc). 98 | if [ -f /etc/bash_completion ] && ! shopt -oq posix; then 99 | . /etc/bash_completion 100 | fi 101 | 102 | export PIP_DOWNLOAD_CACHE=$HOME/.pip_download_cache 103 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/api/0-overview.md: -------------------------------------------------------------------------------- 1 | This describes the resources that make up the official {{ cookiecutter.project_name }} API v1. 2 | 3 | ## Authentication 4 | 5 | For clients to authenticate, the token key should be included in the Authorization HTTP header. The key should be prefixed by the string literal `Bearer`, with whitespace separating the two strings. For example: 6 | 7 | ``` 8 | Authorization: Bearer 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b 9 | ``` 10 | Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response. 11 | 12 | An example request: 13 | ```bash 14 | curl -X GET http://127.0.0.1:8000/api/example/ -H `Authorization: Bearer 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b` 15 | ``` 16 | 17 | ## Current Version 18 | 19 | By default, all requests receive the `1.0` version of the API. We encourage you to explicitly request this version via the Accept header. 20 | 21 | ``` 22 | Accept: application/vnd.{{ cookiecutter.main_module|replace('_', '')|replace('-', '') }}+json; version=1.0 23 | ``` 24 | 25 | __Important:__ The default version of the API may change in the future. If you're building an application and care about the stability of the API, be sure to request a specific version in the `Accept` header as shown in the examples below. 26 | 27 | ``` 28 | GET /bookings/ HTTP/1.1 29 | Host: example.com 30 | Accept: application/vnd.{{ cookiecutter.main_module|replace('_', '')|replace('-', '') }}+json; version=1.0 31 | ``` 32 | 33 | 34 | ## Schema 35 | 36 | All timestamps are returned in ISO 8601 format: 37 | 38 | `YYYY-MM-DDTHH:MM:SSZ` 39 | 40 | 41 | ## HTTP Verbs 42 | 43 | Where possible, API v1 strives to use appropriate HTTP verbs for each action. 44 | 45 | Verb | Description 46 | ------- | ------------- 47 | HEAD | Can be issued against any resource to get just the HTTP header info. 48 | GET | Used for retrieving resources. 49 | POST | Used for creating resources, or performing custom actions (such as merging a pull request). 50 | PATCH | Used for updating resources with partial JSON data. For instance, an Issue resource has title and body attributes. A PATCH request may accept one or more of the attributes to update the resource. PATCH is a relatively new and uncommon HTTP verb, so resource endpoints also accept POST requests. 51 | PUT | Used for replacing resources or collections. For PUT requests with no body attribute, be sure to set the Content-Length header to zero. 52 | DELETE | Used for deleting resources. 53 | 54 | ## Pagination 55 | {%- if cookiecutter.pagination == 'CursorPagination' %} 56 | 57 | Requests that return multiple items will be paginated to 30 items by default. You can specify cursor for the item with the `?cursor` parameter. For some resources, you can also set a custom page size up to 1000 with the `?per_page` parameter. 58 | 59 | Note that offset is 1-based and that omitting the `?cursor` parameter will return results from offset 1. 60 | 61 | By Default, the results are ordered in descending order of creation time based on field `-created_at` and it does not return `count` as part of the response. 62 | {%- elif cookiecutter.pagination == 'LimitOffsetPagination' %} 63 | 64 | Requests that return multiple items will be paginated to 30 items by default. You can specify offset for the item with the `?offset` parameter. For some resources, you can also set a custom page size up to 1000 with the `?per_page` parameter. 65 | 66 | Note that offset is 1-based and that omitting the `?offset` parameter will return results from offset 1. 67 | {%- endif %} 68 | 69 | ## Rate Limit 70 | 71 | All the unauthorized urls have a rate limit of 10,000 requests/day/IP. After exceeding the limit, you'll get `HTTP TOO MANY REQUESTS` with status code `429`. When this happens you'll also receive `X-Throttle-Wait-Seconds: ` header in response header. 72 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/docs/api/1-auth.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | !!!info 4 | For API overview and usages, check out [this page](0-overview.md). 5 | 6 | ## Login 7 | 8 | ``` 9 | POST /api/auth/login 10 | ``` 11 | 12 | __Parameters__ 13 | 14 | Name | Description 15 | ---------|------------------------------------- 16 | email | email of the user. 17 | password | password of the user. 18 | 19 | __Request__ 20 | ```json 21 | { 22 | "email": "hello@example.com", 23 | "password": "VerySafePassword0909" 24 | } 25 | ``` 26 | 27 | __Response__ 28 | ```json 29 | 30 | Status: 200 OK 31 | { 32 | "auth_token": "eyJ0eXAiOiJKV1QiL", 33 | "email": "ak123@fueled.com", 34 | "id": "f9dceed1-0f19-49f4-a874-0c2e131abf79", 35 | "first_name": "", 36 | "last_name": "" 37 | } 38 | ``` 39 | 40 | 41 | ## Register 42 | 43 | ``` 44 | POST /api/auth/register 45 | ``` 46 | 47 | __Parameters__ 48 | 49 | | Name | Description | 50 | | ---------- | ---------------------------------------------------------- | 51 | | email | email of the user. Errors out if email already registered. | 52 | | password | password of the user. | 53 | | first_name | first name of the user. | 54 | | last_name | last name of the user. | 55 | 56 | **Request** 57 | 58 | ```json 59 | { 60 | "email": "hello@example.com", 61 | "password": "VerySafePassword0909", 62 | "first_name": "S", 63 | "last_name": "K" 64 | } 65 | ``` 66 | 67 | __Response__ 68 | 69 | ```json 70 | 71 | Status: 201 Created 72 | { 73 | "auth_token": "eyJ0eXAiOiJKV1QiLCJh", 74 | "email": "test@test.com", 75 | "id": "f9dceed1-0f19-49f4-a874-0c2e131abf79", 76 | "first_name": "S", 77 | "last_name": "K" 78 | } 79 | ``` 80 | 81 | ## Change password 82 | 83 | ``` 84 | POST /api/auth/password_change (requires authentication) 85 | ``` 86 | 87 | __Parameters__ 88 | 89 | Name | Description 90 | -----------------|------------------------------------- 91 | current_password | Current password of the user. 92 | new_password | New password of the user. 93 | 94 | __Request__ 95 | ```json 96 | { 97 | "current_password": "NotSoSafePassword", 98 | "new_password": "VerySafePassword0909" 99 | } 100 | ``` 101 | 102 | __Response__ 103 | ``` 104 | Status: 204 No-Content 105 | ``` 106 | 107 | 108 | ## Request password for reset 109 | 110 | Send an email to user if the email exist. 111 | 112 | ``` 113 | POST /api/auth/password_reset 114 | ``` 115 | 116 | __Parameters__ 117 | 118 | Name | Description 119 | ------|------------------------------------- 120 | email | (required) valid email of an existing user. 121 | 122 | __Request__ 123 | ```json 124 | { 125 | "email": "hello@example.com" 126 | } 127 | ``` 128 | 129 | __Response__ 130 | ```json 131 | 132 | Status: 200 OK 133 | { 134 | "message": "Further instructions will be sent to the email if it exists" 135 | } 136 | ``` 137 | 138 | 139 | ## Confirm password reset 140 | 141 | Confirm password reset for the user using the token sent in email. 142 | 143 | ``` 144 | POST /api/auth/password_reset_confirm 145 | ``` 146 | 147 | __Parameters__ 148 | 149 | Name | Description 150 | --------------|------------------------------------- 151 | new_password | New password of the user 152 | token | Token decoded from the url (verification link) 153 | 154 | 155 | __Request__ 156 | ```json 157 | { 158 | "new_password": "new_pass", 159 | "token" : "IgotTHISfromTHEverificationLINKinEmail" 160 | } 161 | ``` 162 | 163 | __Response__ 164 | ``` 165 | Status: 204 No-Content 166 | ``` 167 | 168 | !!!Note 169 | The verification link uses the format of key `password-confirm` in `FRONTEND_URLS` dict in settings/common. 170 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/{{cookiecutter.main_module}}/base/api/pagination.py: -------------------------------------------------------------------------------- 1 | # Third Party Stuff 2 | from rest_framework.pagination import CursorPagination as DrfCursorPagination 3 | from rest_framework.pagination import LimitOffsetPagination as DrfLimitOffsetPagination 4 | 5 | 6 | class LimitOffsetPagination(DrfLimitOffsetPagination): 7 | 8 | # Client can control the offset using this query parameter. 9 | offset_query_param = "offset" 10 | 11 | # Client can control the page size using this query parameter. 12 | # Default is 'None'. Set to eg 'page_size' to enable usage. 13 | limit_query_param = "per_page" 14 | 15 | # Set to an integer to limit the maximum page size the client may request. 16 | # Only relevant if 'page_size_query_param' has also been set. 17 | max_limit = 1000 18 | 19 | 20 | def limit_offset_paginated_response( 21 | request, queryset, serializer_class, extra_context=None 22 | ): 23 | """Utlity function to return a paginated response. 24 | 25 | Returns `Response` object with pagination info after serializing the django 26 | `queryset` as per the `serializer_class` given and processing the current 27 | `page` from the `request` object. 28 | 29 | If `extra_context`(dict) is provided, it will be passed to the serializer_class 30 | as context variables. 31 | """ 32 | paginator = LimitOffsetPagination() 33 | paginated_queryset = paginator.paginate_queryset(queryset=queryset, request=request) 34 | serializer_context = {"request": request} 35 | 36 | if extra_context: 37 | serializer_context.update(extra_context) 38 | 39 | serializer = serializer_class( 40 | paginated_queryset, context=serializer_context, many=True 41 | ) 42 | return paginator.get_paginated_response(data=serializer.data) 43 | 44 | 45 | class CursorPagination(DrfCursorPagination): 46 | """ 47 | The cursor pagination implementation is necessarily complex. 48 | For an overview of the position/offset style we use, see this post: 49 | https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api 50 | 51 | Cursor Pagination does not returns the `count` as part of the response 52 | """ 53 | 54 | # Ordering field used for cursor pagination 55 | # While using cursor pagination, ensure ordering field is part of that model 56 | ordering = "-created_at" 57 | 58 | # Client can control the cursor using this query parameter. 59 | cursor_query_param = "cursor" 60 | 61 | # Client can control the page size using this query parameter. 62 | # Default is 'None'. Set to eg 'page_size' to enable usage. 63 | page_size_query_param = "per_page" 64 | 65 | # Set to an integer to limit the maximum page size the client may request. 66 | # Only relevant if 'page_size_query_param' has also been set. 67 | max_page_size = None 68 | 69 | # The offset in the cursor is used in situations where we have a 70 | # nearly-unique index. (Eg millisecond precision creation timestamps) 71 | # We guard against malicious users attempting to cause expensive database 72 | # queries, by having a hard cap on the maximum possible size of the offset. 73 | offset_cutoff = 1000 74 | 75 | 76 | def cursor_paginated_response(request, queryset, serializer_class, extra_context=None): 77 | """Utlity function to return a paginated response. 78 | 79 | Returns `Response` object with pagination info after serializing the django 80 | `queryset` as per the `serializer_class` given and processing the current 81 | `page` from the `request` object. 82 | 83 | If `extra_context`(dict) is provided, it will be passed to the serializer_class 84 | as context variables. 85 | """ 86 | paginator = CursorPagination() 87 | paginated_queryset = paginator.paginate_queryset(queryset=queryset, request=request) 88 | serializer_context = {"request": request} 89 | 90 | if extra_context: 91 | serializer_context.update(extra_context) 92 | 93 | serializer = serializer_class( 94 | paginated_queryset, context=serializer_context, many=True 95 | ) 96 | return paginator.get_paginated_response(data=serializer.data) 97 | -------------------------------------------------------------------------------- /{{cookiecutter.github_repository}}/provisioner/roles/nginx/templates/nginx.conf.j2: -------------------------------------------------------------------------------- 1 | {% raw %}user {{ nginx_user }}; 2 | worker_processes {{ nginx_worker_processes }}; 3 | 4 | pid /var/run/nginx.pid; 5 | 6 | events { 7 | worker_connections {{ nginx_worker_connections }}; 8 | # multi_accept on; 9 | } 10 | 11 | http { 12 | 13 | # Basic Settings 14 | # -------------------------------------- 15 | 16 | charset utf-8; 17 | sendfile on; 18 | tcp_nopush on; 19 | tcp_nodelay on; 20 | client_body_timeout 12; 21 | client_header_timeout 12; 22 | keepalive_timeout 15; 23 | send_timeout 10; 24 | types_hash_max_size 2048; 25 | 26 | # server_names_hash_bucket_size 64; 27 | # server_name_in_redirect off; 28 | 29 | include /etc/nginx/mime.types; 30 | default_type application/octet-stream; 31 | 32 | # Security Headers 33 | # -------------------------------------------------------------------------- 34 | # don't send the nginx version number in error pages and Server header 35 | server_tokens off; 36 | 37 | # config to don't allow the browser to render the page inside an frame or iframe 38 | # and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking 39 | # if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri 40 | # https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options 41 | add_header X-Frame-Options SAMEORIGIN; 42 | 43 | # when serving user-supplied content, include a X-Content-Type-Options: nosniff header along with the Content-Type: header, 44 | # to disable content-type sniffing on some browsers. 45 | # https://www.owasp.org/index.php/List_of_useful_HTTP_headers 46 | # currently supported in IE > 8 http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx 47 | # http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx 48 | # 'soon' on Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=471020 49 | add_header X-Content-Type-Options nosniff; 50 | 51 | # This header enables the Cross-site scripting (XSS) filter built into most recent web browsers. 52 | # It's usually enabled by default anyway, so the role of this header is to re-enable the filter for 53 | # this particular website if it was disabled by the user. 54 | # https://www.owasp.org/index.php/List_of_useful_HTTP_headers 55 | add_header X-XSS-Protection "1; mode=block"; 56 | 57 | 58 | # Logging Settings 59 | # -------------------------------------- 60 | 61 | access_log /var/log/nginx/access.log; 62 | error_log /var/log/nginx/error.log; 63 | 64 | 65 | # Gzip Settings 66 | # -------------------------------------- 67 | 68 | gzip on; 69 | gzip_disable "msie6"; 70 | {% endraw %} 71 | {%- if cookiecutter.enable_whitenoise.lower() == 'y' %} 72 | # gzip_vary on; 73 | # gzip_proxied any; 74 | # gzip_comp_level 6; 75 | # gzip_buffers 16 8k; 76 | # gzip_http_version 1.1; 77 | # gzip_types text/plain text/css application/json text/javascript application/javascript application/x-javascript text/xml application/xml application/xml+rss; 78 | {%- else %} 79 | gzip_vary on; 80 | gzip_proxied any; 81 | gzip_comp_level 6; 82 | gzip_buffers 16 8k; 83 | gzip_http_version 1.1; 84 | gzip_types text/plain text/css application/json text/javascript application/javascript application/x-javascript text/xml application/xml application/xml+rss; 85 | {%- endif %} 86 | {% raw %} 87 | 88 | # nginx-naxsi config 89 | # -------------------------------------- 90 | # Uncomment it if you installed nginx-naxsi 91 | 92 | #include /etc/nginx/naxsi_core.rules; 93 | 94 | 95 | # nginx-passenger config 96 | # -------------------------------------- 97 | # Uncomment it if you installed nginx-passenger 98 | 99 | #passenger_root /usr; 100 | #passenger_ruby /usr/bin/ruby; 101 | 102 | # Virtual Host Configs 103 | # -------------------------------------- 104 | 105 | include /etc/nginx/conf.d/*.conf; 106 | include /etc/nginx/sites-enabled/*; 107 | }{% endraw %} 108 | --------------------------------------------------------------------------------