├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .python-version ├── Dockerfile ├── Dockerfile-webpack ├── LICENSE.txt ├── README.md ├── apps ├── __init__.py ├── misc │ ├── __init__.py │ ├── choices.py │ ├── context_processors.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── celery_autoreload.py │ │ │ └── test_celery.py │ ├── models.py │ └── tasks.py └── users │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── managers.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ └── urls │ ├── __init__.py │ └── auth.py ├── assets ├── .eslintrc.json ├── .stylelintrc.json ├── img │ └── favicon.png ├── index.js ├── js │ └── main.js ├── package-lock.json ├── package.json ├── scss │ ├── _variables.scss │ ├── libs │ │ └── reset.scss │ └── main.scss └── webpack.config.js ├── conf ├── __init__.py ├── admin.py ├── apps.py ├── celery.py ├── settings.py ├── urls.py └── wsgi.py ├── docker-compose.yml ├── logs └── .gitignore ├── manage.py ├── poetry.lock ├── pyproject.toml ├── scripts.py ├── setup.cfg ├── templates ├── 403.html ├── 404.html ├── 500.html ├── base.html ├── error_base.html └── registration │ ├── activate.html │ ├── activation_complete.html │ ├── activation_email.html │ ├── activation_email.txt │ ├── activation_email_subject.txt │ ├── base.html │ ├── logged_out.html │ ├── login.html │ ├── logout.html │ ├── password_change_done.html │ ├── password_change_form.html │ ├── password_reset_complete.html │ ├── password_reset_confirm.html │ ├── password_reset_done.html │ ├── password_reset_email.html │ ├── password_reset_form.html │ ├── registration_form.html │ ├── resend_activation_complete.html │ └── resend_activation_form.html └── tests ├── __init__.py ├── misc ├── __init__.py └── test_choices.py └── test_int.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.* 3 | .git 4 | .gitignore 5 | .dockerignore 6 | .editorconfig 7 | .gitignore 8 | .vscode 9 | Dockerfile* 10 | docker-compose* 11 | node_modules 12 | assets/node_modules 13 | logs 14 | media 15 | static 16 | README.md 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.py] 12 | indent_size = 4 13 | combine_as_imports = true 14 | max_line_length = 119 15 | multi_line_output = 4 16 | quote_type = double 17 | indent_style = space 18 | 19 | [*.js] 20 | indent_size = 2 21 | quote_type = double 22 | 23 | [*.{sass,scss,less}] 24 | indent_size = 2 25 | 26 | [*.yml] 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_size = 2 31 | 32 | [*.json] 33 | indent_size = 2 34 | 35 | [*.{diff,md}] 36 | trim_trailing_whitespace = false 37 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Basic Config 2 | ENV=dev 3 | DEBUG=on 4 | 5 | # Time & Language 6 | LANGUAGE_CODE=en-us 7 | TIMEZONE=UTC 8 | USE_I18N=on 9 | USE_L10N=on 10 | 11 | # Emails 12 | DEFAULT_FROM_EMAIL=no-reply@example.com 13 | EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend 14 | 15 | # Security and Users 16 | SECRET_KEY=123 17 | ALLOWED_HOSTS="*" 18 | LOGIN_URL=/login/ 19 | LOGIN_REDIRECT_URL=/ 20 | 21 | # Databases 22 | DATABASE_URL=postgres://localhost:5432/django_db 23 | 24 | # Static & Media Files 25 | STATIC_URL=/static/ 26 | MEDIA_URL=/media/ 27 | USE_S3_STATIC_STORAGE=off 28 | AWS_ACCESS_KEY_ID= 29 | AWS_SECRET_ACCESS_KEY= 30 | AWS_STORAGE_BUCKET_NAME= 31 | AWS_S3_ENDPOINT_URL= 32 | 33 | # Celery 34 | CELERY_BROKER_URL=redis://cache 35 | CELERY_TASK_ALWAYS_EAGER=off 36 | 37 | # Sentry 38 | USE_SENTRY=off 39 | SENTRY_DSN= 40 | 41 | # Other apps 42 | USE_DEBUG_TOOLBAR=on 43 | USE_DJANGO_EXTENSIONS=on 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python bytecode 2 | __pycache__ 3 | *.egg-info 4 | 5 | # Django dynamic directories 6 | logs 7 | media 8 | 9 | # Database files 10 | *.sqlite3 11 | *.rdb 12 | 13 | # Environment variables 14 | .env 15 | .env.docker 16 | 17 | # Static files 18 | assets/node_modules 19 | assets/bundles/webpack-bundle.dev.json 20 | assets/bundles/dev 21 | 22 | # IDE config 23 | .vscode 24 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8.1 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Pull base image 2 | FROM python:3.8.1-slim 3 | 4 | # Install system dependencies 5 | RUN apt-get update 6 | RUN apt-get install git -y 7 | 8 | # Set environment varibles 9 | ENV DJANGO_ENV=dev 10 | ENV PYTHONDONTWRITEBYTECODE=1 11 | ENV PYTHONUNBUFFERED=1 12 | ENV POETRY_VERSION=1.0.0 13 | 14 | # Install Poetry 15 | RUN pip install "poetry==$POETRY_VERSION" 16 | 17 | # Create dynamic directories 18 | RUN mkdir /code /logs /uploads /code/apps /code/conf 19 | 20 | # Set work directory 21 | WORKDIR /code 22 | 23 | # Install project dependencies 24 | COPY poetry.lock pyproject.toml ./ 25 | 26 | RUN poetry config virtualenvs.create false \ 27 | && poetry install $(test "$DJANGO_ENV" == prod && echo "--no-dev") --no-interaction --no-ansi --no-root 28 | -------------------------------------------------------------------------------- /Dockerfile-webpack: -------------------------------------------------------------------------------- 1 | # Pull base image 2 | FROM node:latest 3 | 4 | # Set work directory 5 | WORKDIR /code 6 | 7 | # Copy webpack config 8 | COPY assets/webpack.config.js ./ 9 | 10 | # Install node modules 11 | COPY assets/package*.json ./ 12 | RUN npm install 13 | RUN npm rebuild node-sass # Fix issue: https://github.com/sass/node-sass/issues/2536 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Francisco Ceruti 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Starter Project 2 | 3 | An opinionated boilerplate for your Django projects. 4 | 5 | ## 🎖 Tool Choices 6 | 7 | - **Env Variables Manager**: [Django Environ](https://github.com/joke2k/django-environ) 8 | - **Package manager:** [Poetry](https://python-poetry.org/) 9 | - **Database:** [PostgreSQL](https://www.postgresql.org/) 10 | - **Task Queue:** [Celery](http://www.celeryproject.org/) with [Redis](https://redis.io/) 11 | - **Testing:** [PyTest](https://docs.pytest.org/en/latest/) 12 | - **Logging:** [Sentry](https://sentry.io) 13 | - **Python Code Formatter**: [Black](https://github.com/psf/black) 14 | - **Static File Storage:** [AWS S3](https://aws.amazon.com/s3/) or [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/) 15 | - **Dev Orchestration:** [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/) 16 | - **Static File Compiler:** [Webpack](https://webpack.js.org/) 17 | - **JS lint:** [ESLint](https://eslint.org/) 18 | - **Style lint:** [Stylelint](https://github.com/stylelint/stylelint) 19 | 20 | ## 🤓 Getting started 21 | 22 | This is a guide on how to use this repo and save hours of boring configuration. 23 | 24 | ### 1) Add the boilerplate 25 | 26 | Instead of running `django-admin startproject` to start your new project, clone this repo in a directory of your choosing 27 | 28 | git clone git@github.com:fceruti/django-starter-project.git 29 | 30 | At this point you may either 31 | 32 | 1. Start a clean git repo by removing the `.git` directory and then running `git init`. 33 | 2. Receive patches and updates of this repo by modifying `.git/config` and switch `[remote "origin"]` for `[remote "upstream"]`, and then add your own `origin` by running `git remote add origin `. 34 | 35 | ### 2) Set environment variables 36 | 37 | Following the [12 Factor App Guide](https://www.12factor.net/), we will configure our app by setting the configuration variables in the environment. To do that, just create a file named `.env` in the root of the project and [django-environ](https://github.com/joke2k/django-environ) will pick it up and populate our settings. 38 | 39 | You may use the example file as a starting point: 40 | 41 | cp .env.example .env 42 | 43 | ### 3) Create and migrate database 44 | 45 | I'll call this database `django_db` for the purposes of this guide, but you can call it whatever you want. Just watch out for careless copy-pasting. 46 | 47 | Now run 48 | 49 | poetry run migrate 50 | 51 | ### 4) Run the project 52 | 53 | You have two choices, either you turn every service on your own or you use docker-compose 54 | 55 | #### A) Use Docker Compose 56 | 57 | We need to override `DATABASE_URL` environment variable inside of the Docker containers to connect directly to your host machine. Create a file called `.env.docker` with the following content: 58 | 59 | ``` 60 | DATABASE_URL=postgres://@host.docker.internal:5432/django_db 61 | ``` 62 | 63 | - **user** is the user on your host machine that has access to postgres in this case. 64 | 65 | Now we are ready to start the project. 66 | 67 | docker-compose up 68 | 69 | 70 | Visit [http://127.0.0.1:8000](http://127.0.0.1:8000) and you'll see your site up and running 🧘‍♀️ 71 | 72 | #### B) Run by yourself 73 | 74 | First of all, install [pyenv](https://github.com/pyenv/pyenv) so you can use the specified python version in `.python-version` file. Then, install [poetry](https://python-poetry.org/docs/), which is a package manager replacement for pip. Now install all the project's dependencies with 75 | 76 | poetry install 77 | 78 | Make sure you have `redis-server` running and finally on 3 separate consoles run: 79 | 80 | **server** 81 | 82 | poetry run worker 83 | 84 | **worker** 85 | 86 | poetry run server 87 | 88 | 89 | **webpack** 90 | 91 | cd assets 92 | npm install 93 | npm run dev 94 | 95 | Visit [http://127.0.0.1:8000](http://127.0.0.1:8000) and you'll see your site up and running 🧘‍♀️ 96 | 97 | ## 🧞‍♂️ Shortcuts 98 | 99 | ### Docker commands 100 | 101 | Here are a few commands that may come in handy 102 | 103 | | Command | Description | 104 | | --------------------------------------------------------------------- | ------------------------------------------------------------- | 105 | | `docker ps` | List all containers (-a to include stopped) | 106 | | `docker system df -v` | Display a list of all images, containers and volumes | 107 | | `docker logs --follow ` | Display the logs of a container | 108 | | `docker exec -it /bin/bash` | Attach into a running container | 109 | | `docker run --rm /bin/bash` | Run a docker container based on an image and get a prompt | 110 | | `docker-compose run --rm web /bin/bash` | Same as before but for services defined in docker-compose.yml | 111 | | `docker-compose run --rm web /bin/bash -c 'python manage.py migrate'` | Run a management command | 112 | 113 | ### Poetry commands 114 | 115 | These shortcuts are at your disposal: 116 | 117 | | Command | Shortcut for | 118 | | --------------------------- | ----------------------------------------------- | 119 | | `poetry run tests` | `poetry run pytest tests/` | 120 | | `poetry run server` | `poetry run python manage.py runserver_plus` | 121 | | `poetry run worker` | `poetry run python manage.py celery_autoreload` | 122 | | `poetry run shell` | `poetry run python manage.py shell_plus` | 123 | | `poetry run makemigrations` | `poetry run python manage.py makemigrations` | 124 | | `poetry run migrate` | `poetry run python manage.py migrate` | 125 | 126 | To compile your static files, you need to have npm installed and all the local dependencies (run `npm install`). Then can execute the following commands 127 | | Command | Shortcut for | 128 | | -------------------- | --------------------------------------------- | 129 | | `npm run dev` | `webpack --mode development --env dev--watch` | 130 | | `npm run build_stg` | `webpack --mode production --env stg` | 131 | | `npm run build_prod` | `webpack --mode production --env prod` | 132 | | `npm run lint:js` | `eslint js/** --fix` | 133 | | `npm run lint:csss` | `stylelint scss/*.scss --syntax scss` | 134 | 135 | ## 🎛 Environment variables 136 | 137 | These environment variables can be provided to configure your project. 138 | 139 | ### Django 140 | 141 | | Name | Values | Default | Description | 142 | | ------------------- | --------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | 143 | | ENV | dev, test, qa, prod | prod | Indicates in which environment the project is running on | 144 | | DEBUG | on, off | off | Run server in debug mode | 145 | | LANGUAGE_CODE | Language Identifier (RFC 3066) | en-US | [List of language codes](http://www.i18nguy.com/unicode/language-identifiers.html) | 146 | | TIME_ZONE | Record of IANA time zone database | America/Santiago | [List of timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | 147 | | USE_I18N | on, off | on | Enable translation system | 148 | | USE_L10N | on, off | on | Enable localized formatting | 149 | | USE_TZ | on, off | on | Enable timezone aware dates in | 150 | | DEFAULT_FROM_EMAIL | E-mail address | -- | Email address from which transactional emails will be sent from | 151 | | EMAIL_BACKEND | Path to backend | django.core.mail.backends.smtp.EmailBackend | The backend to use for sending emails. [List of backends](https://docs.djangoproject.com/en/2.2/topics/email/#email-backends) | 152 | | SECRET_KEY | Random string | -- | Used to provide cryptographic signing | 153 | | ALLOWED_HOSTS | List of domains | -- | Represents the host/domain names that this site can serve. | 154 | | DJANGO_DATABASE_URL | Database url | -- | Describes the database connection with [a url structure](https://github.com/joke2k/django-environ). | 155 | | LOGIN_URL | Url | /login/ | Url to redirect users when login is needed | 156 | | LOGIN_REDIRECT_URL | Url | / | Url to redirect users after login in | 157 | | STATIC_URL | Url | /static/ | Url from which static files are served | 158 | | MEDIA_URL | Url | /media/ | Url from which media files are served | 159 | | STATIC_ROOT | Directory path | \${project_root}/static | Directory where all static files will be collected to | 160 | | MEDIA_ROOT | Directory path | \${project_root}/media | Directory where all uploaded files will be stored | 161 | | LOGS_ROOT | Directory path | \${project_root}/logs | Directory where all logs will be stored | 162 | 163 | ### Celery 164 | 165 | | Name | Values | Default | Description | 166 | | ------------------------ | ------------ | ------- | ----------------------------------------------------------------------------------------------------------- | 167 | | CELERY_BROKER_URL | Database url | -- | A common value for development is to use redis://cache, but it's recommended for production to use RabbitMQ | 168 | | CELERY_TASK_ALWAYS_EAGER | on, off | off | If this is True, all tasks will be executed locally by blocking until the task returns. | 169 | 170 | ### Django Storages 171 | 172 | [Documentation](https://django-storages.readthedocs.io/en/latest/) 173 | 174 | | Name | Values | Default | Description | 175 | | ----------------------- | ------- | ------- | ---------------------------- | 176 | | USE_S3_STATIC_STORAGE | on, off | off | Whether or not to use S3 | 177 | | AWS_ACCESS_KEY_ID | str | -- | AWS Access key id | 178 | | AWS_SECRET_ACCESS_KEY | str | -- | AWS Secret access key | 179 | | AWS_STORAGE_BUCKET_NAME | str | -- | Name of S3 bucket to be used | 180 | 181 | #### Django Debug Toolbar 182 | 183 | | Name | Values | Default | Description | 184 | | ----------------- | ------- | ------- | ---------------------------- | 185 | | USE_DEBUG_TOOLBAR | on, off | off | Enables django debug toolbar | 186 | 187 | #### Logging & Sentry 188 | 189 | | Name | Values | Default | Description | 190 | | ---------- | ------- | ------- | ------------------------------------------------- | 191 | | LOGS_ROOT | path | -- | Path to the directory where logs are to be stored | 192 | | USE_SENTRY | on, off | off | Enables sentry | 193 | | SENTRY_DSN | string | -- | Private URL-like configuration | 194 | 195 | ## ♻️ VSCode settings 196 | 197 | Nowadays, my go-to editor is VSCode, so here's a template for `.vscode/settings.json`: 198 | 199 | { 200 | // Editor 201 | "editor.formatOnSave": true, 202 | 203 | "files.exclude": { 204 | "**/__pycache__": true, 205 | "**/.pytest_cache": true, 206 | "**/*.egg-info": true 207 | }, 208 | 209 | // Search 210 | "search.exclude": { 211 | "**/.git": true, 212 | "**/.vscode": true, 213 | "**/node_modules": true, 214 | "**/static": true, 215 | "**/media": true, 216 | "**/logs": true, 217 | "**/tmp": true, 218 | "**/locale": true 219 | }, 220 | "search.showLineNumbers": true, 221 | 222 | // Python 223 | "python.venvPath": "", 224 | "python.envFile": "${workspaceFolder}/.env", 225 | "python.jediEnabled": false, 226 | 227 | // Linting 228 | "python.linting.enabled": true, 229 | "python.linting.pylintEnabled": true, 230 | "python.linting.flake8Enabled": true, 231 | "python.formatting.provider": "black", 232 | "python.linting.pylintArgs": [ 233 | "--load-plugins", 234 | "pylint_django", 235 | "--rcfile", 236 | "setup.cfg" 237 | ], 238 | 239 | // Eslint 240 | "eslint.options": { 241 | "configFile": ".eslintrc.json" 242 | }, 243 | "eslint.nodePath": "assets/node_modules", 244 | "eslint.workingDirectories": ["assets/"], 245 | "editor.codeActionsOnSave": { 246 | "source.fixAll.eslint": true 247 | }, 248 | 249 | // Stylelint 250 | "css.validate": false, 251 | "less.validate": false, 252 | "scss.validate": false 253 | } 254 | 255 | To fill the `python.venvPath` run `poetry show -v` to see the path to your virtual environment. 256 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/apps/__init__.py -------------------------------------------------------------------------------- /apps/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/apps/misc/__init__.py -------------------------------------------------------------------------------- /apps/misc/choices.py: -------------------------------------------------------------------------------- 1 | class ChoicesMeta(type): 2 | def __getattribute__(cls, name): 3 | if name and not name.startswith("_"): 4 | try: 5 | value = cls.__dict__[name] 6 | if not callable(value): 7 | if isinstance(value, tuple): 8 | return value[0] 9 | else: 10 | return value 11 | except KeyError: 12 | pass 13 | return super().__getattribute__(name) 14 | 15 | 16 | class Choices(metaclass=ChoicesMeta): 17 | """ 18 | Helper class to create choices from django models. Every property you 19 | add is a choice. If you define as value a string, then the value is 20 | the database entry, while the property name is the display name. If 21 | you pass a tuple, the first element is the database entry, and the 22 | second the display name. 23 | 24 | Usage: 25 | 26 | class CarType(Choices): 27 | SUV = 'SUV', 28 | SEDAN = ('SD', 'Sedan') 29 | HATCHBACK = ('HB', 'Hatchback') 30 | CONVERTIBLE = ('CV', 'Convertible') 31 | COUPE = ('CP', 'Coupe') 32 | 33 | class Car(models.Model): 34 | type = models.CharField(max_length=10, choices=CarType.choices()) 35 | 36 | 37 | convertibles = Car.objects.filter(type=CarType.CONVERTIBLE) 38 | """ 39 | 40 | @classmethod 41 | def choices(cls): 42 | for attr_name in dir(cls): 43 | if attr_name.startswith("_"): 44 | continue 45 | if attr_name in ["keys", "choices"]: 46 | continue 47 | value = cls.__dict__[attr_name] 48 | if not callable(value): 49 | if isinstance(value, tuple): 50 | yield value 51 | else: 52 | yield value, attr_name 53 | 54 | @classmethod 55 | def keys(cls): 56 | return [choice[0] for choice in cls.choices()] 57 | -------------------------------------------------------------------------------- /apps/misc/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import get_language 2 | 3 | 4 | def django_settings(request): 5 | 6 | return { 7 | "LANGUAGE": get_language(), 8 | } 9 | -------------------------------------------------------------------------------- /apps/misc/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/apps/misc/management/__init__.py -------------------------------------------------------------------------------- /apps/misc/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/apps/misc/management/commands/__init__.py -------------------------------------------------------------------------------- /apps/misc/management/commands/celery_autoreload.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import subprocess 3 | 4 | from django.core.management.base import BaseCommand 5 | from django.utils import autoreload 6 | 7 | 8 | def restart_celery(*args, **kwargs): 9 | print("Restarting celery...") 10 | autoreload.raise_last_exception() 11 | kill_worker_cmd = "ps aux | grep bin/celery | awk '{print $2}' | xargs kill -9" 12 | subprocess.call(kill_worker_cmd, shell=True) 13 | start_worker_cmd = "celery -A conf worker -l info" 14 | subprocess.call(shlex.split(start_worker_cmd)) 15 | 16 | 17 | class Command(BaseCommand): 18 | def handle(self, *args, **options): 19 | self.stdout.write("Starting celery worker with autoreload...") 20 | autoreload.run_with_reloader(restart_celery, args=None, kwargs=None) 21 | -------------------------------------------------------------------------------- /apps/misc/management/commands/test_celery.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from apps.misc.tasks import task_dummy 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Closes the specified poll for voting" 8 | 9 | def handle(self, *args, **options): 10 | task_dummy.delay(1, 2) 11 | -------------------------------------------------------------------------------- /apps/misc/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | 5 | 6 | class TimestampedModel(models.Model): 7 | updated_at = models.DateTimeField(auto_now=True) 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | 10 | class Meta: 11 | abstract = True 12 | 13 | 14 | class KeyModel(TimestampedModel): 15 | key = models.CharField( 16 | max_length=255, unique=True, db_index=True, null=False, blank=True 17 | ) 18 | 19 | class Meta: 20 | abstract = True 21 | 22 | @property 23 | def short_key(self): 24 | return self.key[:8] 25 | 26 | def save(self, **kwargs): 27 | if not self.key: 28 | while True: 29 | new_key = str(uuid.uuid4()) 30 | try: 31 | self.__class__.objects.get(key=new_key) 32 | continue 33 | except self.__class__.DoesNotExist: 34 | self.key = new_key 35 | break 36 | super().save(**kwargs) 37 | -------------------------------------------------------------------------------- /apps/misc/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import task 2 | 3 | 4 | @task 5 | def task_dummy(arg_a, arg_b): 6 | print("Called task_dummy with a:%s b:%s" % (arg_a, arg_b)) 7 | -------------------------------------------------------------------------------- /apps/users/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "apps.users.apps.UsersApp" 2 | -------------------------------------------------------------------------------- /apps/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import models as base_models 3 | from django.contrib.auth.admin import UserAdmin as CoreUserAdmin 4 | 5 | from . import models 6 | 7 | 8 | @admin.register(models.User) 9 | class UserAdmin(CoreUserAdmin): 10 | ordering = ["id"] 11 | list_display = ( 12 | "id", 13 | "email", 14 | "is_active", 15 | "is_superuser", 16 | "last_login", 17 | "date_joined", 18 | ) 19 | list_display_links = ("id", "email") 20 | list_filter = ("is_active", "is_staff", "is_superuser", "last_login", "date_joined") 21 | search_fields = ("email",) 22 | 23 | fieldsets = ( 24 | (None, {"fields": ("password",)}), 25 | ("Permissions", {"fields": ("is_active", "is_staff", "is_superuser")}), 26 | ("Important dates", {"fields": ("last_login", "date_joined")}), 27 | ) 28 | 29 | add_fieldsets = ("Authentication", {"fields": ("email", "password1", "password2"),}) 30 | 31 | 32 | admin.site.unregister(base_models.Group) 33 | 34 | 35 | @admin.register(models.Group) 36 | class GroupAdmin(admin.ModelAdmin): 37 | list_display = ("id", "name") 38 | list_display_links = ("id", "name") 39 | search_fields = ("name",) 40 | -------------------------------------------------------------------------------- /apps/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersApp(AppConfig): 5 | name = "apps.users" 6 | -------------------------------------------------------------------------------- /apps/users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import models as base_models 2 | 3 | 4 | class UserManager(base_models.BaseUserManager): 5 | def create_user(self, email, password, **extra_fields): 6 | if not email: 7 | raise ValueError("The Email must be set") 8 | email = self.normalize_email(email) 9 | user = self.model(email=email, **extra_fields) 10 | user.set_password(password) 11 | user.save() 12 | return user 13 | 14 | def create_superuser(self, email, password, **extra_fields): 15 | extra_fields.setdefault("is_superuser", True) 16 | extra_fields.setdefault("is_staff", True) 17 | extra_fields.setdefault("is_active", True) 18 | 19 | if extra_fields.get("is_superuser") is not True: 20 | raise ValueError("Superuser must have is_superuser=True.") 21 | return self.create_user(email, password, **extra_fields) 22 | -------------------------------------------------------------------------------- /apps/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-26 07:18 2 | 3 | import django.contrib.auth.models 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0011_update_proxy_permissions"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Group", 19 | fields=[], 20 | options={"proxy": True, "indexes": [], "constraints": [],}, 21 | bases=("auth.group",), 22 | managers=[("objects", django.contrib.auth.models.GroupManager()),], 23 | ), 24 | migrations.CreateModel( 25 | name="User", 26 | fields=[ 27 | ( 28 | "id", 29 | models.AutoField( 30 | auto_created=True, 31 | primary_key=True, 32 | serialize=False, 33 | verbose_name="ID", 34 | ), 35 | ), 36 | ("password", models.CharField(max_length=128, verbose_name="password")), 37 | ( 38 | "last_login", 39 | models.DateTimeField( 40 | blank=True, null=True, verbose_name="last login" 41 | ), 42 | ), 43 | ( 44 | "is_superuser", 45 | models.BooleanField( 46 | default=False, 47 | help_text="Designates that this user has all permissions without explicitly assigning them.", 48 | verbose_name="superuser status", 49 | ), 50 | ), 51 | ( 52 | "email", 53 | models.EmailField( 54 | db_index=True, max_length=254, null=True, unique=True 55 | ), 56 | ), 57 | ("is_active", models.BooleanField(default=True)), 58 | ("is_staff", models.BooleanField(default=False)), 59 | ( 60 | "date_joined", 61 | models.DateTimeField(default=django.utils.timezone.now), 62 | ), 63 | ( 64 | "groups", 65 | models.ManyToManyField( 66 | blank=True, 67 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 68 | related_name="user_set", 69 | related_query_name="user", 70 | to="auth.Group", 71 | verbose_name="groups", 72 | ), 73 | ), 74 | ( 75 | "user_permissions", 76 | models.ManyToManyField( 77 | blank=True, 78 | help_text="Specific permissions for this user.", 79 | related_name="user_set", 80 | related_query_name="user", 81 | to="auth.Permission", 82 | verbose_name="user permissions", 83 | ), 84 | ), 85 | ], 86 | options={"abstract": False,}, 87 | ), 88 | ] 89 | -------------------------------------------------------------------------------- /apps/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/apps/users/migrations/__init__.py -------------------------------------------------------------------------------- /apps/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import models as base_models 2 | from django.db import models 3 | from django.utils import timezone 4 | 5 | from apps.users import managers 6 | 7 | 8 | class User(base_models.AbstractBaseUser, base_models.PermissionsMixin): 9 | email = models.EmailField(unique=True, null=True, db_index=True) 10 | is_active = models.BooleanField(default=True) 11 | is_staff = models.BooleanField(default=False) 12 | 13 | date_joined = models.DateTimeField(default=timezone.now) 14 | 15 | REQUIRED_FIELDS = [] 16 | USERNAME_FIELD = "email" 17 | 18 | objects = managers.UserManager() 19 | 20 | def get_full_name(self): 21 | return self.email 22 | 23 | def get_short_name(self): 24 | return self.email 25 | 26 | def get_username(self): 27 | return self.email 28 | 29 | 30 | class Group(base_models.Group): 31 | class Meta: 32 | proxy = True 33 | -------------------------------------------------------------------------------- /apps/users/urls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/apps/users/urls/__init__.py -------------------------------------------------------------------------------- /apps/users/urls/auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path("login/", auth_views.LoginView.as_view(), name="login"), 6 | path("logout/", auth_views.LogoutView.as_view(), name="logout"), 7 | path( 8 | "password-change/", 9 | auth_views.PasswordChangeView.as_view(), 10 | name="password_change", 11 | ), 12 | path( 13 | "password-change/done/", 14 | auth_views.PasswordChangeDoneView.as_view(), 15 | name="password_change_done", 16 | ), 17 | path( 18 | "password-reset/", auth_views.PasswordResetView.as_view(), name="password_reset" 19 | ), 20 | path( 21 | "password-reset/done/", 22 | auth_views.PasswordResetDoneView.as_view(), 23 | name="password_reset_done", 24 | ), 25 | path( 26 | "reset///", 27 | auth_views.PasswordResetConfirmView.as_view(), 28 | name="password_reset_confirm", 29 | ), 30 | path( 31 | "reset/done/", 32 | auth_views.PasswordResetCompleteView.as_view(), 33 | name="password_reset_complete", 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /assets/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:prettier/recommended"], 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "es6": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "indentation": 2, 5 | "number-leading-zero": null, 6 | "string-quotes": "double", 7 | "block-opening-brace-space-before": "always", 8 | "at-rule-no-unknown": [ 9 | true, 10 | { 11 | "ignoreAtRules": ["function", "if", "each", "include", "mixin"] 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/assets/img/favicon.png -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | import "./scss/main.scss"; 2 | import "./js/main.js"; 3 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | const name = "Universe"; 2 | 3 | console.log(`Hello ${name}! (Javascript ES6)`); 4 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-starter-project", 3 | "version": "1.0.0", 4 | "description": "Asset files for django project.", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev": "webpack --mode development --env dev --watch", 8 | "build_stg": "webpack --mode production --env stg", 9 | "build_prod": "webpack --mode production --env prod", 10 | "lint:js": "eslint js/** --fix", 11 | "lint:scss": "stylelint scss/*.scss --syntax scss --fix" 12 | }, 13 | "author": "Francisco Ceruti ", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.13.16", 17 | "@babel/preset-env": "^7.13.15", 18 | "babel-eslint": "^10.1.0", 19 | "babel-loader": "^8.2.2", 20 | "css-loader": "^3.6.0", 21 | "eslint": "^6.8.0", 22 | "eslint-config-prettier": "^6.15.0", 23 | "eslint-plugin-prettier": "^3.4.0", 24 | "file-loader": "^5.1.0", 25 | "mini-css-extract-plugin": "^0.9.0", 26 | "node-sass": "^7.0.0", 27 | "prettier": "^1.19.1", 28 | "sass-loader": "^8.0.2", 29 | "stylelint": "^13.12.0", 30 | "stylelint-config-standard": "^19.0.0", 31 | "webpack": "^4.46.0", 32 | "webpack-bundle-tracker": "^0.4.3", 33 | "webpack-cli": "^3.3.12" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $color: red; 2 | -------------------------------------------------------------------------------- /assets/scss/libs/reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | border: 0; 88 | font: inherit; 89 | font-size: 100%; 90 | margin: 0; 91 | padding: 0; 92 | vertical-align: baseline; 93 | } 94 | 95 | /* HTML5 display-role reset for older browsers */ 96 | 97 | article, 98 | aside, 99 | details, 100 | figcaption, 101 | figure, 102 | footer, 103 | header, 104 | hgroup, 105 | menu, 106 | nav, 107 | section { 108 | display: block; 109 | } 110 | 111 | body { 112 | line-height: 1; 113 | } 114 | 115 | ol, 116 | ul { 117 | list-style: none; 118 | } 119 | 120 | blockquote, 121 | q { 122 | quotes: none; 123 | } 124 | 125 | blockquote { 126 | &::before, 127 | &::after { 128 | content: ""; 129 | content: none; 130 | } 131 | } 132 | 133 | q { 134 | &::before, 135 | &::after { 136 | content: ""; 137 | content: none; 138 | } 139 | } 140 | 141 | table { 142 | border-collapse: collapse; 143 | border-spacing: 0; 144 | } 145 | -------------------------------------------------------------------------------- /assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "libs/reset"; 3 | @import "structure"; 4 | 5 | body { 6 | color: $color; 7 | } 8 | -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 2 | const path = require("path"); 3 | const webpack = require("webpack"); 4 | const BundleTracker = require("webpack-bundle-tracker"); 5 | 6 | const resolve = path.resolve.bind(path, __dirname); 7 | 8 | module.exports = (env, argv) => { 9 | let output; 10 | let extractCssPlugin; 11 | let fileLoaderName; 12 | 13 | switch (env) { 14 | case "prod": 15 | publicPath = "https://example.com/static/bundles/prod/"; 16 | outputPath = resolve("bundles/prod"); 17 | break; 18 | case "stg": 19 | publicPath = "https://staging.example.com/static/bundles/stg/"; 20 | outputPath = resolve("bundles/stg"); 21 | break; 22 | case "dev": 23 | publicPath = "http://127.0.0.1:8000/static/bundles/dev/"; 24 | outputPath = resolve("bundles/dev"); 25 | break; 26 | } 27 | 28 | switch (argv.mode) { 29 | case "production": 30 | output = { 31 | path: outputPath, 32 | filename: "[chunkhash]/[name].js", 33 | chunkFilename: "[chunkhash]/[name].[id].js", 34 | publicPath: publicPath 35 | }; 36 | extractCssPlugin = new MiniCssExtractPlugin({ 37 | filename: "[chunkhash]/[name].css", 38 | chunkFilename: "[chunkhash]/[name].[id].css" 39 | }); 40 | fileLoaderName = "[path][name].[contenthash].[ext]"; 41 | break; 42 | 43 | case "development": 44 | output = { 45 | path: outputPath, 46 | filename: "[name].js", 47 | chunkFilename: "[name].js", 48 | publicPath: publicPath 49 | }; 50 | extractCssPlugin = new MiniCssExtractPlugin({ 51 | filename: "[name].css", 52 | chunkFilename: "[name].[id].css" 53 | }); 54 | fileLoaderName = "[path][name].[ext]"; 55 | break; 56 | default: 57 | break; 58 | } 59 | 60 | return { 61 | mode: argv.mode, 62 | entry: "./index.js", 63 | output, 64 | module: { 65 | rules: [ 66 | // Scripts 67 | { 68 | test: /\.js$/, 69 | exclude: /node_modules/, 70 | loader: "babel-loader" 71 | }, 72 | // Styles 73 | { 74 | test: /\.(sa|sc|c)ss$/, 75 | use: [ 76 | MiniCssExtractPlugin.loader, 77 | { 78 | loader: "css-loader", 79 | options: { 80 | sourceMap: true 81 | } 82 | }, 83 | { 84 | loader: "sass-loader", 85 | options: { 86 | sourceMap: true 87 | } 88 | } 89 | ] 90 | }, 91 | // Fonts 92 | { 93 | test: /\.(eot|otf|ttf|woff|woff2)(\?v=[0-9.]+)?$/, 94 | loader: "file-loader", 95 | options: { 96 | outputPath: "fonts", 97 | name: fileLoaderName 98 | } 99 | }, 100 | // Images 101 | { 102 | test: /\.(png|svg|jpg)(\?v=[0-9.]+)?$/, 103 | loader: "file-loader", 104 | options: { 105 | outputPath: "images", 106 | name: fileLoaderName 107 | } 108 | } 109 | ] 110 | }, 111 | plugins: [ 112 | new BundleTracker({ 113 | filename: `bundles/webpack-bundle.${env}.json` 114 | }), 115 | extractCssPlugin 116 | ], 117 | devtool: "source-map" 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /conf/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /conf/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | class CustomAdminSite(admin.AdminSite): 5 | # Text to put at the end of each page's . 6 | site_title = "Admin" 7 | 8 | # Text to put in each page's <h1>. 9 | site_header = "Admin" 10 | 11 | # Text to put at the top of the admin index page. 12 | index_title = "Admin" 13 | -------------------------------------------------------------------------------- /conf/apps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.apps import AdminConfig 2 | 3 | 4 | class CustomAdminConfig(AdminConfig): 5 | default_site = "conf.admin.CustomAdminSite" 6 | -------------------------------------------------------------------------------- /conf/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | # set the default Django settings module for the 'celery' program. 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings") 7 | 8 | app = Celery("conf") 9 | 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object("django.conf:settings", namespace="CELERY") 15 | 16 | # Load task modules from all registered Django app configs. 17 | app.autodiscover_tasks() 18 | -------------------------------------------------------------------------------- /conf/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import environ 4 | 5 | env = environ.Env() 6 | root_path = environ.Path(__file__) - 2 7 | env.read_env(env_file=root_path(".env")) 8 | 9 | 10 | # ----------------------------------------------------------------------------- 11 | # Basic Config 12 | # ----------------------------------------------------------------------------- 13 | ENV = env("ENV", default="prod") 14 | assert ENV in ["dev", "test", "prod", "qa"] 15 | DEBUG = env.bool("DEBUG", default=False) 16 | BASE_DIR = root_path() 17 | ROOT_URLCONF = "conf.urls" 18 | WSGI_APPLICATION = "conf.wsgi.application" 19 | 20 | # ----------------------------------------------------------------------------- 21 | # Time & Language 22 | # ----------------------------------------------------------------------------- 23 | LANGUAGE_CODE = env("LANGUAGE_CODE", default="en-us") 24 | TIME_ZONE = env("TIMEZONE", default="UTC") 25 | USE_I18N = env("USE_I18N", default=True) 26 | USE_L10N = env("USE_L10N", default=True) 27 | USE_TZ = env("USE_TZ", default=True) 28 | 29 | # ----------------------------------------------------------------------------- 30 | # Emails 31 | # ----------------------------------------------------------------------------- 32 | DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL", default="") 33 | EMAIL_BACKEND = env( 34 | "EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend" 35 | ) 36 | 37 | # ----------------------------------------------------------------------------- 38 | # Security and Users 39 | # ----------------------------------------------------------------------------- 40 | SECRET_KEY = env("SECRET_KEY") 41 | ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[]) 42 | AUTH_USER_MODEL = "users.User" 43 | AUTH_PASSWORD_VALIDATORS = [ 44 | { 45 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 46 | }, 47 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 48 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 49 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 50 | ] 51 | AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) 52 | LOGIN_URL = env("LOGIN_URL", default="/login/") 53 | LOGIN_REDIRECT_URL = env("LOGIN_REDIRECT_URL", default="/") 54 | 55 | # ----------------------------------------------------------------------------- 56 | # Databases 57 | # ----------------------------------------------------------------------------- 58 | DJANGO_DATABASE_URL = env.db("DATABASE_URL") 59 | DATABASES = {"default": DJANGO_DATABASE_URL} 60 | 61 | # ----------------------------------------------------------------------------- 62 | # Applications configuration 63 | # ----------------------------------------------------------------------------- 64 | INSTALLED_APPS = [ 65 | # First party 66 | "django.contrib.auth", 67 | "django.contrib.contenttypes", 68 | "django.contrib.sessions", 69 | "django.contrib.messages", 70 | "django.contrib.staticfiles", 71 | # Third party 72 | "webpack_loader", 73 | # Local 74 | "conf.apps.CustomAdminConfig", 75 | "apps.misc", 76 | "apps.users", 77 | ] 78 | 79 | MIDDLEWARE = [ 80 | "django.middleware.security.SecurityMiddleware", 81 | "django.contrib.sessions.middleware.SessionMiddleware", 82 | "django.middleware.common.CommonMiddleware", 83 | "django.middleware.csrf.CsrfViewMiddleware", 84 | "django.contrib.auth.middleware.AuthenticationMiddleware", 85 | "django.contrib.messages.middleware.MessageMiddleware", 86 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 87 | ] 88 | 89 | TEMPLATES = [ 90 | { 91 | "BACKEND": "django.template.backends.django.DjangoTemplates", 92 | "DIRS": [root_path("templates")], 93 | "APP_DIRS": True, 94 | "OPTIONS": { 95 | "context_processors": [ 96 | "django.template.context_processors.debug", 97 | "django.template.context_processors.request", 98 | "django.contrib.auth.context_processors.auth", 99 | "django.contrib.messages.context_processors.messages", 100 | "apps.misc.context_processors.django_settings", 101 | ], 102 | }, 103 | }, 104 | ] 105 | 106 | # ----------------------------------------------------------------------------- 107 | # Static & Media Files 108 | # ----------------------------------------------------------------------------- 109 | STATIC_URL = env("STATIC_URL", default="/static/") 110 | STATIC_ROOT = env("STATIC_ROOT", default=root_path("static")) 111 | 112 | MEDIA_URL = env("MEDIA_URL", default="/media/") 113 | MEDIA_ROOT = env("MEDIA_ROOT", default=root_path("media")) 114 | ADMIN_MEDIA_PREFIX = STATIC_URL + "admin/" 115 | 116 | STATICFILES_DIRS = ( 117 | ("bundles", root_path("assets/bundles")), 118 | ("img", root_path("assets/img")), 119 | ) 120 | 121 | webpack_stats_filename = "webpack-bundle.%s.json" % ENV 122 | stats_file = os.path.join(root_path("assets/bundles/"), webpack_stats_filename) 123 | 124 | WEBPACK_LOADER = { 125 | "DEFAULT": { 126 | "CACHE": not DEBUG, 127 | "BUNDLE_DIR_NAME": "bundles/", # must end with slash 128 | "STATS_FILE": stats_file, 129 | "POLL_INTERVAL": 0.1, 130 | "TIMEOUT": None, 131 | "IGNORE": [r".+\.hot-update.js", r".+\.map"], 132 | } 133 | } 134 | 135 | USE_S3_STATIC_STORAGE = env.bool("USE_S3_STATIC_STORAGE", default=False) 136 | 137 | if USE_S3_STATIC_STORAGE: 138 | AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") 139 | AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") 140 | AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") 141 | AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL") 142 | AWS_S3_OBJECT_PARAMETERS = { 143 | "CacheControl": "max-age=86400", 144 | } 145 | AWS_LOCATION = "static" 146 | AWS_DEFAULT_ACL = "public-read" 147 | STATIC_URL = f"{AWS_S3_ENDPOINT_URL}/{AWS_LOCATION}/" 148 | STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 149 | 150 | # ----------------------------------------------------------------------------- 151 | # Celery 152 | # ----------------------------------------------------------------------------- 153 | CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://cache") 154 | CELERY_TASK_ALWAYS_EAGER = env("CELERY_TASK_ALWAYS_EAGER", default=False) 155 | 156 | # ----------------------------------------------------------------------------- 157 | # Django Debug Toolbar 158 | # ----------------------------------------------------------------------------- 159 | USE_DEBUG_TOOLBAR = env.bool("USE_DEBUG_TOOLBAR", default=DEBUG) 160 | 161 | if USE_DEBUG_TOOLBAR: 162 | INSTALLED_APPS += ["debug_toolbar"] 163 | INTERNAL_IPS = ["127.0.0.1"] 164 | MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") 165 | 166 | # ----------------------------------------------------------------------------- 167 | # Django Extensions 168 | # ----------------------------------------------------------------------------- 169 | USE_DJANGO_EXTENSIONS = env.bool("USE_DJANGO_EXTENSIONS", default=False) 170 | if USE_DJANGO_EXTENSIONS: 171 | INSTALLED_APPS += [ 172 | "django_extensions", 173 | ] 174 | 175 | # ----------------------------------------------------------------------------- 176 | # Logging 177 | # ----------------------------------------------------------------------------- 178 | LOGS_ROOT = env("LOGS_ROOT", default=root_path("logs")) 179 | LOGGING = { 180 | "version": 1, 181 | "disable_existing_loggers": False, 182 | "formatters": { 183 | "console_format": {"format": "%(name)-12s %(levelname)-8s %(message)s"}, 184 | "file_format": { 185 | "format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s" 186 | }, 187 | }, 188 | "handlers": { 189 | "console": { 190 | "level": "DEBUG", 191 | "class": "logging.StreamHandler", 192 | "formatter": "console_format", 193 | }, 194 | "file": { 195 | "level": "INFO", 196 | "class": "logging.handlers.RotatingFileHandler", 197 | "filename": os.path.join(LOGS_ROOT, "django.log"), 198 | "maxBytes": 1024 * 1024 * 15, # 15MB 199 | "backupCount": 10, 200 | "formatter": "file_format", 201 | }, 202 | }, 203 | "loggers": { 204 | "django": { 205 | "level": "INFO", 206 | "handlers": ["console", "file"], 207 | "propagate": False, 208 | }, 209 | "apps": { 210 | "level": "DEBUG", 211 | "handlers": ["console", "file"], 212 | "propagate": False, 213 | }, 214 | }, 215 | } 216 | 217 | USE_SENTRY = env.bool("USE_SENTRY", default=False) 218 | 219 | if USE_SENTRY: 220 | import sentry_sdk 221 | from sentry_sdk.integrations.django import DjangoIntegration 222 | 223 | sentry_sdk.init( 224 | dsn=env("SENTRY_DSN"), integrations=[DjangoIntegration()], environment=ENV 225 | ) 226 | -------------------------------------------------------------------------------- /conf/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from django.views.generic import TemplateView 6 | 7 | urlpatterns = [ 8 | path("", include("apps.users.urls.auth")), 9 | path("admin/", admin.site.urls), 10 | ] 11 | 12 | if settings.DEBUG: 13 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 14 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 15 | 16 | if settings.USE_DEBUG_TOOLBAR: 17 | import debug_toolbar 18 | 19 | urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),) 20 | 21 | 22 | if settings.ENV == "dev": 23 | urlpatterns += [ 24 | path("error/403/", TemplateView.as_view(template_name="403.html")), 25 | path("error/404/", TemplateView.as_view(template_name="404.html")), 26 | path("error/500/", TemplateView.as_view(template_name="500.html")), 27 | ] 28 | -------------------------------------------------------------------------------- /conf/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for conf project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | web: 5 | image: dev_server 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | volumes: 10 | - .:/code 11 | - ./logs/web:/logs 12 | - ./media:/uploads 13 | command: poetry run python manage.py runserver_plus 0.0.0.0:8000 14 | ports: 15 | - 8000:8000 16 | env_file: 17 | - .env 18 | - .env.docker 19 | environment: 20 | - ALLOWED_HOSTS="*" 21 | - BROKER_URL=redis://cache 22 | - MEDIA_ROOT=/uploads 23 | - LOGS_ROOT=/logs 24 | links: 25 | - redis:cache 26 | 27 | worker: 28 | image: dev_worker 29 | build: 30 | context: . 31 | dockerfile: Dockerfile 32 | volumes: 33 | - .:/code 34 | - ./logs/worker:/logs 35 | - ./media:/uploads 36 | command: poetry run worker 37 | env_file: 38 | - .env 39 | - .env.docker 40 | environment: 41 | - BROKER_URL=redis://cache 42 | - MEDIA_ROOT=/uploads 43 | - LOGS_ROOT=/logs 44 | links: 45 | - redis:cache 46 | 47 | redis: 48 | image: redis 49 | expose: 50 | - "6379" 51 | 52 | webpack: 53 | image: dev_webpack 54 | build: 55 | context: . 56 | dockerfile: Dockerfile-webpack 57 | volumes: 58 | - ./assets:/code 59 | command: npm run dev 60 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "amqp" 3 | version = "2.6.1" 4 | description = "Low-level AMQP client for Python (fork of amqplib)." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 8 | 9 | [package.dependencies] 10 | vine = ">=1.1.3,<5.0.0a1" 11 | 12 | [[package]] 13 | name = "appdirs" 14 | version = "1.4.4" 15 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 16 | category = "dev" 17 | optional = false 18 | python-versions = "*" 19 | 20 | [[package]] 21 | name = "asgiref" 22 | version = "3.3.4" 23 | description = "ASGI specs, helper code, and adapters" 24 | category = "main" 25 | optional = false 26 | python-versions = ">=3.6" 27 | 28 | [package.dependencies] 29 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 30 | 31 | [package.extras] 32 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 33 | 34 | [[package]] 35 | name = "astroid" 36 | version = "2.5.3" 37 | description = "An abstract syntax tree for Python with inference support." 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=3.6" 41 | 42 | [package.dependencies] 43 | lazy-object-proxy = ">=1.4.0" 44 | typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 45 | wrapt = ">=1.11,<1.13" 46 | 47 | [[package]] 48 | name = "atomicwrites" 49 | version = "1.4.0" 50 | description = "Atomic file writes." 51 | category = "dev" 52 | optional = false 53 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 54 | 55 | [[package]] 56 | name = "attrs" 57 | version = "20.3.0" 58 | description = "Classes Without Boilerplate" 59 | category = "dev" 60 | optional = false 61 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 62 | 63 | [package.extras] 64 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 65 | docs = ["furo", "sphinx", "zope.interface"] 66 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 67 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 68 | 69 | [[package]] 70 | name = "billiard" 71 | version = "3.6.4.0" 72 | description = "Python multiprocessing fork with improvements and bugfixes" 73 | category = "main" 74 | optional = false 75 | python-versions = "*" 76 | 77 | [[package]] 78 | name = "black" 79 | version = "19.10b0" 80 | description = "The uncompromising code formatter." 81 | category = "dev" 82 | optional = false 83 | python-versions = ">=3.6" 84 | 85 | [package.dependencies] 86 | appdirs = "*" 87 | attrs = ">=18.1.0" 88 | click = ">=6.5" 89 | pathspec = ">=0.6,<1" 90 | regex = "*" 91 | toml = ">=0.9.4" 92 | typed-ast = ">=1.4.0" 93 | 94 | [package.extras] 95 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 96 | 97 | [[package]] 98 | name = "celery" 99 | version = "4.4.7" 100 | description = "Distributed Task Queue." 101 | category = "main" 102 | optional = false 103 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 104 | 105 | [package.dependencies] 106 | billiard = ">=3.6.3.0,<4.0" 107 | kombu = ">=4.6.10,<4.7" 108 | pytz = ">0.0-dev" 109 | vine = "1.3.0" 110 | 111 | [package.extras] 112 | arangodb = ["pyArango (>=1.3.2)"] 113 | auth = ["cryptography"] 114 | azureblockblob = ["azure-storage (==0.36.0)", "azure-common (==1.1.5)", "azure-storage-common (==1.1.0)"] 115 | brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] 116 | cassandra = ["cassandra-driver (<3.21.0)"] 117 | consul = ["python-consul"] 118 | cosmosdbsql = ["pydocumentdb (==2.3.2)"] 119 | couchbase = ["couchbase-cffi (<3.0.0)", "couchbase (<3.0.0)"] 120 | couchdb = ["pycouchdb"] 121 | django = ["Django (>=1.11)"] 122 | dynamodb = ["boto3 (>=1.9.178)"] 123 | elasticsearch = ["elasticsearch"] 124 | eventlet = ["eventlet (>=0.24.1)"] 125 | gevent = ["gevent"] 126 | librabbitmq = ["librabbitmq (>=1.5.0)"] 127 | lzma = ["backports.lzma"] 128 | memcache = ["pylibmc"] 129 | mongodb = ["pymongo[srv] (>=3.3.0)"] 130 | msgpack = ["msgpack"] 131 | pymemcache = ["python-memcached"] 132 | pyro = ["pyro4"] 133 | redis = ["redis (>=3.2.0)"] 134 | riak = ["riak (>=2.0)"] 135 | s3 = ["boto3 (>=1.9.125)"] 136 | slmq = ["softlayer-messaging (>=1.0.3)"] 137 | solar = ["ephem"] 138 | sqlalchemy = ["sqlalchemy"] 139 | sqs = ["boto3 (>=1.9.125)", "pycurl (==7.43.0.5)"] 140 | tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] 141 | yaml = ["PyYAML (>=3.10)"] 142 | zookeeper = ["kazoo (>=1.3.1)"] 143 | zstd = ["zstandard"] 144 | 145 | [[package]] 146 | name = "certifi" 147 | version = "2020.12.5" 148 | description = "Python package for providing Mozilla's CA Bundle." 149 | category = "main" 150 | optional = false 151 | python-versions = "*" 152 | 153 | [[package]] 154 | name = "click" 155 | version = "7.1.2" 156 | description = "Composable command line interface toolkit" 157 | category = "dev" 158 | optional = false 159 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 160 | 161 | [[package]] 162 | name = "colorama" 163 | version = "0.4.4" 164 | description = "Cross-platform colored terminal text." 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 168 | 169 | [[package]] 170 | name = "django" 171 | version = "3.2.13" 172 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 173 | category = "main" 174 | optional = false 175 | python-versions = ">=3.6" 176 | 177 | [package.dependencies] 178 | asgiref = ">=3.3.2,<4" 179 | pytz = "*" 180 | sqlparse = ">=0.2.2" 181 | 182 | [package.extras] 183 | argon2 = ["argon2-cffi (>=19.1.0)"] 184 | bcrypt = ["bcrypt"] 185 | 186 | [[package]] 187 | name = "django-debug-toolbar" 188 | version = "2.2.1" 189 | description = "A configurable set of panels that display various debug information about the current request/response." 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=3.5" 193 | 194 | [package.dependencies] 195 | Django = ">=1.11" 196 | sqlparse = ">=0.2.0" 197 | 198 | [[package]] 199 | name = "django-environ" 200 | version = "0.4.5" 201 | description = "Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application." 202 | category = "main" 203 | optional = false 204 | python-versions = "*" 205 | 206 | [[package]] 207 | name = "django-extensions" 208 | version = "2.2.9" 209 | description = "Extensions for Django" 210 | category = "main" 211 | optional = false 212 | python-versions = "*" 213 | 214 | [package.dependencies] 215 | six = ">=1.2" 216 | 217 | [[package]] 218 | name = "django-storages" 219 | version = "1.11.1" 220 | description = "Support for many storage backends in Django" 221 | category = "main" 222 | optional = false 223 | python-versions = ">=3.5" 224 | 225 | [package.dependencies] 226 | Django = ">=2.2" 227 | 228 | [package.extras] 229 | azure = ["azure-storage-blob (>=1.3.1,<12.0.0)"] 230 | boto3 = ["boto3 (>=1.4.4)"] 231 | dropbox = ["dropbox (>=7.2.1)"] 232 | google = ["google-cloud-storage (>=1.15.0)"] 233 | libcloud = ["apache-libcloud"] 234 | sftp = ["paramiko"] 235 | 236 | [[package]] 237 | name = "django-webpack-loader" 238 | version = "0.6.0" 239 | description = "Transparently use webpack with django" 240 | category = "main" 241 | optional = false 242 | python-versions = "*" 243 | 244 | [[package]] 245 | name = "flake8" 246 | version = "3.9.1" 247 | description = "the modular source code checker: pep8 pyflakes and co" 248 | category = "dev" 249 | optional = false 250 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 251 | 252 | [package.dependencies] 253 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 254 | mccabe = ">=0.6.0,<0.7.0" 255 | pycodestyle = ">=2.7.0,<2.8.0" 256 | pyflakes = ">=2.3.0,<2.4.0" 257 | 258 | [[package]] 259 | name = "importlib-metadata" 260 | version = "4.0.1" 261 | description = "Read metadata from Python packages" 262 | category = "main" 263 | optional = false 264 | python-versions = ">=3.6" 265 | 266 | [package.dependencies] 267 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 268 | zipp = ">=0.5" 269 | 270 | [package.extras] 271 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 272 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 273 | 274 | [[package]] 275 | name = "isort" 276 | version = "5.8.0" 277 | description = "A Python utility / library to sort Python imports." 278 | category = "dev" 279 | optional = false 280 | python-versions = ">=3.6,<4.0" 281 | 282 | [package.extras] 283 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 284 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 285 | colors = ["colorama (>=0.4.3,<0.5.0)"] 286 | 287 | [[package]] 288 | name = "kombu" 289 | version = "4.6.11" 290 | description = "Messaging library for Python." 291 | category = "main" 292 | optional = false 293 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 294 | 295 | [package.dependencies] 296 | amqp = ">=2.6.0,<2.7" 297 | importlib-metadata = {version = ">=0.18", markers = "python_version < \"3.8\""} 298 | 299 | [package.extras] 300 | azureservicebus = ["azure-servicebus (>=0.21.1)"] 301 | azurestoragequeues = ["azure-storage-queue"] 302 | consul = ["python-consul (>=0.6.0)"] 303 | librabbitmq = ["librabbitmq (>=1.5.2)"] 304 | mongodb = ["pymongo (>=3.3.0)"] 305 | msgpack = ["msgpack"] 306 | pyro = ["pyro4"] 307 | qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] 308 | redis = ["redis (>=3.3.11)"] 309 | slmq = ["softlayer-messaging (>=1.0.3)"] 310 | sqlalchemy = ["sqlalchemy"] 311 | sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)"] 312 | yaml = ["PyYAML (>=3.10)"] 313 | zookeeper = ["kazoo (>=1.3.1)"] 314 | 315 | [[package]] 316 | name = "lazy-object-proxy" 317 | version = "1.6.0" 318 | description = "A fast and thorough lazy object proxy." 319 | category = "dev" 320 | optional = false 321 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 322 | 323 | [[package]] 324 | name = "mccabe" 325 | version = "0.6.1" 326 | description = "McCabe checker, plugin for flake8" 327 | category = "dev" 328 | optional = false 329 | python-versions = "*" 330 | 331 | [[package]] 332 | name = "more-itertools" 333 | version = "8.7.0" 334 | description = "More routines for operating on iterables, beyond itertools" 335 | category = "dev" 336 | optional = false 337 | python-versions = ">=3.5" 338 | 339 | [[package]] 340 | name = "packaging" 341 | version = "20.9" 342 | description = "Core utilities for Python packages" 343 | category = "dev" 344 | optional = false 345 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 346 | 347 | [package.dependencies] 348 | pyparsing = ">=2.0.2" 349 | 350 | [[package]] 351 | name = "pathspec" 352 | version = "0.8.1" 353 | description = "Utility library for gitignore style pattern matching of file paths." 354 | category = "dev" 355 | optional = false 356 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 357 | 358 | [[package]] 359 | name = "pluggy" 360 | version = "0.13.1" 361 | description = "plugin and hook calling mechanisms for python" 362 | category = "dev" 363 | optional = false 364 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 365 | 366 | [package.dependencies] 367 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 368 | 369 | [package.extras] 370 | dev = ["pre-commit", "tox"] 371 | 372 | [[package]] 373 | name = "psycopg2-binary" 374 | version = "2.8.6" 375 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 376 | category = "main" 377 | optional = false 378 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 379 | 380 | [[package]] 381 | name = "py" 382 | version = "1.10.0" 383 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 384 | category = "dev" 385 | optional = false 386 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 387 | 388 | [[package]] 389 | name = "pycodestyle" 390 | version = "2.7.0" 391 | description = "Python style guide checker" 392 | category = "dev" 393 | optional = false 394 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 395 | 396 | [[package]] 397 | name = "pyflakes" 398 | version = "2.3.1" 399 | description = "passive checker of Python programs" 400 | category = "dev" 401 | optional = false 402 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 403 | 404 | [[package]] 405 | name = "pylint" 406 | version = "2.7.4" 407 | description = "python code static checker" 408 | category = "dev" 409 | optional = false 410 | python-versions = "~=3.6" 411 | 412 | [package.dependencies] 413 | astroid = ">=2.5.2,<2.7" 414 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 415 | isort = ">=4.2.5,<6" 416 | mccabe = ">=0.6,<0.7" 417 | toml = ">=0.7.1" 418 | 419 | [package.extras] 420 | docs = ["sphinx (==3.5.1)", "python-docs-theme (==2020.12)"] 421 | 422 | [[package]] 423 | name = "pylint-django" 424 | version = "2.4.3" 425 | description = "A Pylint plugin to help Pylint understand the Django web framework" 426 | category = "dev" 427 | optional = false 428 | python-versions = "*" 429 | 430 | [package.dependencies] 431 | pylint = ">=2.0" 432 | pylint-plugin-utils = ">=0.5" 433 | 434 | [package.extras] 435 | for_tests = ["django-tables2", "factory-boy", "coverage", "pytest"] 436 | with_django = ["django"] 437 | 438 | [[package]] 439 | name = "pylint-plugin-utils" 440 | version = "0.6" 441 | description = "Utilities and helpers for writing Pylint plugins" 442 | category = "dev" 443 | optional = false 444 | python-versions = "*" 445 | 446 | [package.dependencies] 447 | pylint = ">=1.7" 448 | 449 | [[package]] 450 | name = "pyparsing" 451 | version = "2.4.7" 452 | description = "Python parsing module" 453 | category = "dev" 454 | optional = false 455 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 456 | 457 | [[package]] 458 | name = "pytest" 459 | version = "5.4.3" 460 | description = "pytest: simple powerful testing with Python" 461 | category = "dev" 462 | optional = false 463 | python-versions = ">=3.5" 464 | 465 | [package.dependencies] 466 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 467 | attrs = ">=17.4.0" 468 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 469 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 470 | more-itertools = ">=4.0.0" 471 | packaging = "*" 472 | pluggy = ">=0.12,<1.0" 473 | py = ">=1.5.0" 474 | wcwidth = "*" 475 | 476 | [package.extras] 477 | checkqa-mypy = ["mypy (==v0.761)"] 478 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 479 | 480 | [[package]] 481 | name = "pytest-django" 482 | version = "3.10.0" 483 | description = "A Django plugin for pytest." 484 | category = "dev" 485 | optional = false 486 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 487 | 488 | [package.dependencies] 489 | pytest = ">=3.6" 490 | 491 | [package.extras] 492 | docs = ["sphinx", "sphinx-rtd-theme"] 493 | testing = ["django", "django-configurations (>=2.0)", "six"] 494 | 495 | [[package]] 496 | name = "pytest-env" 497 | version = "0.6.2" 498 | description = "py.test plugin that allows you to add environment variables." 499 | category = "dev" 500 | optional = false 501 | python-versions = "*" 502 | 503 | [package.dependencies] 504 | pytest = ">=2.6.0" 505 | 506 | [[package]] 507 | name = "pytz" 508 | version = "2021.1" 509 | description = "World timezone definitions, modern and historical" 510 | category = "main" 511 | optional = false 512 | python-versions = "*" 513 | 514 | [[package]] 515 | name = "redis" 516 | version = "3.5.3" 517 | description = "Python client for Redis key-value store" 518 | category = "main" 519 | optional = false 520 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 521 | 522 | [package.extras] 523 | hiredis = ["hiredis (>=0.1.3)"] 524 | 525 | [[package]] 526 | name = "regex" 527 | version = "2021.4.4" 528 | description = "Alternative regular expression module, to replace re." 529 | category = "dev" 530 | optional = false 531 | python-versions = "*" 532 | 533 | [[package]] 534 | name = "sentry-sdk" 535 | version = "0.14.4" 536 | description = "Python client for Sentry (https://getsentry.com)" 537 | category = "main" 538 | optional = false 539 | python-versions = "*" 540 | 541 | [package.dependencies] 542 | certifi = "*" 543 | urllib3 = ">=1.10.0" 544 | 545 | [package.extras] 546 | aiohttp = ["aiohttp (>=3.5)"] 547 | beam = ["beam (>=2.12)"] 548 | bottle = ["bottle (>=0.12.13)"] 549 | celery = ["celery (>=3)"] 550 | django = ["django (>=1.8)"] 551 | falcon = ["falcon (>=1.4)"] 552 | flask = ["flask (>=0.11)", "blinker (>=1.1)"] 553 | pyspark = ["pyspark (>=2.4.4)"] 554 | rq = ["rq (>=0.6)"] 555 | sanic = ["sanic (>=0.8)"] 556 | sqlalchemy = ["sqlalchemy (>=1.2)"] 557 | tornado = ["tornado (>=5)"] 558 | 559 | [[package]] 560 | name = "six" 561 | version = "1.15.0" 562 | description = "Python 2 and 3 compatibility utilities" 563 | category = "main" 564 | optional = false 565 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 566 | 567 | [[package]] 568 | name = "sqlparse" 569 | version = "0.4.2" 570 | description = "A non-validating SQL parser." 571 | category = "main" 572 | optional = false 573 | python-versions = ">=3.5" 574 | 575 | [[package]] 576 | name = "toml" 577 | version = "0.10.2" 578 | description = "Python Library for Tom's Obvious, Minimal Language" 579 | category = "dev" 580 | optional = false 581 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 582 | 583 | [[package]] 584 | name = "typed-ast" 585 | version = "1.4.3" 586 | description = "a fork of Python 2 and 3 ast modules with type comment support" 587 | category = "dev" 588 | optional = false 589 | python-versions = "*" 590 | 591 | [[package]] 592 | name = "typing-extensions" 593 | version = "3.7.4.3" 594 | description = "Backported and Experimental Type Hints for Python 3.5+" 595 | category = "main" 596 | optional = false 597 | python-versions = "*" 598 | 599 | [[package]] 600 | name = "urllib3" 601 | version = "1.26.5" 602 | description = "HTTP library with thread-safe connection pooling, file post, and more." 603 | category = "main" 604 | optional = false 605 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 606 | 607 | [package.extras] 608 | brotli = ["brotlipy (>=0.6.0)"] 609 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 610 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 611 | 612 | [[package]] 613 | name = "vine" 614 | version = "1.3.0" 615 | description = "Promises, promises, promises." 616 | category = "main" 617 | optional = false 618 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 619 | 620 | [[package]] 621 | name = "wcwidth" 622 | version = "0.2.5" 623 | description = "Measures the displayed width of unicode strings in a terminal" 624 | category = "dev" 625 | optional = false 626 | python-versions = "*" 627 | 628 | [[package]] 629 | name = "werkzeug" 630 | version = "0.16.1" 631 | description = "The comprehensive WSGI web application library." 632 | category = "dev" 633 | optional = false 634 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 635 | 636 | [package.extras] 637 | dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] 638 | termcolor = ["termcolor"] 639 | watchdog = ["watchdog"] 640 | 641 | [[package]] 642 | name = "wrapt" 643 | version = "1.12.1" 644 | description = "Module for decorators, wrappers and monkey patching." 645 | category = "dev" 646 | optional = false 647 | python-versions = "*" 648 | 649 | [[package]] 650 | name = "zipp" 651 | version = "3.4.1" 652 | description = "Backport of pathlib-compatible object wrapper for zip files" 653 | category = "main" 654 | optional = false 655 | python-versions = ">=3.6" 656 | 657 | [package.extras] 658 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 659 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 660 | 661 | [metadata] 662 | lock-version = "1.1" 663 | python-versions = "^3.6" 664 | content-hash = "4295d81f51991bfd6d11de5c831da71381d15263dbfd8f8ca847e3d047bdaa98" 665 | 666 | [metadata.files] 667 | amqp = [ 668 | {file = "amqp-2.6.1-py2.py3-none-any.whl", hash = "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"}, 669 | {file = "amqp-2.6.1.tar.gz", hash = "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21"}, 670 | ] 671 | appdirs = [ 672 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 673 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 674 | ] 675 | asgiref = [ 676 | {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, 677 | {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, 678 | ] 679 | astroid = [ 680 | {file = "astroid-2.5.3-py3-none-any.whl", hash = "sha256:bea3f32799fbb8581f58431c12591bc20ce11cbc90ad82e2ea5717d94f2080d5"}, 681 | {file = "astroid-2.5.3.tar.gz", hash = "sha256:ad63b8552c70939568966811a088ef0bc880f99a24a00834abd0e3681b514f91"}, 682 | ] 683 | atomicwrites = [ 684 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 685 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 686 | ] 687 | attrs = [ 688 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 689 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 690 | ] 691 | billiard = [ 692 | {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, 693 | {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, 694 | ] 695 | black = [ 696 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 697 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 698 | ] 699 | celery = [ 700 | {file = "celery-4.4.7-py2.py3-none-any.whl", hash = "sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45"}, 701 | {file = "celery-4.4.7.tar.gz", hash = "sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f"}, 702 | ] 703 | certifi = [ 704 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 705 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 706 | ] 707 | click = [ 708 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 709 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 710 | ] 711 | colorama = [ 712 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 713 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 714 | ] 715 | django = [ 716 | {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"}, 717 | {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"}, 718 | ] 719 | django-debug-toolbar = [ 720 | {file = "django-debug-toolbar-2.2.1.tar.gz", hash = "sha256:7aadab5240796ffe8e93cc7dfbe2f87a204054746ff7ff93cd6d4a0c3747c853"}, 721 | {file = "django_debug_toolbar-2.2.1-py3-none-any.whl", hash = "sha256:7feaee934608f5cdd95432154be832fe30fda6c1249018191e2c27bc0b6a965e"}, 722 | ] 723 | django-environ = [ 724 | {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, 725 | {file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"}, 726 | ] 727 | django-extensions = [ 728 | {file = "django-extensions-2.2.9.tar.gz", hash = "sha256:2f81b618ba4d1b0e58603e25012e5c74f88a4b706e0022a3b21f24f0322a6ce6"}, 729 | {file = "django_extensions-2.2.9-py2.py3-none-any.whl", hash = "sha256:b19182d101a441fe001c5753553a901e2ef3ff60e8fbbe38881eb4a61fdd17c4"}, 730 | ] 731 | django-storages = [ 732 | {file = "django-storages-1.11.1.tar.gz", hash = "sha256:c823dbf56c9e35b0999a13d7e05062b837bae36c518a40255d522fbe3750fbb4"}, 733 | {file = "django_storages-1.11.1-py3-none-any.whl", hash = "sha256:f28765826d507a0309cfaa849bd084894bc71d81bf0d09479168d44785396f80"}, 734 | ] 735 | django-webpack-loader = [ 736 | {file = "django-webpack-loader-0.6.0.tar.gz", hash = "sha256:60bab6b9a037a5346fad12d2a70a6bc046afb33154cf75ed640b93d3ebd5f520"}, 737 | {file = "django_webpack_loader-0.6.0-py2.py3-none-any.whl", hash = "sha256:970b968c2a8975fb7eff56a3bab5d0d90d396740852d1e0c50c5cfe2b824199a"}, 738 | ] 739 | flake8 = [ 740 | {file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"}, 741 | {file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"}, 742 | ] 743 | importlib-metadata = [ 744 | {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"}, 745 | {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"}, 746 | ] 747 | isort = [ 748 | {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, 749 | {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, 750 | ] 751 | kombu = [ 752 | {file = "kombu-4.6.11-py2.py3-none-any.whl", hash = "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a"}, 753 | {file = "kombu-4.6.11.tar.gz", hash = "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"}, 754 | ] 755 | lazy-object-proxy = [ 756 | {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, 757 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, 758 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, 759 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, 760 | {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, 761 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, 762 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, 763 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, 764 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, 765 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, 766 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, 767 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, 768 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, 769 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, 770 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, 771 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, 772 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, 773 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, 774 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, 775 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, 776 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, 777 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, 778 | ] 779 | mccabe = [ 780 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 781 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 782 | ] 783 | more-itertools = [ 784 | {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, 785 | {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, 786 | ] 787 | packaging = [ 788 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 789 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 790 | ] 791 | pathspec = [ 792 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 793 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 794 | ] 795 | pluggy = [ 796 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 797 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 798 | ] 799 | psycopg2-binary = [ 800 | {file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"}, 801 | {file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"}, 802 | {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"}, 803 | {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"}, 804 | {file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"}, 805 | {file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"}, 806 | {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"}, 807 | {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"}, 808 | {file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"}, 809 | {file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"}, 810 | {file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"}, 811 | {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"}, 812 | {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"}, 813 | {file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"}, 814 | {file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"}, 815 | {file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"}, 816 | {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"}, 817 | {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"}, 818 | {file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"}, 819 | {file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"}, 820 | {file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"}, 821 | {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"}, 822 | {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"}, 823 | {file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"}, 824 | {file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"}, 825 | {file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"}, 826 | {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"}, 827 | {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"}, 828 | {file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"}, 829 | {file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"}, 830 | {file = "psycopg2_binary-2.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83"}, 831 | {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"}, 832 | {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"}, 833 | {file = "psycopg2_binary-2.8.6-cp39-cp39-win32.whl", hash = "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056"}, 834 | {file = "psycopg2_binary-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6"}, 835 | ] 836 | py = [ 837 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 838 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 839 | ] 840 | pycodestyle = [ 841 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 842 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 843 | ] 844 | pyflakes = [ 845 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 846 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 847 | ] 848 | pylint = [ 849 | {file = "pylint-2.7.4-py3-none-any.whl", hash = "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a"}, 850 | {file = "pylint-2.7.4.tar.gz", hash = "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"}, 851 | ] 852 | pylint-django = [ 853 | {file = "pylint-django-2.4.3.tar.gz", hash = "sha256:a5a4515209a6237d1d390a4a307d53f53baaf4f058ecf4bb556c775d208f6b0d"}, 854 | {file = "pylint_django-2.4.3-py3-none-any.whl", hash = "sha256:dc5ed27bb7662d73444ccd15a0b3964ed6ced6cc2712b85db616102062d2ec35"}, 855 | ] 856 | pylint-plugin-utils = [ 857 | {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, 858 | {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, 859 | ] 860 | pyparsing = [ 861 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 862 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 863 | ] 864 | pytest = [ 865 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 866 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 867 | ] 868 | pytest-django = [ 869 | {file = "pytest-django-3.10.0.tar.gz", hash = "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6"}, 870 | {file = "pytest_django-3.10.0-py2.py3-none-any.whl", hash = "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4"}, 871 | ] 872 | pytest-env = [ 873 | {file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"}, 874 | ] 875 | pytz = [ 876 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 877 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 878 | ] 879 | redis = [ 880 | {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, 881 | {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, 882 | ] 883 | regex = [ 884 | {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, 885 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, 886 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, 887 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, 888 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, 889 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, 890 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, 891 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, 892 | {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, 893 | {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, 894 | {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, 895 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, 896 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, 897 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, 898 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, 899 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, 900 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, 901 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, 902 | {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, 903 | {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, 904 | {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, 905 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, 906 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, 907 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, 908 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, 909 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, 910 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, 911 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, 912 | {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, 913 | {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, 914 | {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, 915 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, 916 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, 917 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, 918 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, 919 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, 920 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, 921 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, 922 | {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, 923 | {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, 924 | {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, 925 | ] 926 | sentry-sdk = [ 927 | {file = "sentry-sdk-0.14.4.tar.gz", hash = "sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c"}, 928 | {file = "sentry_sdk-0.14.4-py2.py3-none-any.whl", hash = "sha256:799a8bf76b012e3030a881be00e97bc0b922ce35dde699c6537122b751d80e2c"}, 929 | ] 930 | six = [ 931 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 932 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 933 | ] 934 | sqlparse = [ 935 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 936 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 937 | ] 938 | toml = [ 939 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 940 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 941 | ] 942 | typed-ast = [ 943 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 944 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 945 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 946 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 947 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 948 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 949 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 950 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 951 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 952 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 953 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 954 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 955 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 956 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 957 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 958 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 959 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 960 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 961 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 962 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 963 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 964 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 965 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 966 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 967 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 968 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 969 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 970 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 971 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 972 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 973 | ] 974 | typing-extensions = [ 975 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 976 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 977 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 978 | ] 979 | urllib3 = [ 980 | {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, 981 | {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, 982 | ] 983 | vine = [ 984 | {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"}, 985 | {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, 986 | ] 987 | wcwidth = [ 988 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 989 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 990 | ] 991 | werkzeug = [ 992 | {file = "Werkzeug-0.16.1-py2.py3-none-any.whl", hash = "sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2"}, 993 | {file = "Werkzeug-0.16.1.tar.gz", hash = "sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04"}, 994 | ] 995 | wrapt = [ 996 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 997 | ] 998 | zipp = [ 999 | {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, 1000 | {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, 1001 | ] 1002 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = ["Francisco Ceruti <hello@fceruti.com>"] 3 | description = "Opinionated boilerplate for Django projects." 4 | license = "MIT License" 5 | name = "django-starter-project" 6 | packages = [ 7 | {include = "apps/**/*.py"}, 8 | {include = "conf/**/*.py"}, 9 | ] 10 | version = "0.2.0" 11 | 12 | [tool.poetry.scripts] 13 | makemigrations = "scripts:makemigrations" 14 | migrate = "scripts:migrate" 15 | server = "scripts:server" 16 | shell = "scripts:shell" 17 | tests = "scripts:tests" 18 | worker = "scripts:worker" 19 | 20 | [tool.poetry.dependencies] 21 | celery = "^4.4.0" 22 | django = ">=2.2.13" 23 | django-environ = "^0.4.5" 24 | django-extensions = "^2.2.6" 25 | django-storages = "^1.8" 26 | django-webpack-loader = "^0.6.0" 27 | psycopg2-binary = "^2.8.4" 28 | python = "^3.6" 29 | redis = "^3.3.11" 30 | sentry-sdk = "^0.14.1" 31 | 32 | [tool.poetry.dev-dependencies] 33 | Werkzeug = "^0.16.0" 34 | black = "^19.10b0" 35 | django-debug-toolbar = "^2.1" 36 | flake8 = "^3.8.1" 37 | pylint = "^2.5.0" 38 | pylint-django = "^2.0.15" 39 | pytest = "^5.3.4" 40 | pytest-django = "^3.8.0" 41 | pytest-env = "^0.6.2" 42 | 43 | [build-system] 44 | build-backend = "poetry.masonry.api" 45 | requires = ["poetry>=0.12"] 46 | -------------------------------------------------------------------------------- /scripts.py: -------------------------------------------------------------------------------- 1 | # This is a temporary workaround till Poetry supports scripts, see 2 | # https://github.com/sdispater/poetry/issues/241. 3 | from subprocess import check_call 4 | 5 | 6 | def server(*args) -> None: 7 | check_call(["python", "manage.py", "runserver_plus", "0.0.0.0:8000"]) 8 | 9 | 10 | def tests() -> None: 11 | check_call(["pytest", "tests/"]) 12 | 13 | 14 | def worker() -> None: 15 | check_call(["python", "manage.py", "celery_autoreload"]) 16 | 17 | 18 | def migrate() -> None: 19 | check_call(["python", "manage.py", "migrate"]) 20 | 21 | 22 | def makemigrations() -> None: 23 | check_call(["python", "manage.py", "makemigrations"]) 24 | 25 | 26 | def shell() -> None: 27 | check_call(["python", "manage.py", "shell_plus"]) 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | addopts = -p no:warnings 4 | env = 5 | DJANGO_SETTINGS_MODULE=conf.settings 6 | DJANGO_ENV=test 7 | CELERY_TASK_ALWAYS_EAGER=on 8 | EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend 9 | 10 | [flake8] 11 | exclude = 12 | assets, 13 | logs, 14 | media, 15 | templates, 16 | */migrations/*.py, 17 | 18 | max-line-length = 120 19 | ignore = 20 | C0103, # Variable name doesnt conform to snake_case naming style 21 | C0114, # missing-module-docstring 22 | C0115, # missing-class-docstring 23 | C0116, # missing-function-docstring 24 | C0330, # Wrong hanging indentation 25 | E1101, # Instance has not member 26 | E1136, # Value is unsubscriptable 27 | W0201, # attribute-defined-outside-init 28 | W0614, # unused-wildcard-import (W0614) 29 | W0703, # broad-except 30 | W503, # line break after binary operator 31 | W504, # line break after binary operator 32 | 33 | [pylint] 34 | load-plugins = pylint_django 35 | max-line-length = 120 36 | ignore= 37 | assets, 38 | logs, 39 | media, 40 | templates, 41 | */migrations/*.py, 42 | disable= 43 | C0103, # Variable name doesnt conform to snake_case naming style 44 | C0114, # missing-module-docstring 45 | C0115, # missing-class-docstring 46 | C0116, # missing-function-docstring 47 | C0330, # Wrong hanging indentation 48 | E1101, # Instance has not member 49 | E1136, # Value is unsubscriptable 50 | R0201, # no-self-use 51 | R0903, # too-few-public-methods 52 | R1720, # no-else-raise 53 | W0201, # attribute-defined-outside-init 54 | W0511, # fixme 55 | W0613, # unused-argument 56 | W0614, # unused-wildcard-import (W0614) 57 | W0703, # broad-except 58 | W503, # line break before binary operator 59 | W504, # line break after binary operator 60 | R1705, # no-else-return 61 | 62 | enable = 63 | W0611 # unused-import (W0611) 64 | 65 | [isort] 66 | skip = 67 | static 68 | assets 69 | logs 70 | media 71 | tests 72 | node_modules 73 | templates 74 | migrations 75 | node_modules 76 | not_skip = __init__.py 77 | multi_line_output = 4 78 | -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "error_base.html" %} 2 | 3 | {% block page_title %}Error 403{% endblock page_title %} 4 | 5 | {% block error_body %} 6 | <h1>Error 403</h1> 7 | {% endblock error_body %} 8 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "error_base.html" %} 2 | 3 | {% block page_title %}Error 404{% endblock page_title %} 4 | 5 | {% block error_body %} 6 | <h1>Error 404</h1> 7 | {% endblock error_body %} 8 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "error_base.html" %} 2 | 3 | {% block page_title %}Error 500{% endblock page_title %} 4 | 5 | {% block error_body %} 6 | <h1>Error 500</h1> 7 | {% endblock error_body %} 8 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load render_bundle from webpack_loader %} 2 | {% load static %} 3 | <!doctype html> 4 | <html lang="{{ LANGUAGE }}"> 5 | <head> 6 | <meta charset="utf-8"> 7 | <title>{% block page_title %}{% endblock %} 8 | 9 | 10 | 11 | {% block page_extra_meta %}{% endblock %} 12 | {% render_bundle 'main' 'css' %} 13 | 14 | 15 | {% block body %}{% endblock %} 16 | {% render_bundle 'main' 'js' %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/error_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 | {% block error_body %} {% endblock %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/registration/activate.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Account activation failed' %}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/registration/activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Your account is now activated.' %}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/registration/activation_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | {{ site.name }} {% trans "registration" %} 7 | 8 | 9 | 10 |

