27 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # wemake-django-template documentation build configuration file, created by
2 | # sphinx-quickstart on Sat Sep 30 12:42:34 2017.
3 | #
4 | # This file is execfile()d with the current directory set to its
5 | # containing dir.
6 | #
7 | # Note that not all possible configuration values are present in this
8 | # autogenerated file.
9 | #
10 | # All configuration values have a default; values that are commented out
11 | # serve to show the default.
12 |
13 | # If extensions (or modules to document with autodoc) are in another directory,
14 | # add these directories to sys.path here. If the directory is relative to the
15 | # documentation root, use os.path.abspath to make it absolute, like shown here.
16 |
17 | import os
18 | import sys
19 |
20 | import django
21 | import tomli
22 |
23 | # We need `server` to be importable from here:
24 | sys.path.insert(0, os.path.abspath('..'))
25 |
26 | # Django setup, all deps must be present to succeed:
27 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
28 | django.setup()
29 |
30 |
31 | # -- Project information -----------------------------------------------------
32 |
33 | def _get_project_meta():
34 | with open('../pyproject.toml', mode='rb') as pyproject:
35 | return tomli.load(pyproject)['tool']['poetry']
36 |
37 |
38 | pkg_meta = _get_project_meta()
39 | project = str(pkg_meta['name'])
40 | author = str(pkg_meta['authors'][0])
41 | copyright = author # noqa: WPS125
42 |
43 | # The short X.Y version
44 | version = str(pkg_meta['version'])
45 | # The full version, including alpha/beta/rc tags
46 | release = version
47 |
48 |
49 | # -- General configuration ------------------------------------------------
50 |
51 | # If your documentation needs a minimal Sphinx version, state it here.
52 | needs_sphinx = '5.0'
53 |
54 | # Add any Sphinx extension module names here, as strings. They can be
55 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
56 | # ones.
57 | extensions = [
58 | 'sphinx.ext.autodoc',
59 | 'sphinx.ext.doctest',
60 | 'sphinx.ext.todo',
61 | 'sphinx.ext.coverage',
62 | 'sphinx.ext.viewcode',
63 | 'sphinx.ext.githubpages',
64 | 'sphinx.ext.napoleon',
65 |
66 | # 3rd party, order matters:
67 | # https://github.com/wemake-services/wemake-django-template/issues/159
68 | 'sphinx_autodoc_typehints',
69 | ]
70 |
71 | # Add any paths that contain templates here, relative to this directory.
72 | templates_path = ['_templates']
73 |
74 | # The suffix(es) of source filenames.
75 | # You can specify multiple suffix as a list of string:
76 | source_suffix = ['.rst']
77 |
78 | # The master toctree document.
79 | master_doc = 'index'
80 |
81 | # The language for content autogenerated by Sphinx. Refer to documentation
82 | # for a list of supported languages.
83 | #
84 | # This is also used if you do content translation via gettext catalogs.
85 | # Usually you set "language" from the command line for these cases.
86 | language = 'en'
87 |
88 | # List of patterns, relative to source directory, that match files and
89 | # directories to ignore when looking for source files.
90 | # This patterns also effect to html_static_path and html_extra_path
91 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
92 |
93 | # The name of the Pygments (syntax highlighting) style to use.
94 | pygments_style = 'sphinx'
95 |
96 | # If true, `todo` and `todoList` produce output, else they produce nothing.
97 | todo_include_todos = True
98 |
99 |
100 | # -- Options for HTML output ----------------------------------------------
101 |
102 | # The theme to use for HTML and HTML Help pages. See the documentation for
103 | # a list of builtin themes.
104 | html_theme = 'alabaster'
105 |
106 | # Theme options are theme-specific and customize the look and feel of a theme
107 | # further. For a list of options available for each theme, see the
108 | # documentation.
109 | html_theme_options = {}
110 |
111 | # Add any paths that contain custom static files (such as style sheets) here,
112 | # relative to this directory. They are copied after the builtin static files,
113 | # so a file named "default.css" will overwrite the builtin "default.css".
114 | html_static_path = ['_static']
115 |
116 | # Custom sidebar templates, must be a dictionary that maps document names
117 | # to template names.
118 | #
119 | # This is required for the alabaster theme
120 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
121 | html_sidebars = {
122 | '**': [
123 | 'about.html',
124 | 'navigation.html',
125 | 'moreinfo.html',
126 | 'searchbox.html',
127 | ],
128 | }
129 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to wemake-django-template's documentation!
2 | ==================================================
3 |
4 |
5 | What this project is all about?
6 | The main idea of this project is to provide a fully configured
7 | template for ``django`` projects, where code quality, testing,
8 | documentation, security, and scalability are number one priorities.
9 |
10 | This template is a result of implementing
11 | `our processes `_,
12 | it should not be considered as an independent part.
13 |
14 |
15 | Goals
16 | -----
17 |
18 | When developing this template we had several goals in mind:
19 |
20 | - Development environment should be bootstrapped easily,
21 | so we use ``docker-compose`` for that
22 | - Development should be consistent, so we use strict quality and style checks
23 | - Development, testing, and production should have the same environment,
24 | so again we develop, test, and run our apps in ``docker`` containers
25 | - Documentation and codebase are the only sources of truth
26 |
27 |
28 | Limitations
29 | -----------
30 |
31 | This project implies that:
32 |
33 | - You are using ``docker`` for deployment
34 | - You are using Gitlab and Gitlab CI
35 | - You are not using any frontend assets in ``django``,
36 | you store your frontend separately
37 |
38 |
39 | Should I choose this template?
40 | ------------------------------
41 |
42 | This template is oriented on big projects,
43 | when there are multiple people working on it for a long period of time.
44 |
45 | If you want to simply create a working prototype without all these
46 | limitations and workflows - feel free to choose any
47 | `other template `_.
48 |
49 |
50 | How to start
51 | ------------
52 |
53 | You should start with reading the documentation.
54 | Reading order is important.
55 |
56 | There are multiple processes that you need to get familiar with:
57 |
58 | - First time setup phase: what system requirements you must fulfill,
59 | how to install dependencies, how to start your project
60 | - Active development phase: how to make changes, run tests,
61 |
62 |
63 | .. toctree::
64 | :maxdepth: 2
65 | :caption: Setting things up:
66 |
67 | pages/template/overview.rst
68 | pages/template/development.rst
69 | pages/template/django.rst
70 |
71 | .. toctree::
72 | :maxdepth: 2
73 | :caption: Quality assurance:
74 |
75 | pages/template/documentation.rst
76 | pages/template/linters.rst
77 | pages/template/testing.rst
78 | pages/template/security.rst
79 | pages/template/gitlab-ci.rst
80 |
81 | .. toctree::
82 | :maxdepth: 2
83 | :caption: Production:
84 |
85 | pages/template/production-checklist.rst
86 | pages/template/production.rst
87 |
88 | .. toctree::
89 | :maxdepth: 1
90 | :caption: Extras:
91 |
92 | pages/template/upgrading-template.rst
93 | pages/template/faq.rst
94 | pages/template/troubleshooting.rst
95 |
96 |
97 | Indexes and tables
98 | ==================
99 |
100 | * :ref:`genindex`
101 | * :ref:`modindex`
102 | * :ref:`search`
103 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=wemake-django-template
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/pages/project/glossary.rst:
--------------------------------------------------------------------------------
1 | Words that have special meaning in this context
2 | ===============================================
3 |
4 | Global context
5 | --------------
6 |
7 | .. glossary::
8 |
9 | Placeholder API
10 | Remove JSON API that we use as a fake service.
11 | https://jsonplaceholder.typicode.com/
12 |
13 | Identity context
14 | ----------------
15 |
16 | .. glossary::
17 |
18 | lead_id
19 | Remote integer-based ID for our users generated by :term:`Placeholder API`.
20 |
21 | Pictures context
22 | ----------------
23 |
24 | .. glossary::
25 |
26 | Dashboard
27 | A place where we show :term:`picture` items.
28 |
29 | Picture
30 | A digital image that we fetch from :term:`Placeholder API`
31 | to show to our users.
32 |
33 | Favourites
34 | Locally saved links to pictures that user decided to have in their profile.
35 |
--------------------------------------------------------------------------------
/docs/pages/template/development.rst:
--------------------------------------------------------------------------------
1 | Development
2 | ===========
3 |
4 | Our development process is focused on high quality and development comfort.
5 | We use tools that are proven to be the best in class.
6 |
7 | There are two possible ways to develop your apps.
8 |
9 | 1. local development
10 | 2. development inside ``docker``
11 |
12 | You can choose one or use both at the same time.
13 | How to choose what method should you use?
14 |
15 | Local development is much easier and much faster.
16 | You can choose it if you don't have too many infrastructure dependencies.
17 | That's a default option for the new projects.
18 |
19 | Choosing ``docker`` development means that you already have a complex
20 | setup of different technologies, containers, networks, etc.
21 | This is a default option for older and more complicated projects.
22 |
23 |
24 | Dependencies
25 | ------------
26 |
27 | We use ``poetry`` to manage dependencies.
28 | So, please do not use ``virtualenv`` or ``pip`` directly.
29 | Before going any further, please,
30 | take a moment to read the `official documentation `_
31 | about ``poetry`` to know some basics.
32 |
33 | If you are using ``docker`` then prepend ``docker compose run --rm web``
34 | before any of those commands to execute them.
35 |
36 | Please, note that you don't need almost all of them with ``docker``.
37 | You can just skip this sub-section completely.
38 | Go right to `Development with docker`_.
39 |
40 | Installing dependencies
41 | ~~~~~~~~~~~~~~~~~~~~~~~
42 |
43 | You do not need to run any of these command for ``docker`` based development,
44 | since it is already executed inside ``Dockerfile``.
45 |
46 | Please, note that ``poetry`` will automatically create a ``virtualenv`` for
47 | this project. It will use you current ``python`` version.
48 | To install all existing dependencies run:
49 |
50 | .. code:: bash
51 |
52 | poetry install
53 |
54 | To install dependencies for production use, you will need to run:
55 |
56 | .. code:: bash
57 |
58 | poetry install --no-dev
59 |
60 | And to activate ``virtualenv`` created by ``poetry`` run:
61 |
62 | .. code:: bash
63 |
64 | poetry shell
65 |
66 | Adding new dependencies
67 | ~~~~~~~~~~~~~~~~~~~~~~~
68 |
69 | To add a new dependency you can run:
70 |
71 | - ``poetry add django`` to install ``django`` as a production dependency
72 | - ``poetry add --dev pytest`` to install ``pytest``
73 | as a development dependency
74 |
75 | This command might be used with ``docker``.
76 |
77 | Updating poetry version
78 | ~~~~~~~~~~~~~~~~~~~~~~~
79 |
80 | Package managers should also be pinned very strictly.
81 | We had a lot of problems in production
82 | because we were not pinning package manager versions.
83 |
84 | This can result in broken ``lock`` files, inconsistent installation process,
85 | bizarre bugs, and missing packages. You do not want to experience that!
86 |
87 | How can we have the same ``poetry`` version for all users in a project?
88 | That's where ``[build-system]`` tag shines. It specifies the exact version of
89 | your ``poetry`` installation that must be used for the project.
90 | Version mismatch will fail your build.
91 |
92 | When you want to update ``poetry``, you have to bump it in several places:
93 |
94 | 1. ``pyproject.toml``
95 | 2. ``docker/django/Dockerfile``
96 |
97 | Then you are fine!
98 |
99 |
100 | Development with docker
101 | -----------------------
102 |
103 | To start development server inside ``docker`` you will need to run:
104 |
105 | .. code:: bash
106 |
107 | export DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 # enable buildkit
108 | docker compose build
109 | docker compose run --rm web python manage.py migrate
110 | docker compose up
111 |
112 | Running scripts inside docker
113 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
114 |
115 | As we have already mentioned inside the previous section
116 | we use ``docker compose run`` to run scripts inside docker.
117 |
118 | What do you need to know about it?
119 |
120 | 1. You can run anything you want: ``poetry``, ``python``, ``sh``, etc
121 | 2. Most likely it will have a permanent effect, due to ``docker volumes``
122 | 3. You need to use ``--rm`` to automatically remove this container afterward
123 |
124 | **Note**: ``docker`` commands do not need to use ``virtualenv`` at all.
125 |
126 | Local development
127 | -----------------
128 |
129 | When cloning a project for the first time you may
130 | need to configure it properly,
131 | see :ref:`django` section for more information.
132 |
133 | **Note**, that you will need to activate ``virtualenv`` created
134 | by ``poetry`` before running any of these commands.
135 | **Note**, that you only need to run these commands once per project.
136 |
137 | Local database
138 | ~~~~~~~~~~~~~~
139 |
140 | When using local development environment without ``docker``,
141 | you will need a ``postgres`` up and running.
142 | To create new development database run
143 | (make sure that database and user names are correct for your case):
144 |
145 | .. code:: bash
146 |
147 | psql postgres -U postgres -f scripts/create_dev_database.sql
148 |
149 | Then migrate your database:
150 |
151 | .. code:: bash
152 |
153 | python manage.py migrate
154 |
155 | Running project
156 | ~~~~~~~~~~~~~~~
157 |
158 | If you have reached this point, you should be able to run the project.
159 |
160 | .. code:: bash
161 |
162 | python manage.py runserver
163 |
--------------------------------------------------------------------------------
/docs/pages/template/django.rst:
--------------------------------------------------------------------------------
1 | .. _django:
2 |
3 | Django
4 | ======
5 |
6 |
7 | Configuration
8 | -------------
9 |
10 | We share the same configuration structure for almost every possible
11 | environment.
12 |
13 | We use:
14 |
15 | - ``django-split-settings`` to organize ``django``
16 | settings into multiple files and directories
17 | - ``.env`` files to store secret configuration
18 | - ``python-decouple`` to load ``.env`` files into ``django``
19 |
20 | Components
21 | ~~~~~~~~~~
22 |
23 | If you have some specific components like ``celery`` or ``mailgun`` installed,
24 | they could be configured in separate files.
25 | Just create a new file in ``server/settings/components/``.
26 | Then add it into ``server/settings/__init__.py``.
27 |
28 | Environments
29 | ~~~~~~~~~~~~
30 |
31 | To run ``django`` on different environments just
32 | specify ``DJANGO_ENV`` environment variable.
33 | It must have the same name as one of the files
34 | from ``server/settings/environments/``.
35 | Then, values from this file will override other settings.
36 |
37 | Local settings
38 | ~~~~~~~~~~~~~~
39 |
40 | If you need some specific local configuration tweaks,
41 | you can create file ``server/settings/environments/local.py.template``
42 | to ``server/settings/environments/local.py``.
43 | It will be loaded into your settings automatically if exists.
44 |
45 | .. code:: bash
46 |
47 | cp server/settings/environments/local.py.template server/settings/environments/local.py
48 |
49 | See ``local.py.template`` version for the reference.
50 |
51 |
52 | Secret settings
53 | ---------------
54 |
55 | We share the same mechanism for secret settings for all our tools.
56 | We use ``.env`` files for ``django``, ``postgres``, ``docker``, etc.
57 |
58 | Initially, you will need to copy file
59 | ``config/.env.template`` to ``config/.env``:
60 |
61 | .. code:: bash
62 |
63 | cp config/.env.template config/.env
64 |
65 | When adding any new secret ``django`` settings you will need to:
66 |
67 | 1. Add new key and value to ``config/.env``
68 | 2. Add new key without value to ``config/.env.template``,
69 | add a comment on how to get this value for other users
70 | 3. Add new variable inside ``django`` settings
71 | 4. Use ``python-decouple`` to load this ``env`` variable like so:
72 | ``MY_SECRET = config('MY_SECRET')``
73 |
74 |
75 | Secret settings in production
76 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
77 |
78 | We do not store our secret settings inside our source code.
79 | All sensible settings are stored in ``config/.env`` file,
80 | which is not tracked by the version control.
81 |
82 | So, how do we store secrets? We store them as secret environment variables
83 | in `GitLab CI `_.
84 | Then we use `dump-env `_
85 | to dump variables from both environment and ``.env`` file template.
86 | Then, this file is copied inside ``docker`` image and when
87 | this image is built - everything is ready for production.
88 |
89 | Here's an example:
90 |
91 | 1. We add a ``SECRET_DJANGO_SECRET_KEY`` variable to Gitlab CI secret variables
92 | 2. Then ``dump-env`` dumps ``SECRET_DJANGO_SECRET_KEY``
93 | as ``DJANGO_SECRET_KEY`` and writes it to ``config/.env`` file
94 | 3. Then it is loaded by ``django`` inside the settings:
95 | ``SECRET_KEY = config('DJANGO_SECRET_KEY')``
96 |
97 | However, there are different options to store secret settings:
98 |
99 | - `ansible-vault `_
100 | - `git-secret `_
101 | - `Vault `_
102 |
103 | Depending on a project we use different tools.
104 | With ``dump-env`` being the default and the simplest one.
105 |
106 |
107 | Extensions
108 | ----------
109 |
110 | We use different ``django`` extensions that make your life easier.
111 | Here's a full list of the extensions for both development and production:
112 |
113 | - `django-split-settings`_ - organize
114 | ``django`` settings into multiple files and directories.
115 | Easily override and modify settings.
116 | Use wildcards in settings file paths and mark settings files as optional
117 | - `django-axes`_ - keep track
118 | of failed login attempts in ``django`` powered sites
119 | - `django-csp`_ - `Content Security Policy`_ for ``django``
120 | - `django-referrer-policy`_ - middleware implementing the `Referrer-Policy`_
121 | - `django-health-check`_ - checks for various conditions and provides reports
122 | when anomalous behavior is detected
123 | - `django-add-default-value`_ - this django Migration Operation can be used to
124 | transfer a Fields default value to the database scheme
125 | - `django-deprecate-fields`_ - this package allows deprecating model fields and
126 | allows removing them in a backwards compatible manner
127 | - `django-migration-linter`_ - detect backward incompatible migrations for
128 | your django project
129 | - `zero-downtime-migrations`_ - apply ``django`` migrations on PostgreSql
130 | without long locks on tables
131 |
132 | Development only extensions:
133 |
134 | - `django-debug-toolbar`_ - a configurable set of panels that
135 | display various debug information about the current request/response
136 | - `django-querycount`_ - middleware that prints the number
137 | of DB queries to the runserver console
138 | - `nplusone`_ - auto-detecting the `n+1 queries problem`_ in ``django``
139 |
140 | .. _django-split-settings: https://github.com/sobolevn/django-split-settings
141 | .. _django-axes: https://github.com/jazzband/django-axes
142 | .. _django-csp: https://github.com/mozilla/django-csp
143 | .. _`Content Security Policy`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
144 | .. _django-referrer-policy: https://github.com/ubernostrum/django-referrer-policy
145 | .. _`Referrer-Policy`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
146 | .. _django-health-check: https://github.com/KristianOellegaard/django-health-check
147 | .. _django-add-default-value: https://github.com/3YOURMIND/django-add-default-value
148 | .. _django-deprecate-fields: https://github.com/3YOURMIND/django-deprecate-fields
149 | .. _django-migration-linter: https://github.com/3YOURMIND/django-migration-linter
150 | .. _zero-downtime-migrations: https://github.com/yandex/zero-downtime-migrations
151 | .. _django-debug-toolbar: https://github.com/jazzband/django-debug-toolbar
152 | .. _django-querycount: https://github.com/bradmontgomery/django-querycount
153 | .. _nplusone: https://github.com/jmcarp/nplusone
154 | .. _`n+1 queries problem`: https://stackoverflow.com/questions/97197/what-is-the-n1-select-query-issue
155 |
156 |
157 | Further reading
158 | ---------------
159 |
160 | - `django-split-settings tutorial `_
161 | - `docker env-file docs `_
162 |
163 |
164 | Django admin
165 | ~~~~~~~~~~~~
166 |
167 | - `Django Admin Cookbook `_
168 |
--------------------------------------------------------------------------------
/docs/pages/template/documentation.rst:
--------------------------------------------------------------------------------
1 | Documentation
2 | =============
3 |
4 | `We `_ write a lot of documentation.
5 | Since we believe, that documentation is a crucial factor
6 | which defines project success or failure.
7 |
8 | Here's how we write docs for ``django`` projects.
9 |
10 |
11 | Dependencies
12 | ------------
13 |
14 | We are using ``sphinx`` as a documentation builder.
15 | We use ``sphinx.ext.napoleon`` to write
16 | pretty docstrings inside the source code.
17 | We also use ``sphinx_autodoc_typehints`` to inject type annotations into docs.
18 |
19 | We use ``pyproject.toml`` as the source of truth for our deps.
20 | All docs-related packages are stored under ``docs`` extra.
21 |
22 | To install them use:
23 |
24 | .. code:: bash
25 |
26 | poetry install -E docs
27 |
28 |
29 | Structure
30 | ---------
31 |
32 | We use a clear structure for this documentation.
33 |
34 | - ``pages/template`` contains docs
35 | from `wemake-django-template `_.
36 | These files should not be modified locally.
37 | If you have any kind of question or problems,
38 | just open an issue `on github `_
39 | - ``pages/project`` contains everything related to the project itself.
40 | Usage examples, an auto-generated documentation from your source code,
41 | configuration, business, and project goals
42 | - ``documents`` contains different non-sphinx documents
43 | like ``doc`` files, spreadsheets, and mockups
44 |
45 | Please, do not mix it up.
46 |
47 | How to structure project docs
48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
49 |
50 | It is a good practice to write a single ``rst`` document
51 | for every single ``py`` file.
52 | Obviously, ``rst`` structure fully copies the structure of your source code.
53 | This way it is very easy to navigate through the docs,
54 | since you already know the structure.
55 |
56 | For each ``django`` application we tend to create
57 | a file called ``index.rst`` which is considered
58 | the main file for the application.
59 |
60 | And ``pages/project/index.rst`` is the main file for the whole project.
61 |
62 |
63 | How to contribute
64 | -----------------
65 |
66 | We enforce everyone to write clean and explaining documentation.
67 | However, there are several rules about writing styling.
68 |
69 | We are using `doc8 `_ to validate our docs.
70 | So, here's the command to do it:
71 |
72 | .. code:: bash
73 |
74 | doc8 ./docs
75 |
76 | This is also used in our CI process, so your build will fail
77 | if there are violations.
78 |
79 |
80 | Useful plugins
81 | --------------
82 |
83 | Some ``sphinx`` plugins are not included, since they are very specific.
84 | However, they are very useful:
85 |
86 | - `sphinxcontrib-mermaid `_ - sphinx plugin to create general flowcharts, sequence and gantt diagrams
87 | - `sphinxcontrib-plantuml `_ - sphinx plugin to create UML diagrams
88 | - `nbsphinx `_ - sphinx plugin to embed ``ipython`` notebooks into your docs
89 |
90 |
91 | Further reading
92 | ---------------
93 |
94 | - `sphinx `_
95 | - `sphinx with django `_
96 | - `sphinx-autodoc-typehints `_
97 | - `Architecture Decision Record (ADR) `_
98 | - `adr-tools `_
99 |
--------------------------------------------------------------------------------
/docs/pages/template/faq.rst:
--------------------------------------------------------------------------------
1 | Frequently asked questions
2 | ==========================
3 |
4 | Will you ever support drf / celery / flask / gevent?
5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 |
7 | No. This template is focused on bringing best practices to ``django``
8 | projects. It only includes workflow and configuration for this framework.
9 |
10 | Other tools are not mandatory. And can easily be added by a developer.
11 |
12 | Will you have an build-time option to include or change anything?
13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 |
15 | No, we believe that options bring inconsistencies to the project.
16 | You can also make the wrong choice. So, we are protecting you from that.
17 |
18 | You can only have options that are already present in this template.
19 | Fork it, if you do not agree with this policy.
20 |
21 | This code quality is unbearable! Can I turn it off?
22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23 |
24 | Of course, no one can stop you from that.
25 | But what the point in using this template then?
26 |
27 | Our code quality defined by this template is minimally acceptable.
28 | We know tools to make it even better. But they are not included.
29 | Since they are literally hardcore.
30 |
--------------------------------------------------------------------------------
/docs/pages/template/gitlab-ci.rst:
--------------------------------------------------------------------------------
1 | Gitlab CI
2 | =========
3 |
4 | We use ``Gitlab CI`` to build our containers, test it,
5 | and store them in the internal registry.
6 |
7 | These images are then pulled into the production servers.
8 |
9 |
10 | Configuration
11 | -------------
12 |
13 | All configuration is done inside ``.gitlab-ci.yml``.
14 |
15 |
16 | Pipelines
17 | ---------
18 |
19 | We have two pipelines configured: for ``master`` and other branches.
20 | That's how it works: we only run testing for feature branches and do the whole
21 | building/testing/deploying process for the ``master`` branch.
22 |
23 | This allows us to speed up development process.
24 |
25 |
26 | Automatic dependencies update
27 | -----------------------------
28 |
29 | You can use `dependabot `_
30 | to enable automatic dependencies updates via Pull Requests to your repository.
31 | Similar to the original template repository: `list of pull requests `_.
32 |
33 | It is available to both Github and Gitlab.
34 | But, for Gitlab version you currently have to update your `.gitlab-ci.yml `_.
35 |
36 |
37 | Secret variables
38 | ----------------
39 |
40 | If some real secret variables are required, then you can use `gitlab secrets `_.
41 | And these kind of variables are required *most* of the time.
42 |
43 | See :ref:`django` on how to use ``dump-env`` and ``gitlab-ci`` together.
44 |
45 |
46 | Documentation
47 | -------------
48 | After each deploy from master branch this documentation compiles into nice looking html page.
49 | See `gitlab pages info `_.
50 |
51 |
52 | Further reading
53 | ---------------
54 |
55 | - `Container Registry `_
56 | - `Gitlab CI/CD `_
57 |
--------------------------------------------------------------------------------
/docs/pages/template/linters.rst:
--------------------------------------------------------------------------------
1 | .. _linters:
2 |
3 | Linters
4 | =======
5 |
6 | This project uses several linters to make coding style consistent.
7 | All configuration is stored inside ``setup.cfg``.
8 |
9 |
10 | wemake-python-styleguide
11 | ------------------------
12 |
13 | ``wemake-python-styleguide`` is a ``flake8`` based plugin.
14 | And it is also the strictest and most opinionated python linter ever.
15 | See `wemake-python-styleguide `_
16 | docs.
17 |
18 | Things that are included in the linting process:
19 |
20 | - `flake8 `_ is used a general tool for linting
21 | - `isort `_ is used to validate ``import`` order
22 | - `bandit `_ for static security checks
23 | - `eradicate `_ to find dead code
24 | - and more!
25 |
26 | Running linting process for all ``python`` files in the project:
27 |
28 | .. code:: bash
29 |
30 | flake8 .
31 |
32 | Extra plugins
33 | ~~~~~~~~~~~~~
34 |
35 | We also use some extra plugins for ``flake8``
36 | that are not bundled with ``wemake-python-styleguide``:
37 |
38 | - `flake8-pytest `_ - ensures that ``pytest`` best practices are used
39 | - `flake8-pytest-style `_ - ensures that ``pytest`` tests and fixtures are written in a single style
40 | - `flake8-django `_ - plugin to enforce best practices in a ``django`` project
41 |
42 |
43 | django-migration-linter
44 | -----------------------
45 |
46 | We use ``django-migration-linter`` to find backward incompatible migrations.
47 | It allows us to write 0-downtime friendly code.
48 |
49 | See `django-migration-linter `_
50 | docs, it contains a lot of useful information about ways and tools to do it.
51 |
52 | That's how this check is executed:
53 |
54 | .. code:: bash
55 |
56 | python manage.py lintmigrations
57 |
58 | Important note: you might want to exclude some packages with broken migrations.
59 | Sometimes, there's nothing we can do about it.
60 |
61 |
62 | yamllint
63 | --------
64 |
65 | Is used to lint your ``yaml`` files.
66 | See `yamllint `_ docs.
67 |
68 | .. code:: bash
69 |
70 | yamllint -d '{"extends": "default", "ignore": ".venv"}' -s .
71 |
72 |
73 | djlint
74 | ------
75 |
76 | Is used to lint and format your ``html`` files.
77 | See `djlint `_ docs.
78 |
79 | .. code:: bash
80 |
81 | djlint --check server
82 | djlint --lint server
83 |
84 |
85 | dotenv-linter
86 | -------------
87 |
88 | Is used to lint your ``.env`` files.
89 | See `dotenv-linter `_ docs.
90 |
91 | .. code:: bash
92 |
93 | dotenv-linter config/.env config/.env.template
94 |
95 |
96 | polint and dennis
97 | -----------------
98 |
99 | Are used to lint your ``.po`` files.
100 | See `polint `_ docs.
101 | Also see `dennis `_ docs.
102 |
103 | .. code:: bash
104 |
105 | polint -i location,unsorted locale
106 | dennis-cmd lint --errorsonly locale
107 |
108 |
109 | Packaging
110 | ---------
111 |
112 | We also use ``pip`` and ``poetry`` self checks to be sure
113 | that packaging works correctly.
114 |
115 | .. code:: bash
116 |
117 | poetry check && pip check
118 |
119 |
120 | Linters that are not included
121 | -----------------------------
122 |
123 | Sometimes we use several other linters that are not included.
124 | That's because they require another technology stack to be installed
125 | or just out of scope.
126 |
127 | We also recommend to check the list of linters
128 | `recommended by wemake-python-styleguide `_.
129 |
130 | Here's the list of these linters. You may still find them useful.
131 |
132 | shellcheck
133 | ~~~~~~~~~~
134 |
135 | This linter is used to lint your ``.sh`` files.
136 | See `shellcheck `_ docs.
137 |
138 | hadolint
139 | ~~~~~~~~
140 |
141 | This linter is used to lint your ``Dockerfile`` syntax.
142 | See `hadolint `_
143 |
--------------------------------------------------------------------------------
/docs/pages/template/overview.rst:
--------------------------------------------------------------------------------
1 | Overview
2 | ========
3 |
4 |
5 | System requirements
6 | -------------------
7 |
8 | - ``git`` with a version at least ``2.16`` or higher
9 | - ``docker`` with a version at least ``18.02`` or higher
10 | - ``docker-compose`` with a version at least ``1.21`` or higher
11 | - ``python`` with exact version, see ``pyproject.toml``
12 |
13 |
14 | Architecture
15 | ------------
16 |
17 | config
18 | ~~~~~~
19 |
20 | - ``config/.env.template`` - a basic example of what keys must be contained in
21 | your ``.env`` file, this file is committed to VCS
22 | and must not contain private or secret values
23 | - ``config/.env`` - main file for secret configuration,
24 | contains private and secret values, should not be committed to VCS
25 |
26 | root project
27 | ~~~~~~~~~~~~
28 |
29 | - ``README.md`` - main readme file, it specifies the entry
30 | point to the project's documentation
31 | - ``.dockerignore`` - specifies what files should not be
32 | copied to the ``docker`` image
33 | - ``.editorconfig`` - file with format specification.
34 | You need to install the required plugin for your IDE in order to enable it
35 | - ``.gitignore`` - file that specifies
36 | what should we commit into the repository and we should not
37 | - ``.gitlab-ci.yml`` - GitLab CI configuration file.
38 | It basically defines what to do with your project
39 | after pushing it to the repository. Currently it is used for testing
40 | and releasing a ``docker`` image
41 | - ``docker-compose.yml`` - this the file specifies ``docker`` services
42 | that are needed for development and testing
43 | - ``docker-compose.override.yml`` - local override for ``docker-compose``.
44 | Is applied automatically and implicitly when
45 | no arguments provided to ``docker-compose`` command
46 | - ``manage.py`` - main file for your ``django`` project.
47 | Used as an entry point for the ``django`` project
48 | - ``pyproject.toml`` - main file of the project.
49 | It defines the project's dependencies.
50 | - ``poetry.lock`` - lock file for dependencies.
51 | It is used to install exactly the same versions of dependencies on each build
52 | - ``setup.cfg`` - configuration file, that is used by all tools in this project
53 | - ``locale/`` - helper folder, that is used to store locale data,
54 | empty by default
55 | - ``scripts/`` - helper folder, that contains various development scripts
56 | and teardown for local development
57 |
58 | server
59 | ~~~~~~
60 |
61 | - ``server/__init__.py`` - package definition, empty file
62 | - ``server/urls.py`` - ``django`` `urls definition `_
63 | - ``server/wsgi.py`` - ``django`` `wsgi definition `_
64 | - ``server/asgi.py`` - ``django`` `asgi definition `_
65 | - ``server/apps/`` - place to put all your apps into
66 | - ``server/apps/main`` - ``django`` application, used as an example,
67 | could be removed
68 | - ``server/settings`` - settings defined with ``django-split-settings``,
69 | see this `tutorial `_
70 | for more information
71 | - ``server/templates`` - external folder for ``django`` templates,
72 | used for simple files as ``robots.txt`` and so on
73 |
74 | docker
75 | ~~~~~~
76 |
77 | - ``docker/docker-compose.prod.yml`` - additional service definition file
78 | used for production
79 | - ``docker/django/Dockerfile`` - ``django`` container definition,
80 | used both for development and production
81 | - ``docker/django/entrypoint.sh`` - entry point script that is used
82 | when ``django`` container is starting
83 | - ``docker/django/gunicorn_config.py`` - that's how we
84 | configure ``gunicorn`` runner
85 | - ``docker/django/gunicorn.sh`` - production script
86 | for ``django`` using ``gunicorn``
87 | - ``docker/django/ci.sh`` - file that specifies all possible checks that
88 | we execute during our CI process for django
89 | - ``docker/caddy/Caddyfile`` - configuration file for Caddy webserver
90 | - ``docker/caddy/ci.sh`` - file that specifies all possible checks that
91 | we execute during our CI process for caddy
92 |
93 | tests
94 | ~~~~~
95 |
96 | - ``tests/test_server`` - tests that ensures that basic ``django``
97 | stuff is working, should not be removed
98 | - ``tests/test_apps/test_main`` - example tests for the ``django`` app,
99 | could be removed
100 | - ``tests/conftest.py`` - main configuration file for ``pytest`` runner
101 |
102 | docs
103 | ~~~~
104 |
105 | - ``docs/Makefile`` - command file that builds the documentation for Unix
106 | - ``docs/make.bat`` - command file for Windows
107 | - ``docs/conf.py`` - ``sphinx`` configuration file
108 | - ``docs/index.rst`` - main documentation file, used as an entry point
109 | - ``docs/pages/project`` - folder that will contain
110 | documentation written by you!
111 | - ``docs/pages/template`` - folder that contains documentation that
112 | is common for each project built with this template
113 | - ``docs/documents`` - folder that should contain any documents you have:
114 | spreadsheets, images, requirements, presentations, etc
115 | - ``docs/README.rst`` - helper file for this directory,
116 | just tells what to do next
117 |
118 |
119 | Container internals
120 | -------------------
121 |
122 | We use the ``docker-compose`` to link different containers together.
123 | We also utilize different ``docker`` networks to control access.
124 |
125 | Some containers might have long starting times, for example:
126 |
127 | - ``postgres``
128 | - ``rabbitmq``
129 | - frontend, like ``node.js``
130 |
131 | We start containers with ``tini``.
132 | Because this way we have a proper signal handling
133 | and eliminate zombie processes.
134 | Read the `official docs `_ to know more.
135 |
--------------------------------------------------------------------------------
/docs/pages/template/production-checklist.rst:
--------------------------------------------------------------------------------
1 | .. _`going-to-production`:
2 |
3 | Going to production
4 | ===================
5 |
6 | This section covers everything you need to know before going to production.
7 |
8 |
9 | Django
10 | ------
11 |
12 | Checks
13 | ~~~~~~
14 |
15 | Before going to production make sure you have checked everything:
16 |
17 | 1. Migrations are up-to-date
18 | 2. Static files are all present
19 | 3. There are no security or other ``django`` warnings
20 |
21 | Checking migrations, static files,
22 | and security is done inside ``ci.sh`` script.
23 |
24 | We check that there are no unapplied migrations:
25 |
26 | .. code :: bash
27 |
28 | python manage.py makemigrations --dry-run --check
29 |
30 | If you have forgotten to create a migration and changed the model,
31 | you will see an error on this line.
32 |
33 | We also check that static files can be collected:
34 |
35 | .. code :: bash
36 |
37 | DJANGO_ENV=production python manage.py collectstatic --no-input --dry-run
38 |
39 | However, this check does not cover all the cases.
40 | Sometimes ``ManifestStaticFilesStorage`` will fail on real cases,
41 | but will pass with ``--dry-run`` option.
42 | You can disable ``--dry-run`` option if you know what you are doing.
43 | Be careful with this option, when working with auto-uploading
44 | your static files to any kind of CDNs.
45 |
46 | That's how we check ``django`` warnings:
47 |
48 | .. code:: bash
49 |
50 | DJANGO_ENV=production python manage.py check --deploy --fail-level WARNING
51 |
52 | These warnings are raised by ``django``
53 | when it detects any configuration issues.
54 |
55 | This command should give not warnings or errors.
56 | It is bundled into ``docker``, so the container will not work with any warnings.
57 |
58 | Static and media files
59 | ~~~~~~~~~~~~~~~~~~~~~~
60 |
61 | We use ``/var/www/django`` folder to store our media
62 | and static files in production as ``/var/www/django/static``
63 | and ``/var/www/django/media``.
64 | Docker uses these two folders as named volumes.
65 | And later these volumes are also mounted to ``caddy``
66 | with ``ro`` mode so it possible to read their contents.
67 |
68 | To find the exact location of these files on your host
69 | you will need to do the following:
70 |
71 | .. code:: bash
72 |
73 | docker volume ls # to find volumes' names
74 | docker volume inspect VOLUME_NAME
75 |
76 | Sometimes storing your media files inside a container is not a good idea.
77 | Use ``CDN`` when you have a lot of user content
78 | or it is very important not to lose it.
79 | There are `helper libraries `_
80 | to bind ``django`` and these services.
81 |
82 | If you don't need ``media`` files support, just remove the volumes.
83 |
84 | Migrations
85 | ~~~~~~~~~~
86 |
87 | We do run migration in the ``gunicorn.sh`` by default.
88 | Why do we do this? Because that's probably the easiest way to do it.
89 | But it clearly has some disadvantages:
90 |
91 | - When scaling your container for multiple nodes you will have multiple
92 | threads running the same migrations. And it might be a problem since
93 | migrations do not guarantee that it will work this way.
94 | - You can perform some operations multiple times
95 | - Possible other evil things may happen
96 |
97 | So, what to do in this case?
98 | Well, you can do whatever it takes to run migrations in a single thread.
99 | For example, you can create a separate container to do just that.
100 | Other options are fine as well.
101 |
102 |
103 | Postgres
104 | --------
105 |
106 | Sometimes using ``postgres`` inside a container
107 | `is not a good idea `_.
108 | So, what should be done in this case?
109 |
110 | First of all, move your database ``docker`` service definition
111 | inside ``docker-compose.override.yml``.
112 | Doing so will not affect development,
113 | but will remove database service from production.
114 | Next, you will need to specify `extra_hosts `_
115 | to contain your ``postgresql`` address.
116 | Lastly, you would need to add new hosts to ``pg_hba.conf``.
117 |
118 | `Here `_
119 | is a nice tutorial about this topic.
120 |
121 |
122 | Caddy
123 | -----
124 |
125 | Let's Encrypt
126 | ~~~~~~~~~~~~~
127 |
128 | We are using ``Caddy`` and ``Let's Encrypt`` for HTTPS.
129 | The Caddy webserver used in the default configuration will get
130 | you a valid certificate from ``Let's Encrypt`` and update it automatically.
131 | All you need to do to enable this is to make sure
132 | that your DNS records are pointing to the server Caddy runs on.
133 |
134 | Read more: `Automatic HTTPS `_
135 | in Caddy docs.
136 |
137 | Caddyfile validation
138 | ~~~~~~~~~~~~~~~~~~~~
139 |
140 | You can also run ``-validate`` command to validate ``Caddyfile`` contents.
141 |
142 | Here's it would look like:
143 |
144 | .. code:: bash
145 |
146 | docker compose-f docker-compose.yml -f docker/docker-compose.prod.yml
147 | run --rm caddy -validate
148 |
149 | This check is not included in the pipeline by default,
150 | because it is quite long to start all the machinery for this single check.
151 |
152 | Disabling HTTPS
153 | ~~~~~~~~~~~~~~~
154 |
155 | You would need to `disable `_
156 | ``https`` inside ``Caddy`` and in production settings for Django.
157 | Because Django itself also redirects to ``https``.
158 | See `docs `_.
159 |
160 | You would also need to disable ``manage.py check``
161 | in ``docker/ci.sh``.
162 | Otherwise, your application won't start,
163 | it would not pass ``django``'s security checks.
164 |
165 | Disabling WWW subdomain
166 | ~~~~~~~~~~~~~~~~~~~~~~~
167 |
168 | If you for some reason do not require ``www.`` subdomain,
169 | then delete ``www.{$DOMAIN_NAME}`` section from ``Caddyfile``.
170 |
171 | Third-Level domains
172 | ~~~~~~~~~~~~~~~~~~~
173 |
174 | You have to disable ``www`` subdomain if
175 | your app works on third-level domains like:
176 |
177 | - ``kira.wemake.services``
178 | - ``support.myapp.com``
179 |
180 | Otherwise, ``Caddy`` will server redirects to ``www.example.yourdomain.com``.
181 |
182 |
183 | Further reading
184 | ---------------
185 |
186 | - Django's deployment `checklist `_
187 |
--------------------------------------------------------------------------------
/docs/pages/template/production.rst:
--------------------------------------------------------------------------------
1 | Production
2 | ==========
3 |
4 | We use different tools and setup for production.
5 | We do not fully provide this part with the template. Why?
6 |
7 | 1. It requires a lot of server configuration
8 | 2. It heavily depends on your needs: performance, price, technology, etc
9 | 3. It is possible to show some vulnerable parts to possible attackers
10 |
11 | So, you will need to deploy your application by yourself.
12 | Here, we would like to cover some basic things that are not changed
13 | from deployment strategy.
14 |
15 | The easiest deployment strategy for small apps is ``docker-compose`` and
16 | ``systemd`` inside a host operating system.
17 |
18 |
19 | Production configuration
20 | ------------------------
21 |
22 | You will need to specify extra configuration
23 | to run ``docker-compose`` in production.
24 | Since production build also uses ``caddy``,
25 | which is not required into the development build.
26 |
27 | .. code:: bash
28 |
29 | docker compose-f docker-compose.yml -f docker/docker-compose.prod.yml up
30 |
31 |
32 | Pulling pre-built images
33 | ------------------------
34 |
35 | You will need to pull pre-built images from ``Gitlab`` to run them.
36 | How to do that?
37 |
38 | The first step is to create a personal access token for this service.
39 | Then, login into your registry with:
40 |
41 | .. code:: bash
42 |
43 | docker login registry.gitlab.your.domain
44 |
45 | And now you are ready to pull your images:
46 |
47 | .. code:: bash
48 |
49 | docker pull your-image:latest
50 |
51 | See `official Gitlab docs `_.
52 |
53 |
54 | Updating already running service
55 | --------------------------------
56 |
57 | If you need to update an already running service,
58 | them you will have to use ``docker service update``
59 | or ``docker stack deploy``.
60 |
61 | Updating existing `service `_.
62 | Updating existing `stack `_.
63 |
64 | Zero-Time Updates
65 | ~~~~~~~~~~~~~~~~~
66 |
67 | Zero-Time Updates can be tricky.
68 | You need to create containers with the new code, update existing services,
69 | wait for the working sessions to be completed, and to shut down old
70 | containers.
71 |
72 |
73 | Further reading
74 | ---------------
75 |
76 | - Production with `docker compose `_
77 | - `Full tutorial `_
78 |
--------------------------------------------------------------------------------
/docs/pages/template/security.rst:
--------------------------------------------------------------------------------
1 | Security
2 | ========
3 |
4 | Security is our first priority.
5 | We try to make projects as secure as possible.
6 | We use a lot of 3rd party tools to achieve that.
7 |
8 |
9 | Django
10 | ------
11 |
12 | Django has a lot of `security-specific settings `_
13 | that are all turned on by default in this template.
14 |
15 | We also :ref:`enforce ` all the best practices
16 | by running ``django`` checks inside CI for each commit.
17 |
18 | We also use a set of custom ``django`` apps
19 | to enforce even more security rules:
20 |
21 | - `django-axes `_ to track and ban repeating access requests
22 | - `django-csp `_ to enforce `Content-Security Policy `_ for our webpages
23 | - `django-http-referrer-policy `_ to enforce `Referrer Policy `_ for our webpages
24 |
25 | And there are also some awesome extensions that are not included:
26 |
27 | - `django-honeypot `_ - django application that provides utilities for preventing automated form spam
28 |
29 | Passwords
30 | ~~~~~~~~~
31 |
32 | We use strong algorithms for password hashing:
33 | ``bcrypt``, ``PBKDF2`` and ``Argon2`` which are known to be secure enough.
34 |
35 |
36 | Dependencies
37 | ------------
38 |
39 | We use `poetry `_ which ensures
40 | that all the dependencies hashes match during the installation process.
41 | Otherwise, the build will fail.
42 | So, it is almost impossible to replace an already existing package
43 | with a malicious one.
44 |
45 | We also use `safety `_
46 | to analyze vulnerable dependencies to prevent the build
47 | to go to the production with known unsafe dependencies.
48 |
49 | .. code:: bash
50 |
51 | safety check
52 |
53 | We also use `Github security alerts `_
54 | for our main template repository.
55 |
56 |
57 | Static analysis
58 | ---------------
59 |
60 | We use ``wemake-python-styleguide`` which
61 | includes `bandit `_ security checks inside.
62 |
63 | You can also install `pyt `_
64 | which is not included by default.
65 | It will include even more static checks for
66 | ``sql`` injections, ``xss`` and others.
67 |
68 |
69 | Dynamic analysis
70 | ----------------
71 |
72 | You can monitor your running application to detect anomalous activities.
73 | Tools to consider:
74 |
75 | - `dagda `_ - a tool to perform static analysis of known vulnerabilities, trojans, viruses, malware & other malicious threats in docker images/containers and to monitor the docker daemon and running docker containers for detecting anomalous activities
76 |
77 | All the tools above are not included into this template.
78 | You have to install them by yourself.
79 |
80 |
81 | Secrets
82 | -------
83 |
84 | We store secrets separately from code. So, it is harder for them to leak.
85 | However, we encourage to use tools like
86 | `truffleHog `_ or `detect-secrets `_ inside your workflow.
87 |
88 | You can also turn on `Gitlab secrets checker `_ which we highly recommend.
89 |
90 |
91 | Audits
92 | ------
93 |
94 | The only way to be sure that your app is secure
95 | is to constantly audit it in production.
96 |
97 | There are different tools to help you:
98 |
99 | - `twa `_ - tiny web auditor that has a lot of security checks for the webpages
100 | - `XSStrike `_ - automated tool to check that your application is not vulnerable to ``xss`` errors
101 | - `docker-bench `_ - a script that checks for dozens of common best-practices around deploying Docker containers in production
102 | - `lynis `_ - a battle-tested security tool for systems running Linux, macOS, or Unix-based operating system
103 | - `trivy `_ - a simple and comprehensive vulnerability scanner for containers
104 |
105 | But, even after all you attempts to secure your application,
106 | it **won't be 100% safe**. Do not fall into this false feeling of security.
107 |
108 |
109 | Further reading
110 | ---------------
111 |
112 | - `Open Web Application Security Project `_
113 | - `Docker security `_
114 | - `AppArmor `_ and `bane `_
115 |
--------------------------------------------------------------------------------
/docs/pages/template/testing.rst:
--------------------------------------------------------------------------------
1 | Testing
2 | =======
3 |
4 | We try to keep our quality standards high.
5 | So, we use different tools to make this possible.
6 |
7 | We use `mypy `_ for optional
8 | static typing.
9 | We run tests with `pytest `_ framework.
10 |
11 |
12 | pytest
13 | ------
14 |
15 | ``pytest`` is the main tool for test discovery, collection, and execution.
16 | It is configured inside ``setup.cfg`` file.
17 |
18 | We use a lot of ``pytest`` plugins that enhance our development experience.
19 | List of these plugins is available inside ``pyproject.toml`` file.
20 |
21 | Running:
22 |
23 | .. code:: bash
24 |
25 | pytest
26 |
27 | We also have some options that are set on each run via ``--addopts``
28 | inside the ``setup.cfg`` file.
29 |
30 | Plugins
31 | ~~~~~~~
32 |
33 | We use different ``pytest`` plugins to make our testing process better.
34 | Here's the full list of things we use:
35 |
36 | - `pytest-django`_ - plugin that introduce a lot of ``django`` specific
37 | helpers, fixtures, and configuration
38 | - `django-test-migrations`_ - plugin to test Django migrations and their order
39 | - `pytest-cov`_ - plugin to measure test coverage
40 | - `pytest-randomly`_ - plugin to execute tests in random order and
41 | also set predictable random seed, so you can easily debug
42 | what went wrong for tests that rely on random behavior
43 | - `pytest-deadfixtures`_ - plugin to find unused or duplicate fixtures
44 | - `pytest-timeout`_ - plugin to raise errors for tests
45 | that take too long to finish, this way you can control test execution speed
46 |
47 | .. _pytest-django: https://github.com/pytest-dev/pytest-django
48 | .. _django-test-migrations: https://github.com/wemake-services/django-test-migrations
49 | .. _pytest-cov: https://github.com/pytest-dev/pytest-cov
50 | .. _pytest-randomly: https://github.com/pytest-dev/pytest-randomly
51 | .. _pytest-deadfixtures: https://github.com/jllorencetti/pytest-deadfixtures
52 | .. _pytest-timeout: https://pypi.org/project/pytest-timeout
53 |
54 | Tweaking tests performance
55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
56 |
57 | There are several options you can provide or remove to make your tests faster:
58 |
59 | - You can use ``pytest-xdist`` together with
60 | ``-n auto`` to schedule several numbers of workers,
61 | sometimes when there are a lot of tests it may increase the testing speed.
62 | But on a small project with a small amount of test it just
63 | gives you an overhead, so removing it (together with ``--boxed``)
64 | will boost your testing performance
65 | - If there are a lot of tests with database access
66 | it may be wise to add
67 | `--reuse-db option `_,
68 | so ``django`` won't recreate database on each test
69 | - If there are a lot of migrations to perform you may also add
70 | `--nomigrations option `_,
71 | so ``django`` won't run all the migrations
72 | and instead will inspect and create models directly
73 | - Removing ``coverage``. Sometimes that an option.
74 | When running tests in TDD style why would you need such a feature?
75 | So, coverage will be calculated when you will ask for it.
76 | That's a huge speed up
77 | - Removing linters. Sometimes you may want to split linting and testing phases.
78 | This might be useful when you have a lot of tests, and you want to run
79 | linters before, so it won't fail your complex testing pyramid with a simple
80 | whitespace violation
81 |
82 |
83 | mypy
84 | ----
85 |
86 | Running ``mypy`` is required before any commit:
87 |
88 | .. code:: bash
89 |
90 | mypy server tests/**/*.py
91 |
92 | This will eliminate a lot of possible ``TypeError`` and other issues
93 | in both ``server/`` and ``tests/`` directories.
94 | We use ``tests/**/*.py`` because ``tests/`` is not a python package,
95 | so it is not importable.
96 |
97 | However, this will not make code 100% safe from errors.
98 | So, both the testing and review process are still required.
99 |
100 | ``mypy`` is configured via ``setup.cfg``.
101 | Read the `docs `_
102 | for more information.
103 |
104 | We also use `django-stubs `_
105 | to type ``django`` internals.
106 | This package is optional and can be removed,
107 | if you don't want to type your ``django`` for some reason.
108 |
--------------------------------------------------------------------------------
/docs/pages/template/troubleshooting.rst:
--------------------------------------------------------------------------------
1 | Troubleshooting
2 | ===============
3 |
4 | This section is about some of the problems you may encounter and
5 | how to solve these problems.
6 |
7 |
8 | Docker
9 | ------
10 |
11 | Pillow
12 | ~~~~~~
13 |
14 | If you want to install ``Pillow`` that you should
15 | add this to dockerfile and rebuild image:
16 |
17 | - ``RUN apk add jpeg-dev zlib-dev``
18 | - ``LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "poetry install ..."``
19 |
20 | See ``_
21 |
22 | Root owns build artifacts
23 | ~~~~~~~~~~~~~~~~~~~~~~~~~
24 |
25 | This happens on some systems.
26 | It happens because build happens in ``docker`` as the ``root`` user.
27 | The fix is to pass current ``UID`` to ``docker``.
28 | See ``_.
29 |
30 | MacOS performance
31 | ~~~~~~~~~~~~~~~~~
32 |
33 | If you use the MacOS you
34 | know that you have problems with disk performance.
35 | Starting and restarting an application is slower than with Linux
36 | (it's very noticeable for project with large codebase).
37 | For particular solve this problem add ``:delegated`` to each
38 | your volumes in ``docker-compose.yml`` file.
39 |
40 | .. code:: yaml
41 |
42 | volumes:
43 | - pgdata:/var/lib/postgresql/data:delegated
44 |
45 | For more information, you can look at the
46 | `docker documents `_
47 | and a good `article `_.
48 |
--------------------------------------------------------------------------------
/docs/pages/template/upgrading-template.rst:
--------------------------------------------------------------------------------
1 | Upgrading template
2 | ==================
3 |
4 | Upgrading your project to be up-to-date with this template is a primary goal.
5 | This is achieved by manually applying ``diff`` to your existing code.
6 |
7 | ``diff`` can be viewed from the project's ``README.md``.
8 | See `an example `_.
9 |
10 | When the upgrade is applied just change the commit hash in your template
11 | to the most recent one.
12 |
13 |
14 | Versions
15 | --------
16 |
17 | Sometimes, when we break something heavily, we create a version.
18 | That's is required for our users, so they can use old releases to create
19 | projects as they used to be a long time ago.
20 |
21 | However, we do not officially support older versions.
22 | And we do not recommend to use them.
23 |
24 | A full list of versions can be `found here `_.
25 |
26 |
27 | Migration guides
28 | ----------------
29 |
30 | Each time we create a new version, we also provide a migration guide.
31 | What is a migration guide?
32 | It is something you have to do to your project
33 | other than just copy-pasting diffs from new versions.
34 |
35 | Goodbye, pipenv!
36 | ~~~~~~~~~~~~~~~~
37 |
38 | This version requires a manual migration step.
39 |
40 | 1. You need to install ``poetry``
41 | 2. You need to create a new ``pyproject.toml`` file with ``poetry init``
42 | 3. You need to adjust name, version, description, and authors meta fields
43 | 4. You need to copy-paste dependencies from ``Pipfile`` to ``pyproject.toml``
44 | 5. You need to set correct version for each dependency in the list,
45 | use ``"^x.y"`` `notation `_
46 | 6. You need to adjust ``[build-system]`` tag and ``POETRY_VERSION`` variable
47 | to fit your ``poetry`` version
48 | 7. Create ``poetry.lock`` file with ``poetry lock``
49 |
50 | It should be fine! You may, however, experience some bugs related to different
51 | dependency version resolution mechanisms. But, ``poetry`` does it better.
52 |
--------------------------------------------------------------------------------
/locale/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/locale/.gitkeep
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 |
7 | def main() -> None:
8 | """
9 | Main function.
10 |
11 | It does several things:
12 | 1. Sets default settings module, if it is not set
13 | 2. Warns if Django is not installed
14 | 3. Executes any given command
15 | """
16 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
17 |
18 | try:
19 | from django.core import management # noqa: WPS433
20 | except ImportError:
21 | raise ImportError(
22 | "Couldn't import Django. Are you sure it's installed and " +
23 | 'available on your PYTHONPATH environment variable? Did you ' +
24 | 'forget to activate a virtual environment?',
25 | )
26 |
27 | management.execute_from_command_line(sys.argv)
28 |
29 |
30 | if __name__ == '__main__':
31 | main()
32 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "testing_homework"
3 | description = "Testing Homework for https://education.borshev.com/python-testing"
4 | version = "1.0.0"
5 | readme = "README.md"
6 | authors = ["Nikita Sobolev "]
7 | classifiers = [
8 | "Private :: Do not Upload",
9 | ]
10 |
11 |
12 | [tool.poetry.dependencies]
13 | python = "3.11.5"
14 |
15 | django = { version = "^4.2", extras = ["argon2"] }
16 | django-split-settings = "^1.2"
17 | django-axes = "^6.1"
18 | django-csp = "^3.7"
19 | django-health-check = "^3.16"
20 | django-http-referrer-policy = "^1.1"
21 | django-permissions-policy = "^4.17"
22 | django-stubs-ext = "^4.2"
23 | django-ratelimit = "3.0.1" # API change in `^4.x`
24 |
25 | psycopg2-binary = "^2.9"
26 | gunicorn = "^21.2"
27 | python-decouple = "^3.8"
28 | structlog = "^23.1"
29 | requests = "^2.28"
30 | attrs = "^23.1"
31 | pydantic = "^2.3"
32 | punq = "^0.6"
33 |
34 | [tool.poetry.group.dev.dependencies]
35 | django-debug-toolbar = "^4.2"
36 | django-querycount = "^0.8"
37 | django-migration-linter = "^5.0"
38 | django-extra-checks = "^0.13"
39 | nplusone = "^1.0"
40 |
41 | wemake-python-styleguide = "^0.18"
42 | flake8-pytest-style = "^1.7"
43 | flake8-logging-format = "^0.9"
44 | nitpick = "^0.34"
45 | doc8 = "^1.0"
46 |
47 | pytest = "^7.4"
48 | pytest-django = "^4.5"
49 | pytest-cov = "^4.0"
50 | django-coverage-plugin = "^3.1"
51 | covdefaults = "^2.3"
52 | pytest-randomly = "^3.15"
53 | pytest-timeout = "^2.1"
54 | hypothesis = "^6.84"
55 | django-test-migrations = "^1.3"
56 |
57 | django-stubs = { version = "^4.2", extras = ["compatible-mypy"] }
58 | types-requests = "^2.31"
59 |
60 | djlint = "^1.32"
61 | yamllint = "^1.32"
62 | safety = "^2.3"
63 | dotenv-linter = "^0.4"
64 | polint = "^0.4"
65 | dennis = "^1.1"
66 | dump-env = "^1.3"
67 | ipython = "^8.15"
68 | import-linter = "^1.11"
69 |
70 | [tool.poetry.group.docs]
71 | optional = true
72 |
73 | [tool.poetry.group.docs.dependencies]
74 | sphinx = "^7.2"
75 | sphinx-autodoc-typehints = "^1.21"
76 | tomli = "^2.0"
77 |
78 |
79 | [build-system]
80 | requires = ["poetry-core>=1.6"]
81 | build-backend = "poetry.core.masonry.api"
82 |
83 |
84 | [tool.djlint]
85 | ignore = "H006,H030,H031,T002"
86 | include = "H017,H035"
87 | indent = 2
88 | blank_line_after_tag = "load,extends"
89 | profile = "django"
90 | max_line_length = 80
91 | format_attribute_template_tags = true
92 |
93 |
94 | [tool.nitpick]
95 | style = "https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/0.18.0/styles/nitpick-style-wemake.toml"
96 |
--------------------------------------------------------------------------------
/scripts/create_dev_database.sql:
--------------------------------------------------------------------------------
1 | /*
2 | This file is used to bootstrap development database locally.
3 |
4 | Note: ONLY development database;
5 | */
6 |
7 | CREATE USER testing_homework SUPERUSER;
8 | CREATE DATABASE testing_homework OWNER testing_homework ENCODING 'utf-8';
9 |
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/__init__.py
--------------------------------------------------------------------------------
/server/apps/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/admin.py:
--------------------------------------------------------------------------------
1 | from typing import final
2 |
3 | from django.contrib import admin
4 |
5 | from server.apps.identity.models import User
6 | from server.common.django.admin import TimeReadOnlyMixin
7 |
8 |
9 | @final
10 | @admin.register(User)
11 | class UserAdmin(TimeReadOnlyMixin, admin.ModelAdmin[User]):
12 | """This class represents `User` in admin panel."""
13 |
14 | list_display = tuple(['id'] + User.REQUIRED_FIELDS)
15 | search_fields = (
16 | 'email',
17 | 'first_name',
18 | 'last_name',
19 | )
20 |
--------------------------------------------------------------------------------
/server/apps/identity/container.py:
--------------------------------------------------------------------------------
1 | import punq
2 | from django.conf import settings
3 |
4 | from server.common.django.types import Settings
5 |
6 | container = punq.Container()
7 |
8 | # Custom dependencies go here:
9 | # TODO: add custom deps
10 |
11 | # Django stuff:
12 | container.register(Settings, instance=settings)
13 |
--------------------------------------------------------------------------------
/server/apps/identity/infrastructure/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/infrastructure/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/infrastructure/django/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/infrastructure/django/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/infrastructure/django/decorators.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, TypeVar
2 |
3 | from django.conf import settings
4 | from django.contrib.auth.decorators import user_passes_test
5 |
6 | _CallableT = TypeVar('_CallableT', bound=Callable[..., Any])
7 |
8 |
9 | def redirect_logged_in_users(
10 | *,
11 | redirect_field_name: str = '',
12 | ) -> Callable[[_CallableT], _CallableT]:
13 | """Decorator for views that checks that the user is NOT logged in."""
14 | return user_passes_test(
15 | lambda user: not user.is_authenticated,
16 | login_url=settings.LOGIN_REDIRECT_URL,
17 | redirect_field_name=redirect_field_name,
18 | )
19 |
--------------------------------------------------------------------------------
/server/apps/identity/infrastructure/django/forms.py:
--------------------------------------------------------------------------------
1 | from typing import final
2 |
3 | from django import forms
4 | from django.contrib.auth.forms import (
5 | AuthenticationForm as BaseAuthenticationForm,
6 | )
7 | from django.contrib.auth.forms import UserCreationForm
8 |
9 | from server.apps.identity.models import User
10 | from server.common.django.forms import DateWidget
11 |
12 |
13 | @final
14 | class RegistrationForm(UserCreationForm[User]):
15 | """Create user with all the contact details."""
16 |
17 | class Meta(object):
18 | model = User
19 | fields = [User.USERNAME_FIELD] + User.REQUIRED_FIELDS
20 | widgets = {
21 | User.USERNAME_FIELD: forms.EmailInput(),
22 | 'date_of_birth': DateWidget(),
23 | }
24 |
25 |
26 | @final
27 | class AuthenticationForm(BaseAuthenticationForm):
28 | """Redefined default email widget."""
29 |
30 | username = forms.EmailField()
31 |
32 |
33 | @final
34 | class UserUpdateForm(forms.ModelForm[User]):
35 | """
36 | Update user with all the required details.
37 |
38 | Except passwords. Why?
39 | 1. Passwords are hard to update
40 | 2. You have to input the current password to set a new one
41 | 3. We would need to notify user by email about the password change
42 | 4. All sessions are killed, we will need to restore at least one manually
43 | 5. We already have a change-password mechanics
44 |
45 | You also cannot change 'email': it is our main identifier.
46 | """
47 |
48 | class Meta(object):
49 | model = User
50 | fields = User.REQUIRED_FIELDS
51 | widgets = {
52 | 'date_of_birth': DateWidget(),
53 | }
54 |
--------------------------------------------------------------------------------
/server/apps/identity/infrastructure/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/infrastructure/services/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/infrastructure/services/placeholder.py:
--------------------------------------------------------------------------------
1 | from typing import final
2 |
3 | import requests
4 |
5 | from server.apps.identity.models import User
6 | from server.common import pydantic_model
7 | from server.common.services import http
8 |
9 |
10 | @final
11 | class UserResponse(pydantic_model.BaseModel):
12 | """Schema for API response with :term:`lead_id`."""
13 |
14 | id: int
15 |
16 |
17 | # TODO: use redis-based caching
18 | @final
19 | class LeadCreate(http.BaseFetcher):
20 | """Service around creating new users and fetching their :term:`lead_id`."""
21 |
22 | _url_path = '/users'
23 |
24 | def __call__(
25 | self,
26 | *,
27 | user: User,
28 | ) -> UserResponse:
29 | """Create remote user and return assigned ids."""
30 | response = requests.post(
31 | self.url_path(),
32 | json=_serialize_user(user),
33 | timeout=self._api_timeout,
34 | )
35 | response.raise_for_status()
36 | return UserResponse.model_validate_json(response.text)
37 |
38 |
39 | @final
40 | class LeadUpdate(http.BaseFetcher):
41 | """Service around editing users."""
42 |
43 | _url_path = '/users/{0}'
44 |
45 | def __call__(
46 | self,
47 | *,
48 | user: User,
49 | ) -> None:
50 | """Update remote user."""
51 | response = requests.patch(
52 | self.url_path().format(user.lead_id),
53 | json=_serialize_user(user),
54 | timeout=self._api_timeout,
55 | )
56 | response.raise_for_status()
57 |
58 |
59 | def _serialize_user(user: User) -> dict[str, str]:
60 | if user.date_of_birth is not None:
61 | date_of_birth = user.date_of_birth.strftime('%d.%m.%Y')
62 | else:
63 | date_of_birth = ''
64 |
65 | return {
66 | 'name': user.first_name,
67 | 'last_name': user.last_name,
68 | 'birthday': date_of_birth,
69 | 'city_of_birth': user.address,
70 | 'position': user.job_title,
71 | 'email': user.email,
72 | 'phone': user.phone,
73 | }
74 |
--------------------------------------------------------------------------------
/server/apps/identity/logic/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/logic/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/logic/usecases/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/logic/usecases/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/logic/usecases/user_create_new.py:
--------------------------------------------------------------------------------
1 | from typing import final
2 |
3 | import attr
4 |
5 | from server.apps.identity.infrastructure.services import placeholder
6 | from server.apps.identity.models import User
7 | from server.common.django.types import Settings
8 |
9 |
10 | @final
11 | @attr.dataclass(slots=True, frozen=True)
12 | class UserCreateNew(object):
13 | """
14 | Create new user in :term:`Placeholder API`.
15 |
16 | Get their :term:`lead_id` back and save it locally.
17 |
18 | .. warning:
19 | This use-case does not handle transactions!
20 |
21 | """
22 |
23 | _settings: Settings
24 |
25 | def __call__(self, user: User) -> None:
26 | """
27 | Execute the usecase.
28 |
29 | Ideally this docstring must contain a link to the user-story, like:
30 | https://sobolevn.me/2019/02/engineering-guide-to-user-stories
31 | """
32 | new_ids = self._create_lead(user)
33 | return self._update_user_ids(user, new_ids)
34 |
35 | def _create_lead(self, user: User) -> placeholder.UserResponse:
36 | return placeholder.LeadCreate(
37 | api_url=self._settings.PLACEHOLDER_API_URL,
38 | api_timeout=self._settings.PLACEHOLDER_API_TIMEOUT,
39 | )(user=user)
40 |
41 | def _update_user_ids(
42 | self,
43 | user: User,
44 | new_ids: placeholder.UserResponse,
45 | ) -> None:
46 | # This can be moved to some other place once this becomes too complex:
47 | user.lead_id = new_ids.id
48 | user.save(update_fields=['lead_id'])
49 |
--------------------------------------------------------------------------------
/server/apps/identity/logic/usecases/user_update.py:
--------------------------------------------------------------------------------
1 | from typing import final
2 |
3 | import attr
4 |
5 | from server.apps.identity.infrastructure.services import placeholder
6 | from server.apps.identity.models import User
7 | from server.common.django.types import Settings
8 |
9 |
10 | @final
11 | @attr.dataclass(slots=True, frozen=True)
12 | class UserUpdate(object):
13 | """
14 | Update existing user in :term:`Placeholder API`.
15 |
16 | Get their :term:`lead_id` back and save it locally.
17 |
18 | .. warning:
19 | This use-case does not handle transactions!
20 |
21 | """
22 |
23 | _settings: Settings
24 |
25 | def __call__(self, user: User) -> None:
26 | """Update existing user in the remote api."""
27 | return self._update_lead(user)
28 |
29 | def _update_lead(self, user: User) -> None:
30 | return placeholder.LeadUpdate(
31 | api_url=self._settings.PLACEHOLDER_API_URL,
32 | api_timeout=self._settings.PLACEHOLDER_API_TIMEOUT,
33 | )(user=user)
34 |
--------------------------------------------------------------------------------
/server/apps/identity/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2022-12-02 09:38
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | """Initial migration for our user model."""
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ('auth', '0012_alter_user_first_name_max_length'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='User',
18 | fields=[
19 | (
20 | 'id',
21 | models.AutoField(
22 | auto_created=True,
23 | primary_key=True,
24 | serialize=False,
25 | verbose_name='ID',
26 | ),
27 | ),
28 | (
29 | 'password',
30 | models.CharField(max_length=128, verbose_name='password'),
31 | ),
32 | (
33 | 'last_login',
34 | models.DateTimeField(
35 | blank=True,
36 | null=True,
37 | verbose_name='last login',
38 | ),
39 | ),
40 | (
41 | 'is_superuser',
42 | models.BooleanField(
43 | default=False,
44 | help_text='Designates that this user has all permissions without explicitly assigning them.', # noqa: E501
45 | verbose_name='superuser status',
46 | ),
47 | ),
48 | ('created_at', models.DateTimeField(auto_now_add=True)),
49 | ('updated_at', models.DateTimeField(auto_now=True)),
50 | ('email', models.EmailField(max_length=254, unique=True)),
51 | ('first_name', models.CharField(max_length=254)),
52 | ('last_name', models.CharField(max_length=254)),
53 | ('date_of_birth', models.DateField(blank=True, null=True)),
54 | ('address', models.CharField(max_length=254)),
55 | ('job_title', models.CharField(max_length=254)),
56 | ('phone', models.CharField(max_length=254)),
57 | ('lead_id', models.IntegerField(blank=True, null=True)),
58 | ('is_staff', models.BooleanField(default=False)),
59 | ('is_active', models.BooleanField(default=True)),
60 | (
61 | 'groups',
62 | models.ManyToManyField(
63 | blank=True,
64 | help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', # noqa: E501
65 | related_name='user_set',
66 | related_query_name='user',
67 | to='auth.Group',
68 | verbose_name='groups',
69 | ),
70 | ),
71 | (
72 | 'user_permissions',
73 | models.ManyToManyField(
74 | blank=True,
75 | help_text='Specific permissions for this user.',
76 | related_name='user_set',
77 | related_query_name='user',
78 | to='auth.Permission',
79 | verbose_name='user permissions',
80 | ),
81 | ),
82 | ],
83 | options={
84 | 'abstract': False,
85 | },
86 | ),
87 | ]
88 |
--------------------------------------------------------------------------------
/server/apps/identity/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tough-dev-school/python-testing-homework/352aa9b27d17d25f9aa6d67ddb4bec03503ae326/server/apps/identity/migrations/__init__.py
--------------------------------------------------------------------------------
/server/apps/identity/models.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Any, Final, final
2 |
3 | from django.contrib.auth.models import (
4 | AbstractBaseUser,
5 | BaseUserManager,
6 | PermissionsMixin,
7 | )
8 | from django.db import models
9 |
10 | from server.common.django.models import TimedMixin
11 |
12 | # For now we use a single length for all items, later it can be changed.
13 | _NAME_LENGTH: Final = 254
14 |
15 |
16 | @final
17 | class _UserManager(BaseUserManager['User']):
18 | def create_user(
19 | self,
20 | email: str,
21 | password: str,
22 | **extra_fields: Any,
23 | ) -> 'User':
24 | """Create user: regular registration process."""
25 | if not email:
26 | # We double check it here,
27 | # but validation should make this unreachable.
28 | raise ValueError('Users must have an email address')
29 |
30 | user = User(email=self.normalize_email(email), **extra_fields)
31 | user.set_password(password)
32 | user.save(using=self._db)
33 | return user
34 |
35 | def create_superuser(
36 | self,
37 | email: str,
38 | password: str,
39 | **extra_fields: Any,
40 | ) -> 'User':
41 | """Create super user."""
42 | user = self.create_user(email, password, **extra_fields)
43 | user.is_superuser = True
44 | user.is_staff = True
45 | # Technically this is not transaction safe, but who cares.
46 | # It is only used in CLI / tests:
47 | user.save(using=self._db, update_fields=['is_superuser', 'is_staff'])
48 | return user
49 |
50 |
51 | @final
52 | class User(AbstractBaseUser, PermissionsMixin, TimedMixin):
53 | """Implementation of :term:`user` in the app."""
54 |
55 | # Identity:
56 | email = models.EmailField(unique=True)
57 |
58 | # Details:
59 | first_name = models.CharField(max_length=_NAME_LENGTH)
60 | last_name = models.CharField(max_length=_NAME_LENGTH)
61 | date_of_birth = models.DateField(null=True, blank=True)
62 | address = models.CharField(max_length=_NAME_LENGTH)
63 | job_title = models.CharField(max_length=_NAME_LENGTH)
64 |
65 | # Contacts:
66 | # NOTE: we don't really care about phone correctness.
67 | phone = models.CharField(max_length=_NAME_LENGTH)
68 |
69 | # Integration with Placeholder API:
70 | lead_id = models.IntegerField(null=True, blank=True)
71 |
72 | # Security:
73 | is_staff = models.BooleanField(default=False)
74 | is_active = models.BooleanField(default=True)
75 |
76 | # Mechanics:
77 | objects = _UserManager() # noqa: WPS110
78 |
79 | USERNAME_FIELD = 'email' # noqa: WPS115
80 | REQUIRED_FIELDS = [ # noqa: WPS115
81 | 'first_name',
82 | 'last_name',
83 | 'date_of_birth',
84 | 'address',
85 | 'job_title',
86 | 'phone',
87 | ]
88 |
89 | if TYPE_CHECKING: # noqa: WPS604
90 | # Raw password that is stored in the instance before it is saved,
91 | # it is actually `str | None` in runtime, but `str` in most tests.
92 | _password: str
93 |
--------------------------------------------------------------------------------
/server/apps/identity/templates/identity/includes/user_model_form.html:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/server/apps/identity/templates/identity/pages/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'common/_base.html' %}
2 |
3 | {% load static %}
4 |
5 | {% block title %}
6 | Регистрация
7 | {% endblock title %}
8 | {% block content %}
9 |
10 |