11 | {% blocktrans with site_name=site.name %} 12 | You (or someone pretending to be you) have asked to register an account at 13 | {{ site_name }}. If this wasn't you, please ignore this email 14 | and your address will be removed from our records. 15 | {% endblocktrans %} 16 |

17 |

18 | {% blocktrans %} 19 | To activate this account, please click the following link within the next 20 | {{ expiration_days }} days: 21 | {% endblocktrans %} 22 |

23 | 24 |

25 | 26 | {{site.domain}}{% url 'registration_activate' activation_key %} 27 | 28 |

29 |

30 | {% blocktrans with site_name=site.name %} 31 | Sincerely, 32 | {{ site_name }} Management 33 | {% endblocktrans %} 34 |

35 | 36 | 37 | 38 | 39 | 40 | {% comment %} 41 | **registration/activation_email.html** 42 | 43 | Used to generate the html body of the activation email. Should display a 44 | link the user can click to activate the account. This template has the 45 | following context: 46 | 47 | ``activation_key`` 48 | The activation key for the new account. 49 | 50 | ``expiration_days`` 51 | The number of days remaining during which the account may be 52 | activated. 53 | 54 | ``site`` 55 | An object representing the site on which the user registered; 56 | depending on whether ``django.contrib.sites`` is installed, this 57 | may be an instance of either ``django.contrib.sites.models.Site`` 58 | (if the sites application is installed) or 59 | ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the 60 | documentation for the Django sites framework 61 | `_ for 62 | details regarding these objects' interfaces. 63 | 64 | ``user`` 65 | The new user account 66 | 67 | ``request`` 68 | ``HttpRequest`` instance for better flexibility. 69 | For example it can be used to compute absolute register URL: 70 | 71 | {{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %} 72 | {% endcomment %} 73 | -------------------------------------------------------------------------------- /templates/registration/activation_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% trans 'Activate account at' %} {{ site.name }}: 3 | 4 | http://{{ site.domain }}{% url 'registration_activate' activation_key %} 5 | 6 | {% blocktrans %}Link is valid for {{ expiration_days }} days.{% endblocktrans %} 7 | -------------------------------------------------------------------------------- /templates/registration/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% trans "Account activation on" %} {{ site.name }} 2 | -------------------------------------------------------------------------------- /templates/registration/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |

Hello

5 | {% block content %} 6 | {% endblock content %} 7 | {% endblock body %} 8 | -------------------------------------------------------------------------------- /templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Logged out' %}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} {% load i18n %} {% block page_title %}{% 2 | trans 'Log in' %}{% endblock %} {% block content %} 3 |
4 | {% csrf_token %} {{ form.as_p }} 5 | 6 | 7 | 8 |
9 | 10 |

11 | {% trans "Forgot password" %}? 12 | {% trans 'Reset it' %}! 13 |

14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block page_title %}{% trans "Logged out" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Successfully logged out" %}.

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Password changed' %}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Password reset successfully' %}

6 |

{% trans 'Log in' %}

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 | {% if validlink %} 6 |
7 | {% csrf_token %} 8 | {{ form.as_p }} 9 | 10 | 11 |
12 | {% else %} 13 |

{% trans 'Password reset failed' %}

14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Email with password reset instructions has been sent.' %}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Reset password at {{ site_name }}{% endblocktrans %}: 3 | {% block reset_link %} 4 | {{ protocol }}://{{ domain }}{% url 'auth_password_reset_confirm' uid token %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /templates/registration/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'registration/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /templates/registration/resend_activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Account Activation Resent" %}{% endblock %} 5 | 6 | {% block content %} 7 |

8 | {% blocktrans %} 9 | We have sent an email to {{ email }} with further instructions. 10 | {% endblocktrans %} 11 |

12 | {% endblock %} 13 | 14 | 15 | {% comment %} 16 | **registration/resend_activation_complete.html** 17 | Used after form for resending account activation is submitted. By default has 18 | the following context: 19 | 20 | ``email`` 21 | The email address submitted in the resend activation form. 22 | {% endcomment %} 23 | -------------------------------------------------------------------------------- /templates/registration/resend_activation_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Resend Activation Email" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | {% csrf_token %} 9 | {{ form.as_p }} 10 | 11 |
12 | {% endblock %} 13 | 14 | 15 | {% comment %} 16 | **registration/resend_activation_form.html** 17 | Used to show the form users will fill out to resend the activation email. By 18 | default, has the following context: 19 | 20 | ``form`` 21 | The registration form. This will be an instance of some subclass 22 | of ``django.forms.Form``; consult `Django's forms documentation 23 | `_ for 24 | information on how to display this in a template. 25 | {% endcomment %} 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/tests/__init__.py -------------------------------------------------------------------------------- /tests/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fceruti/django-starter-project/8abbc146541101a690de387c75b70f09959620ef/tests/misc/__init__.py -------------------------------------------------------------------------------- /tests/misc/test_choices.py: -------------------------------------------------------------------------------- 1 | from apps.misc.choices import Choices 2 | 3 | 4 | def test_choices(): 5 | class CarType(Choices): 6 | SUV = "SUV" 7 | SEDAN = ("SD", "Sedan") 8 | HATCHBACK = ("HB", "Hatchback") 9 | CONVERTIBLE = ("CV", "Convertible") 10 | COUPE = ("CP", "Coupe") 11 | 12 | assert set(CarType.keys()) == set(["SUV", "SD", "HB", "CV", "CP"]) 13 | 14 | assert set(CarType.choices()) == set( 15 | [ 16 | ("SUV", "SUV"), 17 | ("SD", "Sedan"), 18 | ("HB", "Hatchback"), 19 | ("CV", "Convertible"), 20 | ("CP", "Coupe"), 21 | ] 22 | ) 23 | 24 | 25 | def test_choices_accessor(): 26 | class CarType(Choices): 27 | SUV = "SUV" 28 | SEDAN = ("SD", "Sedan") 29 | HATCHBACK = ("HB", "Hatchback") 30 | CONVERTIBLE = ("CV", "Convertible") 31 | COUPE = ("CP", "Coupe") 32 | 33 | assert CarType.SUV == "SUV" 34 | assert CarType.SEDAN == "SD" 35 | -------------------------------------------------------------------------------- /tests/test_int.py: -------------------------------------------------------------------------------- 1 | def test_parse_int(): 2 | assert int("1") == 1 3 | --------------------------------------------------------------------------------