mirror of
https://github.com/marking-hack/backend.git
synced 2024-11-28 04:13:42 +03:00
initial commit
This commit is contained in:
commit
137caccebf
6
.env.template
Normal file
6
.env.template
Normal file
|
@ -0,0 +1,6 @@
|
|||
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/marking_hack
|
||||
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||
REDIS_URL=redis://localhost:6379/1
|
||||
REDIS_CACHE=rediscache://localhost:6379/1
|
||||
USE_DOCKER=no
|
||||
EMAIL_HOST=127.0.0.1:8025
|
335
.gitignore
vendored
Normal file
335
.gitignore
vendored
Normal file
|
@ -0,0 +1,335 @@
|
|||
.idea
|
||||
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
staticfiles/
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# Environments
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
|
||||
### Linux template
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
|
||||
### VisualStudioCode template
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
|
||||
# Provided default Pycharm Run/Debug Configurations should be tracked by git
|
||||
# In case of local modifications made by Pycharm, use update-index command
|
||||
# for each changed file, like this:
|
||||
# git update-index --assume-unchanged .idea/akarpov.iml
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
|
||||
|
||||
### Windows template
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
|
||||
### macOS template
|
||||
# General
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### SublimeText template
|
||||
# Cache files for Sublime Text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# Workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# Project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using Sublime Text
|
||||
# *.sublime-project
|
||||
|
||||
# SFTP configuration file
|
||||
sftp-config.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
Package Control.merged-ca-bundle
|
||||
Package Control.user-ca-bundle
|
||||
oscrypto-ca-bundle.crt
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
|
||||
### Vim template
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-v][a-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
|
||||
### Project template
|
||||
akarpov/media/
|
||||
|
||||
.pytest_cache/
|
||||
.ipython/
|
||||
.env
|
||||
.envs/*
|
||||
!.envs/.local/
|
57
README.md
Normal file
57
README.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Marking hack
|
||||
|
||||
|
||||
## Basic Commands
|
||||
|
||||
### Setting Up Your Users
|
||||
|
||||
- To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go.
|
||||
|
||||
- To create a **superuser account**, use this command:
|
||||
|
||||
$ python manage.py createsuperuser
|
||||
|
||||
For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.
|
||||
|
||||
### Runserver
|
||||
|
||||
$ ./manage.py runserver_plus
|
||||
|
||||
### Type checks
|
||||
|
||||
Running type checks with mypy:
|
||||
|
||||
$ mypy marking_hack
|
||||
|
||||
### Test coverage
|
||||
|
||||
To run the tests, check your test coverage, and generate an HTML coverage report:
|
||||
|
||||
$ coverage run -m pytest
|
||||
$ coverage html
|
||||
$ open htmlcov/index.html
|
||||
|
||||
#### Running tests with pytest
|
||||
|
||||
$ pytest
|
||||
|
||||
### Live reloading and Sass CSS compilation
|
||||
|
||||
Moved to [Live reloading and SASS compilation](https://cookiecutter-django.readthedocs.io/en/latest/developing-locally.html#sass-compilation-live-reloading).
|
||||
|
||||
### Celery
|
||||
|
||||
This app comes with Celery.
|
||||
|
||||
To run a celery worker:
|
||||
|
||||
``` bash
|
||||
cd marking_hack
|
||||
celery -A config.celery_app worker -l info
|
||||
```
|
||||
|
||||
Please note: For Celery's import magic to work, it is important *where* the celery commands are run. If you are in the same folder with *manage.py*, you should be right.
|
||||
|
||||
## Deployment
|
||||
|
||||
The following details how to deploy this application.
|
75
compose/local/django/Dockerfile
Normal file
75
compose/local/django/Dockerfile
Normal file
|
@ -0,0 +1,75 @@
|
|||
ARG PYTHON_VERSION=3.11-slim
|
||||
|
||||
# define an alias for the specfic python version used in this file.
|
||||
FROM python:${PYTHON_VERSION} as python
|
||||
|
||||
# Python build stage
|
||||
FROM python as python-build-stage
|
||||
|
||||
ARG BUILD_ENVIRONMENT=local
|
||||
|
||||
# Install apt packages
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
# dependencies for building Python packages
|
||||
build-essential \
|
||||
# psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
|
||||
# Python 'run' stage
|
||||
FROM python as python-run-stage
|
||||
|
||||
ARG BUILD_ENVIRONMENT=local
|
||||
ARG APP_HOME=/app
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
|
||||
# Install required system dependencies
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
# psycopg2 dependencies
|
||||
libpq-dev \
|
||||
# Translations dependencies
|
||||
gettext \
|
||||
# cleaning up unused files
|
||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
# Configuring poetry
|
||||
RUN poetry config virtualenvs.create false
|
||||
COPY pyproject.toml poetry.lock /
|
||||
|
||||
# Installing requirements
|
||||
RUN poetry install
|
||||
|
||||
|
||||
COPY ./compose/production/django/entrypoint /entrypoint
|
||||
RUN sed -i 's/\r$//g' /entrypoint
|
||||
RUN chmod +x /entrypoint
|
||||
|
||||
COPY ./compose/local/django/start /start
|
||||
RUN sed -i 's/\r$//g' /start
|
||||
RUN chmod +x /start
|
||||
|
||||
|
||||
COPY ./compose/local/django/celery/worker/start /start-celeryworker
|
||||
RUN sed -i 's/\r$//g' /start-celeryworker
|
||||
RUN chmod +x /start-celeryworker
|
||||
|
||||
COPY ./compose/local/django/celery/beat/start /start-celerybeat
|
||||
RUN sed -i 's/\r$//g' /start-celerybeat
|
||||
RUN chmod +x /start-celerybeat
|
||||
|
||||
COPY ./compose/local/django/celery/flower/start /start-flower
|
||||
RUN sed -i 's/\r$//g' /start-flower
|
||||
RUN chmod +x /start-flower
|
||||
|
||||
# copy application code to WORKDIR
|
||||
COPY . ${APP_HOME}
|
||||
|
||||
ENTRYPOINT ["/entrypoint"]
|
8
compose/local/django/celery/beat/start
Normal file
8
compose/local/django/celery/beat/start
Normal file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
|
||||
|
||||
rm -f './celerybeat.pid'
|
||||
exec watchfiles celery.__main__.main --args '-A config.celery_app beat -l INFO'
|
8
compose/local/django/celery/flower/start
Normal file
8
compose/local/django/celery/flower/start
Normal file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
|
||||
exec watchfiles celery.__main__.main \
|
||||
--args \
|
||||
"-A config.celery_app -b \"${CELERY_BROKER_URL}\" flower --basic_auth=\"${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}\""
|
7
compose/local/django/celery/worker/start
Normal file
7
compose/local/django/celery/worker/start
Normal file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
|
||||
|
||||
exec watchfiles celery.__main__.main --args '-A config.celery_app worker -l INFO'
|
9
compose/local/django/start
Normal file
9
compose/local/django/start
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
python manage.py migrate
|
||||
exec python manage.py runserver_plus 0.0.0.0:8000
|
81
compose/production/django/Dockerfile
Normal file
81
compose/production/django/Dockerfile
Normal file
|
@ -0,0 +1,81 @@
|
|||
ARG PYTHON_VERSION=3.11-slim
|
||||
|
||||
# define an alias for the specfic python version used in this file.
|
||||
FROM python:${PYTHON_VERSION} as python
|
||||
|
||||
# Python build stage
|
||||
FROM python as python-build-stage
|
||||
|
||||
ARG BUILD_ENVIRONMENT=production
|
||||
|
||||
# Install apt packages
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
# dependencies for building Python packages
|
||||
build-essential \
|
||||
# psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
|
||||
# Python 'run' stage
|
||||
FROM python as python-run-stage
|
||||
|
||||
ARG BUILD_ENVIRONMENT=production
|
||||
ARG APP_HOME=/app
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
|
||||
# Install required system dependencies
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
# psycopg2 dependencies
|
||||
libpq-dev \
|
||||
# Translations dependencies
|
||||
gettext \
|
||||
# cleaning up unused files
|
||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN addgroup --system django \
|
||||
&& adduser --system --ingroup django django
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
# Configuring poetry
|
||||
RUN poetry config virtualenvs.create false
|
||||
COPY pyproject.toml poetry.lock /
|
||||
|
||||
# Installing requirements
|
||||
RUN poetry install
|
||||
|
||||
|
||||
COPY ./compose/production/django/entrypoint /entrypoint
|
||||
RUN sed -i 's/\r$//g' /entrypoint
|
||||
RUN chmod +x /entrypoint
|
||||
|
||||
COPY ./compose/local/django/start /start
|
||||
RUN sed -i 's/\r$//g' /start
|
||||
RUN chmod +x /start
|
||||
|
||||
|
||||
COPY ./compose/local/django/celery/worker/start /start-celeryworker
|
||||
RUN sed -i 's/\r$//g' /start-celeryworker
|
||||
RUN chmod +x /start-celeryworker
|
||||
|
||||
COPY ./compose/local/django/celery/beat/start /start-celerybeat
|
||||
RUN sed -i 's/\r$//g' /start-celerybeat
|
||||
RUN chmod +x /start-celerybeat
|
||||
|
||||
COPY ./compose/local/django/celery/flower/start /start-flower
|
||||
RUN sed -i 's/\r$//g' /start-flower
|
||||
RUN chmod +x /start-flower
|
||||
|
||||
# copy application code to WORKDIR
|
||||
COPY --chown=django:django . ${APP_HOME}
|
||||
|
||||
# make django owner of the WORKDIR directory as well.
|
||||
RUN chown django:django ${APP_HOME}
|
||||
|
||||
USER django
|
8
compose/production/django/celery/beat/start
Normal file
8
compose/production/django/celery/beat/start
Normal file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
exec celery -A config.celery_app beat -l INFO
|
11
compose/production/django/celery/flower/start
Normal file
11
compose/production/django/celery/flower/start
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
|
||||
|
||||
exec celery \
|
||||
-A config.celery_app \
|
||||
-b "${CELERY_BROKER_URL}" \
|
||||
flower \
|
||||
--basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"
|
8
compose/production/django/celery/worker/start
Normal file
8
compose/production/django/celery/worker/start
Normal file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
exec celery -A config.celery_app worker -l INFO
|
49
compose/production/django/entrypoint
Normal file
49
compose/production/django/entrypoint
Normal file
|
@ -0,0 +1,49 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
|
||||
# N.B. If only .env files supported variable expansion...
|
||||
export CELERY_BROKER_URL="${REDIS_URL}"
|
||||
|
||||
|
||||
if [ -z "${POSTGRES_USER}" ]; then
|
||||
base_postgres_image_default_user='postgres'
|
||||
export POSTGRES_USER="${base_postgres_image_default_user}"
|
||||
fi
|
||||
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
||||
|
||||
python << END
|
||||
import sys
|
||||
import time
|
||||
|
||||
import psycopg2
|
||||
|
||||
suggest_unrecoverable_after = 30
|
||||
start = time.time()
|
||||
|
||||
while True:
|
||||
try:
|
||||
psycopg2.connect(
|
||||
dbname="${POSTGRES_DB}",
|
||||
user="${POSTGRES_USER}",
|
||||
password="${POSTGRES_PASSWORD}",
|
||||
host="${POSTGRES_HOST}",
|
||||
port="${POSTGRES_PORT}",
|
||||
)
|
||||
break
|
||||
except psycopg2.OperationalError as error:
|
||||
sys.stderr.write("Waiting for PostgreSQL to become available...\n")
|
||||
|
||||
if time.time() - start > suggest_unrecoverable_after:
|
||||
sys.stderr.write(" This is taking longer than expected. The following exception may be indicative of an unrecoverable error: '{}'\n".format(error))
|
||||
|
||||
time.sleep(1)
|
||||
END
|
||||
|
||||
>&2 echo 'PostgreSQL is available'
|
||||
|
||||
exec "$@"
|
9
compose/production/django/start
Normal file
9
compose/production/django/start
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
python /app/manage.py collectstatic --noinput
|
||||
exec /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app
|
6
compose/production/postgres/Dockerfile
Normal file
6
compose/production/postgres/Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
|||
FROM postgres:14
|
||||
|
||||
COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance
|
||||
RUN chmod +x /usr/local/bin/maintenance/*
|
||||
RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
|
||||
&& rmdir /usr/local/bin/maintenance
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
BACKUP_DIR_PATH='/backups'
|
||||
BACKUP_FILE_PREFIX='backup'
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
countdown() {
|
||||
declare desc="A simple countdown. Source: https://superuser.com/a/611582"
|
||||
local seconds="${1}"
|
||||
local d=$(($(date +%s) + "${seconds}"))
|
||||
while [ "$d" -ge `date +%s` ]; do
|
||||
echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r";
|
||||
sleep 0.1
|
||||
done
|
||||
}
|
41
compose/production/postgres/maintenance/_sourced/messages.sh
Normal file
41
compose/production/postgres/maintenance/_sourced/messages.sh
Normal file
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
message_newline() {
|
||||
echo
|
||||
}
|
||||
|
||||
message_debug()
|
||||
{
|
||||
echo -e "DEBUG: ${@}"
|
||||
}
|
||||
|
||||
message_welcome()
|
||||
{
|
||||
echo -e "\e[1m${@}\e[0m"
|
||||
}
|
||||
|
||||
message_warning()
|
||||
{
|
||||
echo -e "\e[33mWARNING\e[0m: ${@}"
|
||||
}
|
||||
|
||||
message_error()
|
||||
{
|
||||
echo -e "\e[31mERROR\e[0m: ${@}"
|
||||
}
|
||||
|
||||
message_info()
|
||||
{
|
||||
echo -e "\e[37mINFO\e[0m: ${@}"
|
||||
}
|
||||
|
||||
message_suggestion()
|
||||
{
|
||||
echo -e "\e[33mSUGGESTION\e[0m: ${@}"
|
||||
}
|
||||
|
||||
message_success()
|
||||
{
|
||||
echo -e "\e[32mSUCCESS\e[0m: ${@}"
|
||||
}
|
16
compose/production/postgres/maintenance/_sourced/yes_no.sh
Normal file
16
compose/production/postgres/maintenance/_sourced/yes_no.sh
Normal file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
yes_no() {
|
||||
declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message."
|
||||
local arg1="${1}"
|
||||
|
||||
local response=
|
||||
read -r -p "${arg1} (y/[n])? " response
|
||||
if [[ "${response}" =~ ^[Yy]$ ]]
|
||||
then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
38
compose/production/postgres/maintenance/backup
Normal file
38
compose/production/postgres/maintenance/backup
Normal file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
### Create a database backup.
|
||||
###
|
||||
### Usage:
|
||||
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backup
|
||||
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
working_dir="$(dirname ${0})"
|
||||
source "${working_dir}/_sourced/constants.sh"
|
||||
source "${working_dir}/_sourced/messages.sh"
|
||||
|
||||
|
||||
message_welcome "Backing up the '${POSTGRES_DB}' database..."
|
||||
|
||||
|
||||
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
|
||||
message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PGHOST="${POSTGRES_HOST}"
|
||||
export PGPORT="${POSTGRES_PORT}"
|
||||
export PGUSER="${POSTGRES_USER}"
|
||||
export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
export PGDATABASE="${POSTGRES_DB}"
|
||||
|
||||
backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz"
|
||||
pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}"
|
||||
|
||||
|
||||
message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'."
|
22
compose/production/postgres/maintenance/backups
Normal file
22
compose/production/postgres/maintenance/backups
Normal file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
### View backups.
|
||||
###
|
||||
### Usage:
|
||||
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backups
|
||||
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
working_dir="$(dirname ${0})"
|
||||
source "${working_dir}/_sourced/constants.sh"
|
||||
source "${working_dir}/_sourced/messages.sh"
|
||||
|
||||
|
||||
message_welcome "These are the backups you have got:"
|
||||
|
||||
ls -lht "${BACKUP_DIR_PATH}"
|
55
compose/production/postgres/maintenance/restore
Normal file
55
compose/production/postgres/maintenance/restore
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
### Restore database from a backup.
|
||||
###
|
||||
### Parameters:
|
||||
### <1> filename of an existing backup.
|
||||
###
|
||||
### Usage:
|
||||
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres restore <1>
|
||||
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
||||
working_dir="$(dirname ${0})"
|
||||
source "${working_dir}/_sourced/constants.sh"
|
||||
source "${working_dir}/_sourced/messages.sh"
|
||||
|
||||
|
||||
if [[ -z ${1+x} ]]; then
|
||||
message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
|
||||
exit 1
|
||||
fi
|
||||
backup_filename="${BACKUP_DIR_PATH}/${1}"
|
||||
if [[ ! -f "${backup_filename}" ]]; then
|
||||
message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."
|
||||
|
||||
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
|
||||
message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PGHOST="${POSTGRES_HOST}"
|
||||
export PGPORT="${POSTGRES_PORT}"
|
||||
export PGUSER="${POSTGRES_USER}"
|
||||
export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
export PGDATABASE="${POSTGRES_DB}"
|
||||
|
||||
message_info "Dropping the database..."
|
||||
dropdb "${PGDATABASE}"
|
||||
|
||||
message_info "Creating a new database..."
|
||||
createdb --owner="${POSTGRES_USER}"
|
||||
|
||||
message_info "Applying the backup to the new database..."
|
||||
gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"
|
||||
|
||||
message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."
|
5
compose/production/traefik/Dockerfile
Normal file
5
compose/production/traefik/Dockerfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
FROM traefik:v2.2.11
|
||||
RUN mkdir -p /etc/traefik/acme \
|
||||
&& touch /etc/traefik/acme/acme.json \
|
||||
&& chmod 600 /etc/traefik/acme/acme.json
|
||||
COPY ./compose/production/traefik/traefik.yml /etc/traefik
|
75
compose/production/traefik/traefik.yml
Normal file
75
compose/production/traefik/traefik.yml
Normal file
|
@ -0,0 +1,75 @@
|
|||
log:
|
||||
level: INFO
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
# http
|
||||
address: ":80"
|
||||
http:
|
||||
# https://docs.traefik.io/routing/entrypoints/#entrypoint
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: web-secure
|
||||
|
||||
web-secure:
|
||||
# https
|
||||
address: ":443"
|
||||
|
||||
flower:
|
||||
address: ":5555"
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
# https://docs.traefik.io/master/https/acme/#lets-encrypt
|
||||
acme:
|
||||
email: "sanspie@akarpov.ru"
|
||||
storage: /etc/traefik/acme/acme.json
|
||||
# https://docs.traefik.io/master/https/acme/#httpchallenge
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
|
||||
http:
|
||||
routers:
|
||||
web-secure-router:
|
||||
rule: "Host(`akarpov.ru`) || Host(`www.akarpov.ru`)"
|
||||
entryPoints:
|
||||
- web-secure
|
||||
middlewares:
|
||||
- csrf
|
||||
service: django
|
||||
tls:
|
||||
# https://docs.traefik.io/master/routing/routers/#certresolver
|
||||
certResolver: letsencrypt
|
||||
|
||||
flower-secure-router:
|
||||
rule: "Host(`akarpov.ru`)"
|
||||
entryPoints:
|
||||
- flower
|
||||
service: flower
|
||||
tls:
|
||||
# https://docs.traefik.io/master/routing/routers/#certresolver
|
||||
certResolver: letsencrypt
|
||||
|
||||
middlewares:
|
||||
csrf:
|
||||
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
|
||||
# https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
|
||||
headers:
|
||||
hostsProxyHeaders: ["X-CSRFToken"]
|
||||
|
||||
services:
|
||||
django:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://django:5000
|
||||
|
||||
flower:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://flower:5555
|
||||
|
||||
providers:
|
||||
# https://docs.traefik.io/master/providers/file/
|
||||
file:
|
||||
filename: /etc/traefik/traefik.yml
|
||||
watch: true
|
5
config/__init__.py
Normal file
5
config/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery_app import app as celery_app
|
||||
|
||||
__all__ = ("celery_app",)
|
4
config/api_router.py
Normal file
4
config/api_router.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from django.urls import path, include
|
||||
|
||||
app_name = "api"
|
||||
urlpatterns = [path("", include("marking_hack.market.api.urls"))]
|
17
config/celery_app.py
Normal file
17
config/celery_app.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
# set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||
|
||||
app = Celery("marking_hack")
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
# the configuration object to child processes.
|
||||
# - namespace='CELERY' means all celery-related configuration keys
|
||||
# should have a `CELERY_` prefix.
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
app.autodiscover_tasks()
|
0
config/settings/__init__.py
Normal file
0
config/settings/__init__.py
Normal file
337
config/settings/base.py
Normal file
337
config/settings/base.py
Normal file
|
@ -0,0 +1,337 @@
|
|||
"""
|
||||
Base settings to build other settings files upon.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import environ
|
||||
import structlog
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
|
||||
# marking_hack/
|
||||
APPS_DIR = ROOT_DIR / "marking_hack"
|
||||
env = environ.Env()
|
||||
|
||||
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=True)
|
||||
if READ_DOT_ENV_FILE:
|
||||
# OS environment variables take precedence over variables from .env
|
||||
env.read_env(str(ROOT_DIR / ".env"))
|
||||
|
||||
# GENERAL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
|
||||
DEBUG = env.bool("DJANGO_DEBUG", False)
|
||||
# Local time zone. Choices are
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# though not all of them may be available with every OS.
|
||||
# In Windows, this must be set to your system time zone.
|
||||
TIME_ZONE = "Europe/Moscow"
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
|
||||
LANGUAGE_CODE = "en-us"
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
|
||||
SITE_ID = 1
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
|
||||
USE_I18N = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
|
||||
USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
|
||||
LOCALE_PATHS = [str(ROOT_DIR / "locale")]
|
||||
|
||||
# DATABASES
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
|
||||
DATABASES = {"default": env.db("DATABASE_URL")}
|
||||
DATABASES["default"]["ATOMIC_REQUESTS"] = True
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# URLS
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
|
||||
ROOT_URLCONF = "config.urls"
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
# APPS
|
||||
# ------------------------------------------------------------------------------
|
||||
DJANGO_APPS = [
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
# "django.contrib.humanize", # Handy template tags
|
||||
"django.contrib.admin",
|
||||
"django.forms",
|
||||
]
|
||||
THIRD_PARTY_APPS = [
|
||||
"django_celery_beat",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
"marking_hack.users",
|
||||
"marking_hack.market",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
||||
# MIGRATIONS
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules
|
||||
MIGRATION_MODULES = {"sites": "marking_hack.contrib.sites.migrations"}
|
||||
|
||||
# AUTHENTICATION
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
|
||||
AUTH_USER_MODEL = "users.User"
|
||||
|
||||
# PASSWORDS
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
||||
PASSWORD_HASHERS = [
|
||||
# https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django
|
||||
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
||||
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
# MIDDLEWARE
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.common.BrokenLinkEmailsMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
# STATIC
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
|
||||
STATIC_ROOT = str(ROOT_DIR / "staticfiles")
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
||||
STATIC_URL = "/static/"
|
||||
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
|
||||
STATICFILES_DIRS = [str(APPS_DIR / "static")]
|
||||
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
|
||||
STATICFILES_FINDERS = [
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
]
|
||||
|
||||
# MEDIA
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
||||
MEDIA_ROOT = str(APPS_DIR / "media")
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
# TEMPLATES
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
|
||||
TEMPLATES = [
|
||||
{
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#dirs
|
||||
"DIRS": [str(APPS_DIR / "templates")],
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#app-dirs
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.template.context_processors.i18n",
|
||||
"django.template.context_processors.media",
|
||||
"django.template.context_processors.static",
|
||||
"django.template.context_processors.tz",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer
|
||||
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
||||
|
||||
# FIXTURES
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs
|
||||
FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
|
||||
|
||||
# SECURITY
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
# ADMIN
|
||||
# ------------------------------------------------------------------------------
|
||||
# Django Admin URL.
|
||||
ADMIN_URL = "admin/"
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#admins
|
||||
ADMINS = [("""sanspie""", "sanspie@akarpov.ru")]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
|
||||
MANAGERS = ADMINS
|
||||
|
||||
# LOGGING
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
|
||||
# See https://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"json_formatter": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.processors.JSONRenderer(),
|
||||
},
|
||||
"plain_console": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.dev.ConsoleRenderer(),
|
||||
},
|
||||
"key_value": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.processors.KeyValueRenderer(
|
||||
key_order=["timestamp", "level", "event", "logger"]
|
||||
),
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "plain_console",
|
||||
},
|
||||
"json_file": {
|
||||
"class": "logging.handlers.WatchedFileHandler",
|
||||
"filename": "logs/json.log",
|
||||
"formatter": "json_formatter",
|
||||
},
|
||||
"flat_line_file": {
|
||||
"class": "logging.handlers.WatchedFileHandler",
|
||||
"filename": "logs/flat_line.log",
|
||||
"formatter": "key_value",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django_structlog": {
|
||||
"handlers": ["console", "flat_line_file", "json_file"],
|
||||
"level": "INFO",
|
||||
},
|
||||
# Make sure to replace the following logger's name for yours
|
||||
"django_structlog_demo_project": {
|
||||
"handlers": ["console", "flat_line_file", "json_file"],
|
||||
"level": "INFO",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||
],
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
# Celery
|
||||
# ------------------------------------------------------------------------------
|
||||
if USE_TZ:
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-timezone
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-broker_url
|
||||
CELERY_BROKER_URL = env("CELERY_BROKER_URL")
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_backend
|
||||
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-extended
|
||||
CELERY_RESULT_EXTENDED = True
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-backend-always-retry
|
||||
# https://github.com/celery/celery/pull/6122
|
||||
CELERY_RESULT_BACKEND_ALWAYS_RETRY = True
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-backend-max-retries
|
||||
CELERY_RESULT_BACKEND_MAX_RETRIES = 10
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-accept_content
|
||||
CELERY_ACCEPT_CONTENT = ["json"]
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-task_serializer
|
||||
CELERY_TASK_SERIALIZER = "json"
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_serializer
|
||||
CELERY_RESULT_SERIALIZER = "json"
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-time-limit
|
||||
CELERY_TASK_TIME_LIMIT = 5 * 60
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-soft-time-limit
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 60
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-scheduler
|
||||
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#worker-send-task-events
|
||||
CELERY_WORKER_SEND_TASK_EVENTS = True
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std-setting-task_send_sent_event
|
||||
CELERY_TASK_SEND_SENT_EVENT = True
|
||||
# DRF
|
||||
# -------------------------------------------------------------------------------
|
||||
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
),
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
||||
CORS_URLS_REGEX = r"^/api/.*$"
|
||||
|
||||
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
|
||||
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Marking hack API",
|
||||
"DESCRIPTION": "Documentation of API endpoints of Marking hack",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_PERMISSIONS": [],
|
||||
"SERVERS": [
|
||||
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
|
||||
{"url": "https://dev2.akarpov.ru", "description": "Production server"},
|
||||
],
|
||||
}
|
61
config/settings/local.py
Normal file
61
config/settings/local.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from .base import * # noqa
|
||||
from .base import env
|
||||
|
||||
# GENERAL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
|
||||
DEBUG = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
SECRET_KEY = env(
|
||||
"DJANGO_SECRET_KEY",
|
||||
default="OUKFQXgTXGLApyNntMc7YrwGwc1Sgejj0C7OXNiAko7wMoXPkacvOdC6YukmBsIB",
|
||||
)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "dev2.akarpov.ru"]
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
# CACHES
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
"LOCATION": "",
|
||||
}
|
||||
}
|
||||
|
||||
# WhiteNoise
|
||||
# ------------------------------------------------------------------------------
|
||||
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development
|
||||
INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405
|
||||
|
||||
|
||||
# django-debug-toolbar
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
|
||||
INSTALLED_APPS += ["debug_toolbar"] # noqa F405
|
||||
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
|
||||
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
|
||||
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
|
||||
"SHOW_TEMPLATE_CONTEXT": True,
|
||||
}
|
||||
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
|
||||
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
|
||||
if env("USE_DOCKER") == "yes":
|
||||
import socket
|
||||
|
||||
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
|
||||
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
|
||||
|
||||
# django-extensions
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
|
||||
INSTALLED_APPS += ["django_extensions"] # noqa F405
|
||||
# Celery
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-eager-propagates
|
||||
CELERY_TASK_EAGER_PROPAGATES = True
|
||||
# Your stuff...
|
||||
# ------------------------------------------------------------------------------
|
115
config/settings/production.py
Normal file
115
config/settings/production.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
from .base import * # noqa
|
||||
from .base import env
|
||||
|
||||
# GENERAL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
SECRET_KEY = env("DJANGO_SECRET_KEY")
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["akarpov.ru"])
|
||||
|
||||
# DATABASES
|
||||
# ------------------------------------------------------------------------------
|
||||
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405
|
||||
|
||||
# CACHES
|
||||
# ------------------------------------------------------------------------------
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": env("REDIS_URL"),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
# Mimicing memcache behavior.
|
||||
# https://github.com/jazzband/django-redis#memcached-exceptions-behavior
|
||||
"IGNORE_EXCEPTIONS": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# SECURITY
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect
|
||||
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure
|
||||
SESSION_COOKIE_SECURE = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure
|
||||
CSRF_COOKIE_SECURE = True
|
||||
# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds
|
||||
SECURE_HSTS_SECONDS = 518400
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
|
||||
"DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True
|
||||
)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
|
||||
SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True)
|
||||
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = env.bool(
|
||||
"DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True
|
||||
)
|
||||
|
||||
# STATIC
|
||||
# ------------------------
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
# MEDIA
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ADMIN
|
||||
# ------------------------------------------------------------------------------
|
||||
# Django Admin URL regex.
|
||||
ADMIN_URL = env("DJANGO_ADMIN_URL")
|
||||
# LOGGING
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
|
||||
# See https://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
# A sample logging configuration. The only tangible logging
|
||||
# performed by this configuration is to send an email to
|
||||
# the site admins on every HTTP 500 error when DEBUG=False.
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "%(levelname)s %(asctime)s %(module)s "
|
||||
"%(process)d %(thread)d %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"mail_admins": {
|
||||
"level": "ERROR",
|
||||
"filters": ["require_debug_false"],
|
||||
"class": "django.utils.log.AdminEmailHandler",
|
||||
},
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
"root": {"level": "INFO", "handlers": ["console"]},
|
||||
"loggers": {
|
||||
"django.request": {
|
||||
"handlers": ["mail_admins"],
|
||||
"level": "ERROR",
|
||||
"propagate": True,
|
||||
},
|
||||
"django.security.DisallowedHost": {
|
||||
"level": "ERROR",
|
||||
"handlers": ["console", "mail_admins"],
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# django-rest-framework
|
||||
# -------------------------------------------------------------------------------
|
||||
# Tools that generate code samples can use SERVERS to point to the correct domain
|
||||
SPECTACULAR_SETTINGS["SERVERS"] = [ # noqa F405
|
||||
{"url": "https://akarpov.ru", "description": "Production server"}
|
||||
]
|
33
config/settings/test.py
Normal file
33
config/settings/test.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
With these settings, tests run faster.
|
||||
"""
|
||||
|
||||
from .base import * # noqa
|
||||
from .base import env
|
||||
|
||||
# GENERAL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
SECRET_KEY = env(
|
||||
"DJANGO_SECRET_KEY",
|
||||
default="J8gSpAJLzFVMg8sum1YNYKbcpwiZdZJyqmCTTxYr3V8ZVEwby6sQPXibGkKS7Ha5",
|
||||
)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
|
||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||
|
||||
# PASSWORDS
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
||||
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
||||
|
||||
# EMAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
|
||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
# DEBUGGING FOR TEMPLATES
|
||||
# ------------------------------------------------------------------------------
|
||||
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa F405
|
||||
|
||||
# Your stuff...
|
||||
# ------------------------------------------------------------------------------
|
33
config/urls.py
Normal file
33
config/urls.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
|
||||
urlpatterns = [
|
||||
# Django Admin, use {% url 'admin:index' %}
|
||||
path(settings.ADMIN_URL, admin.site.urls),
|
||||
# User management
|
||||
# Your stuff: custom urls includes go here
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
# API URLS
|
||||
urlpatterns += [
|
||||
# API base url
|
||||
path("api/", include("config.api_router")),
|
||||
# DRF auth token
|
||||
# path("api/auth/token/", obtain_auth_token),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
SpectacularSwaggerView.as_view(url_name="api-schema"),
|
||||
name="api-docs",
|
||||
),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
# This allows the error pages to be debugged during development, just visit
|
||||
# these url in browser to see how these error pages look like.
|
||||
if "debug_toolbar" in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
|
38
config/wsgi.py
Normal file
38
config/wsgi.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
WSGI config for Marking hack project.
|
||||
|
||||
This module contains the WSGI application used by Django's development server
|
||||
and any production WSGI deployments. It should expose a module-level variable
|
||||
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||
this application via the ``WSGI_APPLICATION`` setting.
|
||||
|
||||
Usually you will have the standard Django WSGI application here, but it also
|
||||
might make sense to replace the whole Django WSGI application with a custom one
|
||||
that later delegates to the Django one. For example, you could introduce WSGI
|
||||
middleware here, or combine a Django application with an application of another
|
||||
framework.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
# This allows easy placement of apps within the interior
|
||||
# marking_hack directory.
|
||||
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||
sys.path.append(str(ROOT_DIR / "marking_hack"))
|
||||
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
|
||||
# if running multiple sites in the same mod_wsgi process. To fix this, use
|
||||
# mod_wsgi daemon mode with each site in its own daemon process, or use
|
||||
# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
|
||||
|
||||
# This application object is used by any WSGI server configured to use this
|
||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||
# setting points here.
|
||||
application = get_wsgi_application()
|
||||
# Apply WSGI middleware here.
|
||||
# from helloworld.wsgi import HelloWorldApplication
|
||||
# application = HelloWorldApplication(application)
|
68
local.yml
Normal file
68
local.yml
Normal file
|
@ -0,0 +1,68 @@
|
|||
version: '3'
|
||||
|
||||
volumes:
|
||||
marking_hack_local_postgres_data: {}
|
||||
marking_hack_local_postgres_data_backups: {}
|
||||
|
||||
services:
|
||||
django: &django
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./compose/local/django/Dockerfile
|
||||
image: marking_hack_local_django
|
||||
container_name: marking_hack_local_django
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
volumes:
|
||||
- .:/app:z
|
||||
env_file:
|
||||
- ./.envs/.local/.django
|
||||
- ./.envs/.local/.postgres
|
||||
ports:
|
||||
- "8000:8000"
|
||||
command: /start
|
||||
|
||||
postgres:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./compose/production/postgres/Dockerfile
|
||||
image: marking_hack_production_postgres
|
||||
container_name: marking_hack_local_postgres
|
||||
volumes:
|
||||
- marking_hack_local_postgres_data:/var/lib/postgresql/data
|
||||
- marking_hack_local_postgres_data_backups:/backups
|
||||
env_file:
|
||||
- ./.envs/.local/.postgres
|
||||
|
||||
redis:
|
||||
image: redis:6
|
||||
container_name: marking_hack_local_redis
|
||||
|
||||
celeryworker:
|
||||
<<: *django
|
||||
image: marking_hack_local_celeryworker
|
||||
container_name: marking_hack_local_celeryworker
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
ports: []
|
||||
command: /start-celeryworker
|
||||
|
||||
celerybeat:
|
||||
<<: *django
|
||||
image: marking_hack_local_celerybeat
|
||||
container_name: marking_hack_local_celerybeat
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
ports: []
|
||||
command: /start-celerybeat
|
||||
|
||||
flower:
|
||||
<<: *django
|
||||
image: marking_hack_local_flower
|
||||
container_name: marking_hack_local_flower
|
||||
ports:
|
||||
- "5555:5555"
|
||||
command: /start-flower
|
6
locale/README.rst
Normal file
6
locale/README.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
Translations
|
||||
============
|
||||
|
||||
Translations will be placed in this folder when running::
|
||||
|
||||
python manage.py makemessages
|
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
31
manage.py
Executable file
31
manage.py
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django # noqa
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
# This allows easy placement of apps within the interior
|
||||
# marking_hack directory.
|
||||
current_path = Path(__file__).parent.resolve()
|
||||
sys.path.append(str(current_path / "marking_hack"))
|
||||
|
||||
execute_from_command_line(sys.argv)
|
5
marking_hack/__init__.py
Normal file
5
marking_hack/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
__version__ = "0.1.0"
|
||||
__version_info__ = tuple(
|
||||
int(num) if num.isdigit() else num
|
||||
for num in __version__.replace("-", ".", 1).split(".")
|
||||
)
|
0
marking_hack/common/__init__.py
Normal file
0
marking_hack/common/__init__.py
Normal file
19
marking_hack/common/api.py
Normal file
19
marking_hack/common/api.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class SmallResultsSetPagination(PageNumberPagination):
|
||||
page_size = 10
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 50
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 200
|
||||
|
||||
|
||||
class BigResultsSetPagination(PageNumberPagination):
|
||||
page_size = 100
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 1000
|
18
marking_hack/common/tasks.py
Normal file
18
marking_hack/common/tasks.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from celery import shared_task
|
||||
from django.apps import apps
|
||||
from django.core.files import File
|
||||
|
||||
from marking_hack.utils.files import crop_image
|
||||
|
||||
|
||||
@shared_task()
|
||||
def crop_model_image(pk: int, app_label: str, model_name: str):
|
||||
model = apps.get_model(app_label=app_label, model_name=model_name)
|
||||
instance = model.objects.get(pk=pk)
|
||||
instance.image_cropped.save(
|
||||
instance.image.path.split(".")[0].split("/")[-1] + ".png",
|
||||
File(crop_image(instance.image.path, length=250)),
|
||||
save=False,
|
||||
)
|
||||
instance.save(update_fields=["image_cropped"])
|
||||
return pk
|
14
marking_hack/conftest.py
Normal file
14
marking_hack/conftest.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
|
||||
from marking_hack.users.models import User
|
||||
from marking_hack.users.tests.factories import UserFactory
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def media_storage(settings, tmpdir):
|
||||
settings.MEDIA_ROOT = tmpdir.strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(db) -> User:
|
||||
return UserFactory()
|
0
marking_hack/contrib/__init__.py
Normal file
0
marking_hack/contrib/__init__.py
Normal file
0
marking_hack/contrib/sites/__init__.py
Normal file
0
marking_hack/contrib/sites/__init__.py
Normal file
42
marking_hack/contrib/sites/migrations/0001_initial.py
Normal file
42
marking_hack/contrib/sites/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
import django.contrib.sites.models
|
||||
from django.contrib.sites.models import _simple_domain_name_validator
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Site",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"domain",
|
||||
models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="domain name",
|
||||
validators=[_simple_domain_name_validator],
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=50, verbose_name="display name")),
|
||||
],
|
||||
options={
|
||||
"ordering": ("domain",),
|
||||
"db_table": "django_site",
|
||||
"verbose_name": "site",
|
||||
"verbose_name_plural": "sites",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
managers=[("objects", django.contrib.sites.models.SiteManager())],
|
||||
)
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
import django.contrib.sites.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("sites", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="site",
|
||||
name="domain",
|
||||
field=models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
validators=[django.contrib.sites.models._simple_domain_name_validator],
|
||||
verbose_name="domain name",
|
||||
),
|
||||
)
|
||||
]
|
|
@ -0,0 +1,63 @@
|
|||
"""
|
||||
To understand why this file is here, please read:
|
||||
|
||||
http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def _update_or_create_site_with_sequence(site_model, connection, domain, name):
|
||||
"""Update or create the site with default ID and keep the DB sequence in sync."""
|
||||
site, created = site_model.objects.update_or_create(
|
||||
id=settings.SITE_ID,
|
||||
defaults={
|
||||
"domain": domain,
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
# We provided the ID explicitly when creating the Site entry, therefore the DB
|
||||
# sequence to auto-generate them wasn't used and is now out of sync. If we
|
||||
# don't do anything, we'll get a unique constraint violation the next time a
|
||||
# site is created.
|
||||
# To avoid this, we need to manually update DB sequence and make sure it's
|
||||
# greater than the maximum value.
|
||||
max_id = site_model.objects.order_by('-id').first().id
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT last_value from django_site_id_seq")
|
||||
(current_id,) = cursor.fetchone()
|
||||
if current_id <= max_id:
|
||||
cursor.execute(
|
||||
"alter sequence django_site_id_seq restart with %s",
|
||||
[max_id + 1],
|
||||
)
|
||||
|
||||
|
||||
def update_site_forward(apps, schema_editor):
|
||||
"""Set site domain and name."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
_update_or_create_site_with_sequence(
|
||||
Site,
|
||||
schema_editor.connection,
|
||||
"marking_hack",
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
def update_site_backward(apps, schema_editor):
|
||||
"""Revert site domain and name to default."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
_update_or_create_site_with_sequence(
|
||||
Site,
|
||||
schema_editor.connection,
|
||||
"example.com",
|
||||
"example.com",
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("sites", "0002_alter_domain_unique")]
|
||||
|
||||
operations = [migrations.RunPython(update_site_forward, update_site_backward)]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1.7 on 2021-02-04 14:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("sites", "0003_set_site_domain_and_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="site",
|
||||
options={
|
||||
"ordering": ["domain"],
|
||||
"verbose_name": "site",
|
||||
"verbose_name_plural": "sites",
|
||||
},
|
||||
),
|
||||
]
|
0
marking_hack/contrib/sites/migrations/__init__.py
Normal file
0
marking_hack/contrib/sites/migrations/__init__.py
Normal file
0
marking_hack/market/__init__.py
Normal file
0
marking_hack/market/__init__.py
Normal file
3
marking_hack/market/admin.py
Normal file
3
marking_hack/market/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
0
marking_hack/market/api/__init__.py
Normal file
0
marking_hack/market/api/__init__.py
Normal file
69
marking_hack/market/api/serializers.py
Normal file
69
marking_hack/market/api/serializers.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from numpy import nan
|
||||
from rest_framework import serializers
|
||||
|
||||
from marking_hack.market.models import Store, Region, Item, ItemSale, ItemTransaction
|
||||
|
||||
|
||||
class StoreSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Store
|
||||
fields = ["id_sp", "region_name", "city_name", "postal_code"]
|
||||
|
||||
|
||||
class ItemSerializer(serializers.ModelSerializer):
|
||||
name = serializers.CharField(source="product_name")
|
||||
category = serializers.CharField(source="product_short_name")
|
||||
current_amount = serializers.SerializerMethodField()
|
||||
volume = serializers.SerializerMethodField()
|
||||
|
||||
def get_current_amount(self, obj):
|
||||
amount = obj.sales.first().amount
|
||||
if amount == nan:
|
||||
return 0
|
||||
return float(amount)
|
||||
|
||||
def get_volume(self, obj):
|
||||
print(obj.volume)
|
||||
if obj.volume == nan:
|
||||
return 0
|
||||
return obj.volume
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = [
|
||||
"gtin",
|
||||
"name",
|
||||
"category",
|
||||
"brand",
|
||||
"country",
|
||||
"volume",
|
||||
"current_amount",
|
||||
]
|
||||
|
||||
|
||||
class ListRegionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ["code", "name"]
|
||||
|
||||
|
||||
class ItemSaleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemSale
|
||||
fields = ["date", "type_operation", "cnt"]
|
||||
|
||||
|
||||
class ItemTransactionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemTransaction
|
||||
fields = ["date", "cnt", "sender_region_code"]
|
||||
|
||||
|
||||
class ShopItemSerializer(serializers.Serializer):
|
||||
id_sp = serializers.CharField(max_length=32)
|
||||
items = serializers.ListSerializer(child=serializers.CharField(max_length=32))
|
||||
|
||||
|
||||
class PredictSerializer(serializers.Serializer):
|
||||
date = serializers.DateField()
|
||||
shops = ShopItemSerializer(many=True)
|
16
marking_hack/market/api/urls.py
Normal file
16
marking_hack/market/api/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "market"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.ListStore.as_view()),
|
||||
path("predict", views.PredictItemsView.as_view()),
|
||||
path("<str:id_sp>", views.ListStoreItems.as_view()),
|
||||
path("regions/", views.RegionListView.as_view()),
|
||||
path("regions/<int:code>/sales", views.RegionSalesListView.as_view()),
|
||||
path(
|
||||
"regions/<int:code>/trsansactions", views.RegionTransactionsListView.as_view()
|
||||
),
|
||||
]
|
78
marking_hack/market/api/views.py
Normal file
78
marking_hack/market/api/views.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from marking_hack.common.api import BigResultsSetPagination
|
||||
from marking_hack.market.api.serializers import (
|
||||
StoreSerializer,
|
||||
ItemSerializer,
|
||||
ListRegionSerializer,
|
||||
ItemSaleSerializer,
|
||||
ItemTransactionSerializer,
|
||||
PredictSerializer,
|
||||
)
|
||||
from marking_hack.market.models import Store, Region, StoreItem, Item
|
||||
|
||||
|
||||
class ListStore(generics.ListAPIView):
|
||||
serializer_class = StoreSerializer
|
||||
pagination_class = BigResultsSetPagination
|
||||
queryset = Store.objects.order_by("-id_sp")
|
||||
|
||||
|
||||
class ListStoreItems(generics.ListAPIView):
|
||||
serializer_class = ItemSerializer
|
||||
pagination_class = BigResultsSetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
store = get_object_or_404(Store, id_sp=self.kwargs["id_sp"])
|
||||
ids = (
|
||||
StoreItem.objects.filter(store=store)
|
||||
.values_list("item", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
return Item.objects.filter(id__in=ids)
|
||||
|
||||
|
||||
class RegionListView(generics.ListAPIView):
|
||||
serializer_class = ListRegionSerializer
|
||||
queryset = Region.objects.all()
|
||||
|
||||
|
||||
class RegionSalesListView(generics.ListAPIView):
|
||||
serializer_class = ItemSaleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
region = get_object_or_404(Region, code=self.kwargs["code"])
|
||||
return region.sales.all()
|
||||
|
||||
|
||||
class RegionTransactionsListView(generics.ListAPIView):
|
||||
serializer_class = ItemTransactionSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
region = get_object_or_404(Region, code=self.kwargs["code"])
|
||||
return region.transactions.all()
|
||||
|
||||
|
||||
class PredictItemsView(generics.GenericAPIView):
|
||||
serializer_class = PredictSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = PredictSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.data
|
||||
shops = [dict(x) for x in data["shops"]]
|
||||
for shop in shops:
|
||||
store = get_object_or_404(Store, id_sp=shop["id_sp"])
|
||||
items = []
|
||||
for x_item in shop["items"]:
|
||||
item = get_object_or_404(Item, gtin=x_item)
|
||||
qs = StoreItem.objects.filter(store=store, item=item)
|
||||
if qs.exists():
|
||||
items.append({"id": x_item, "predicted_volume": qs.last().amount})
|
||||
else:
|
||||
print(qs)
|
||||
items.append({"id": x_item, "predicted_volume": 0})
|
||||
data["shops"] = shops
|
||||
return Response(data=data)
|
5
marking_hack/market/apps.py
Normal file
5
marking_hack/market/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MarketConfig(AppConfig):
|
||||
name = "marking_hack.market"
|
163
marking_hack/market/migrations/0001_initial.py
Normal file
163
marking_hack/market/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-25 23:19
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="City",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("fias", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||
("name", models.CharField(max_length=250)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Member",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("inn", models.CharField(db_index=True, max_length=32, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Region",
|
||||
fields=[
|
||||
(
|
||||
"code",
|
||||
models.IntegerField(
|
||||
db_index=True, primary_key=True, serialize=False, unique=True
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Store",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("id_sp", models.CharField(db_index=True, max_length=32, unique=True)),
|
||||
("postal_code", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
"city",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="stores",
|
||||
to="market.city",
|
||||
),
|
||||
),
|
||||
(
|
||||
"member",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="stores",
|
||||
to="market.member",
|
||||
),
|
||||
),
|
||||
(
|
||||
"region",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="stores",
|
||||
to="market.region",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="StoreExport",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("file", models.FileField(upload_to="exports/")),
|
||||
(
|
||||
"store",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="exports",
|
||||
to="market.store",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="member",
|
||||
name="region",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="members",
|
||||
to="market.region",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Item",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("gtin", models.CharField(db_index=True, max_length=32, unique=True)),
|
||||
("product_name", models.CharField(max_length=250)),
|
||||
("product_short_name", models.CharField(max_length=250)),
|
||||
("tnved", models.IntegerField()),
|
||||
("tnved10", models.IntegerField()),
|
||||
("brand", models.CharField(max_length=250)),
|
||||
("country", models.CharField(max_length=100)),
|
||||
("volume", models.IntegerField()),
|
||||
(
|
||||
"member",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="items",
|
||||
to="market.member",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
21
marking_hack/market/migrations/0002_alter_city_fias.py
Normal file
21
marking_hack/market/migrations/0002_alter_city_fias.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-25 23:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="city",
|
||||
name="fias",
|
||||
field=models.UUIDField(
|
||||
db_index=True, default=uuid.uuid4, editable=False, unique=True
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 00:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0002_alter_city_fias"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="country",
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="tnved",
|
||||
field=models.CharField(db_index=True, max_length=32, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="tnved10",
|
||||
field=models.CharField(db_index=True, max_length=32, unique=True),
|
||||
),
|
||||
]
|
18
marking_hack/market/migrations/0004_alter_item_volume.py
Normal file
18
marking_hack/market/migrations/0004_alter_item_volume.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 00:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0003_alter_item_country_alter_item_tnved_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="volume",
|
||||
field=models.FloatField(),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 00:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0004_alter_item_volume"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="tnved",
|
||||
field=models.CharField(db_index=True, max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="tnved10",
|
||||
field=models.CharField(db_index=True, max_length=32),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,63 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 02:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0005_alter_item_tnved_alter_item_tnved10"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ItemTransaction",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateField()),
|
||||
("cnt", models.IntegerField()),
|
||||
(
|
||||
"region",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="transactions",
|
||||
to="market.region",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ItemSale",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateField()),
|
||||
("type_operation", models.CharField(max_length=50)),
|
||||
("cnt", models.IntegerField()),
|
||||
(
|
||||
"region",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sales",
|
||||
to="market.region",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
46
marking_hack/market/migrations/0007_storeitem.py
Normal file
46
marking_hack/market/migrations/0007_storeitem.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 09:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0006_itemtransaction_itemsale"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="StoreItem",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("data", models.DateField()),
|
||||
("amount", models.IntegerField()),
|
||||
(
|
||||
"item",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sales",
|
||||
to="market.item",
|
||||
),
|
||||
),
|
||||
(
|
||||
"store",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sales",
|
||||
to="market.store",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 09:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0007_storeitem"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="storeitem",
|
||||
old_name="data",
|
||||
new_name="date",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 09:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0008_rename_data_storeitem_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="storeitem",
|
||||
options={"ordering": ["-date"]},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-26 10:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("market", "0009_alter_storeitem_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="itemtransaction",
|
||||
name="sender_region_code",
|
||||
field=models.IntegerField(default=1),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
0
marking_hack/market/migrations/__init__.py
Normal file
0
marking_hack/market/migrations/__init__.py
Normal file
113
marking_hack/market/models.py
Normal file
113
marking_hack/market/models.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Region(models.Model):
|
||||
code = models.IntegerField(unique=True, primary_key=True, db_index=True)
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class City(models.Model):
|
||||
fias = models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, unique=True, db_index=True
|
||||
)
|
||||
name = models.CharField(max_length=250)
|
||||
|
||||
|
||||
class Store(models.Model):
|
||||
id_sp = models.CharField(max_length=32, unique=True, db_index=True)
|
||||
member = models.ForeignKey(
|
||||
"Member", related_name="stores", on_delete=models.CASCADE
|
||||
)
|
||||
region = models.ForeignKey(
|
||||
"Region",
|
||||
null=True,
|
||||
related_name="stores",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
city = models.ForeignKey(
|
||||
"City",
|
||||
null=True,
|
||||
related_name="stores",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
postal_code = models.IntegerField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def region_code(self):
|
||||
return self.region_id
|
||||
|
||||
@property
|
||||
def city_name(self):
|
||||
if self.city:
|
||||
return self.city.name
|
||||
return ""
|
||||
|
||||
@property
|
||||
def region_name(self):
|
||||
if self.region:
|
||||
return self.region.name
|
||||
return ""
|
||||
|
||||
|
||||
class Item(models.Model):
|
||||
gtin = models.CharField(max_length=32, unique=True, db_index=True)
|
||||
member = models.ForeignKey("Member", related_name="items", on_delete=models.CASCADE)
|
||||
product_name = models.CharField(max_length=250)
|
||||
product_short_name = models.CharField(max_length=250)
|
||||
tnved = models.CharField(max_length=32, db_index=True)
|
||||
tnved10 = models.CharField(max_length=32, db_index=True)
|
||||
brand = models.CharField(max_length=250)
|
||||
country = models.CharField(max_length=100, blank=True)
|
||||
volume = models.FloatField()
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
inn = models.CharField(max_length=32, unique=True, db_index=True)
|
||||
region = models.ForeignKey(
|
||||
"Region",
|
||||
null=True,
|
||||
related_name="members",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
|
||||
class StoreExport(models.Model):
|
||||
store = models.ForeignKey("Store", related_name="exports", on_delete=models.CASCADE)
|
||||
file = models.FileField(upload_to="exports/")
|
||||
|
||||
def __str__(self):
|
||||
return f"export file from {self.store}"
|
||||
|
||||
|
||||
class ItemSale(models.Model):
|
||||
date = models.DateField()
|
||||
region = models.ForeignKey("Region", related_name="sales", on_delete=models.CASCADE)
|
||||
type_operation = models.CharField(max_length=50)
|
||||
cnt = models.IntegerField()
|
||||
|
||||
|
||||
class ItemTransaction(models.Model):
|
||||
date = models.DateField()
|
||||
region = models.ForeignKey(
|
||||
"Region", related_name="transactions", on_delete=models.CASCADE
|
||||
)
|
||||
sender_region_code = models.IntegerField()
|
||||
cnt = models.IntegerField()
|
||||
|
||||
|
||||
class StoreItem(models.Model):
|
||||
store = models.ForeignKey("Store", related_name="sales", on_delete=models.CASCADE)
|
||||
item = models.ForeignKey("Item", related_name="sales", on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
amount = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-date"]
|
28
marking_hack/market/resources.py
Normal file
28
marking_hack/market/resources.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from import_export import fields, resources
|
||||
from import_export.widgets import ForeignKeyWidget
|
||||
|
||||
from marking_hack.market.models import Store, Item, Region
|
||||
|
||||
|
||||
class ItemResource(resources.ModelResource):
|
||||
store = fields.Field(
|
||||
column_name="store",
|
||||
attribute="store",
|
||||
widget=ForeignKeyWidget(Store, field="id"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ("store", "name", "amount")
|
||||
|
||||
|
||||
class StoreResource(resources.ModelResource):
|
||||
region = fields.Field(
|
||||
column_name="region",
|
||||
attribute="region",
|
||||
widget=ForeignKeyWidget(Region, field="code"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ("name", "region", "postal_code")
|
3
marking_hack/market/views.py
Normal file
3
marking_hack/market/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
marking_hack/static/.gitkeep
Normal file
0
marking_hack/static/.gitkeep
Normal file
0
marking_hack/users/__init__.py
Normal file
0
marking_hack/users/__init__.py
Normal file
0
marking_hack/users/admin.py
Normal file
0
marking_hack/users/admin.py
Normal file
14
marking_hack/users/api/serializers.py
Normal file
14
marking_hack/users/api/serializers.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import serializers
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "name", "url"]
|
||||
|
||||
extra_kwargs = {
|
||||
"url": {"view_name": "api:user-detail", "lookup_field": "username"}
|
||||
}
|
25
marking_hack/users/api/views.py
Normal file
25
marking_hack/users/api/views.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from .serializers import UserSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet):
|
||||
serializer_class = UserSerializer
|
||||
queryset = User.objects.all()
|
||||
lookup_field = "username"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
assert isinstance(self.request.user.id, int)
|
||||
return self.queryset.filter(id=self.request.user.id)
|
||||
|
||||
@action(detail=False)
|
||||
def me(self, request):
|
||||
serializer = UserSerializer(request.user, context={"request": request})
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
13
marking_hack/users/apps.py
Normal file
13
marking_hack/users/apps.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = "marking_hack.users"
|
||||
verbose_name = _("Users")
|
||||
|
||||
def ready(self):
|
||||
try:
|
||||
import marking_hack.users.signals # noqa F401
|
||||
except ImportError:
|
||||
pass
|
118
marking_hack/users/migrations/0001_initial.py
Normal file
118
marking_hack/users/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
0
marking_hack/users/migrations/__init__.py
Normal file
0
marking_hack/users/migrations/__init__.py
Normal file
13
marking_hack/users/models.py
Normal file
13
marking_hack/users/models.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Default custom user model for Marking hack.
|
||||
If adding fields that need to be filled at user signup,
|
||||
check forms.SignupForm and forms.SocialSignupForms accordingly.
|
||||
"""
|
||||
|
||||
#: First and last name do not cover name patterns around the globe
|
||||
first_name = None # type: ignore
|
||||
last_name = None # type: ignore
|
1
marking_hack/users/views.py
Normal file
1
marking_hack/users/views.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
0
marking_hack/utils/__init__.py
Normal file
0
marking_hack/utils/__init__.py
Normal file
19
marking_hack/utils/base.py
Normal file
19
marking_hack/utils/base.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
|
||||
def all_subclasses(cls):
|
||||
return set(cls.__subclasses__()).union(
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)]
|
||||
)
|
||||
|
||||
|
||||
class SubclassesMixin:
|
||||
@classmethod
|
||||
def get_subclasses(cls):
|
||||
content_types = ContentType.objects.filter(app_label=cls._meta.app_label)
|
||||
models = [ct.model_class() for ct in content_types]
|
||||
return [
|
||||
model
|
||||
for model in models
|
||||
if (model is not None and issubclass(model, cls) and model is not cls)
|
||||
]
|
75
marking_hack/utils/files.py
Normal file
75
marking_hack/utils/files.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import os
|
||||
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
from marking_hack.users.models import User
|
||||
|
||||
|
||||
def crop_image(image_path: str, length: int = 500):
|
||||
"""Makes image's thumbnail bt given parameters. By default, crops to 500x500"""
|
||||
image = Image.open(image_path)
|
||||
blob = BytesIO()
|
||||
|
||||
try:
|
||||
if image.size[0] < image.size[1]:
|
||||
# The image is in portrait mode. Height is bigger than width.
|
||||
|
||||
# This makes the width fit the LENGTH in pixels while conserving the ration.
|
||||
resized_image = image.resize(
|
||||
(length, int(image.size[1] * (length / image.size[0])))
|
||||
)
|
||||
|
||||
# Amount of pixel to lose in total on the height of the image.
|
||||
required_loss = resized_image.size[1] - length
|
||||
|
||||
# Crop the height of the image so as to keep the center part.
|
||||
resized_image = resized_image.crop(
|
||||
box=(
|
||||
0,
|
||||
int(required_loss / 2),
|
||||
length,
|
||||
int(resized_image.size[1] - required_loss / 2),
|
||||
)
|
||||
)
|
||||
else:
|
||||
# This image is in landscape mode or already squared. The width is bigger than the heihgt.
|
||||
|
||||
# This makes the height fit the LENGTH in pixels while conserving the ration.
|
||||
resized_image = image.resize(
|
||||
(int(image.size[0] * (length / image.size[1])), length)
|
||||
)
|
||||
|
||||
# Amount of pixel to lose in total on the width of the image.
|
||||
required_loss = resized_image.size[0] - length
|
||||
|
||||
# Crop the width of the image so as to keep 1080 pixels of the center part.
|
||||
resized_image = resized_image.crop(
|
||||
box=(
|
||||
int(required_loss / 2),
|
||||
0,
|
||||
int(resized_image.size[0] - required_loss / 2),
|
||||
length,
|
||||
)
|
||||
)
|
||||
resized_image.save(blob, "PNG")
|
||||
except OSError:
|
||||
print("Can't crop")
|
||||
return blob
|
||||
|
||||
|
||||
def user_file_upload_mixin(instance, filename):
|
||||
"""stores user uploaded files at their folder in media dir"""
|
||||
username = ""
|
||||
if isinstance(instance, User):
|
||||
username = instance.username
|
||||
elif hasattr(instance, "user"):
|
||||
username = instance.user.username
|
||||
elif hasattr(instance, "creator"):
|
||||
username = instance.creator.username
|
||||
|
||||
return os.path.join(f"uploads/{username}/", filename)
|
||||
|
||||
|
||||
def get_filename(filename, request):
|
||||
return filename.upper()
|
15
marking_hack/utils/generators.py
Normal file
15
marking_hack/utils/generators.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
|
||||
def generate_charset(length: int) -> str:
|
||||
"""Generate a random string of characters of a given length."""
|
||||
return "".join(random.choice(string.ascii_letters) for _ in range(length))
|
||||
|
||||
|
||||
def _rand255():
|
||||
return random.randint(0, 255)
|
||||
|
||||
|
||||
def generate_hex_color() -> str:
|
||||
return f"#{_rand255():02X}{_rand255():02X}{_rand255():02X}"
|
27
marking_hack/utils/nums.py
Normal file
27
marking_hack/utils/nums.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
def number_to_base(n: int, b: int) -> list[int]:
|
||||
if n == 0:
|
||||
return [0]
|
||||
digits = []
|
||||
while n:
|
||||
digits.append(int(n % b))
|
||||
n //= b
|
||||
return digits[::-1]
|
||||
|
||||
|
||||
def to_base(n: int, base: list) -> str:
|
||||
if n == 0:
|
||||
return ""
|
||||
|
||||
b = len(base)
|
||||
res = ""
|
||||
|
||||
while n:
|
||||
res += base[int(n % b)]
|
||||
n //= b
|
||||
return res[::-1]
|
||||
|
||||
|
||||
def from_base(expr: str, base: int) -> int:
|
||||
return sum(
|
||||
[int(character) * base**index for index, character in enumerate(expr[::-1])]
|
||||
)
|
8
marking_hack/utils/string.py
Normal file
8
marking_hack/utils/string.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import re
|
||||
|
||||
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
|
||||
|
||||
|
||||
def cleanhtml(raw_html):
|
||||
cleantext = re.sub(CLEANR, "", raw_html)
|
||||
return cleantext
|
3000
poetry.lock
generated
Normal file
3000
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
production.yml
Normal file
63
production.yml
Normal file
|
@ -0,0 +1,63 @@
|
|||
version: '3'
|
||||
|
||||
volumes:
|
||||
production_postgres_data: {}
|
||||
production_postgres_data_backups: {}
|
||||
production_traefik: {}
|
||||
|
||||
services:
|
||||
django: &django
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./compose/production/django/Dockerfile
|
||||
image: marking_hack_production_django
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
env_file:
|
||||
- ./.envs/.production/.django
|
||||
- ./.envs/.production/.postgres
|
||||
command: /start
|
||||
|
||||
postgres:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./compose/production/postgres/Dockerfile
|
||||
image: marking_hack_production_postgres
|
||||
volumes:
|
||||
- production_postgres_data:/var/lib/postgresql/data
|
||||
- production_postgres_data_backups:/backups
|
||||
env_file:
|
||||
- ./.envs/.production/.postgres
|
||||
|
||||
traefik:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./compose/production/traefik/Dockerfile
|
||||
image: marking_hack_production_traefik
|
||||
depends_on:
|
||||
- django
|
||||
volumes:
|
||||
- production_traefik:/etc/traefik/acme
|
||||
ports:
|
||||
- "0.0.0.0:80:80"
|
||||
- "0.0.0.0:443:443"
|
||||
- "0.0.0.0:5555:5555"
|
||||
|
||||
redis:
|
||||
image: redis:6
|
||||
|
||||
celeryworker:
|
||||
<<: *django
|
||||
image: marking_hack_production_celeryworker
|
||||
command: /start-celeryworker
|
||||
|
||||
celerybeat:
|
||||
<<: *django
|
||||
image: marking_hack_production_celerybeat
|
||||
command: /start-celerybeat
|
||||
|
||||
flower:
|
||||
<<: *django
|
||||
image: marking_hack_production_flower
|
||||
command: /start-flower
|
56
pyproject.toml
Normal file
56
pyproject.toml
Normal file
|
@ -0,0 +1,56 @@
|
|||
[tool.poetry]
|
||||
name = "marking_hack"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Alexandr Karpov <alexandr.d.karpov@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
psycopg2 = "^2.9.5"
|
||||
pytz = "^2022.7"
|
||||
psutil = "^5.9.4"
|
||||
python-slugify = "^7.0.0"
|
||||
pillow = "^9.3.0"
|
||||
argon2-cffi = "^21.3.0"
|
||||
whitenoise = "^6.3.0"
|
||||
redis = "^4.4.0"
|
||||
celery = {extras = ["redis"], version = "^5.2.7"}
|
||||
django-celery-beat = "^2.4.0"
|
||||
flower = "^1.2.0"
|
||||
django = "^4.1.5"
|
||||
django-structlog = "^4.0.1"
|
||||
django-environ = "^0.9.0"
|
||||
django-model-utils = "^4.3.1"
|
||||
django-redis = "^5.2.0"
|
||||
djangorestframework = "^3.14.0"
|
||||
django-cors-headers = "^3.13.0"
|
||||
drf-spectacular = "^0.25.1"
|
||||
werkzeug = {extras = ["watchdog"], version = "^2.2.2"}
|
||||
ipdb = "^0.13.11"
|
||||
watchfiles = "^0.18.1"
|
||||
mypy = "^0.991"
|
||||
django-stubs = "^1.13.1"
|
||||
pytest = "^7.2.0"
|
||||
pytest-sugar = "^0.9.6"
|
||||
djangorestframework-stubs = "^1.8.0"
|
||||
flake8 = "^6.0.0"
|
||||
flake8-isort = "^6.0.0"
|
||||
coverage = "^7.0.3"
|
||||
black = "^22.12.0"
|
||||
pylint-django = "^2.5.3"
|
||||
pylint-celery = "^0.3"
|
||||
pre-commit = "^2.21.0"
|
||||
factory-boy = "^3.2.1"
|
||||
django-debug-toolbar = "^3.8.1"
|
||||
django-extensions = "^3.2.1"
|
||||
django-coverage-plugin = "^3.0.0"
|
||||
pytest-django = "^4.5.2"
|
||||
sentry-sdk = "^1.12.0"
|
||||
django-import-export = "^3.1.0"
|
||||
pandas = "^1.5.3"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
3
pytest.ini
Normal file
3
pytest.ini
Normal file
|
@ -0,0 +1,3 @@
|
|||
[pytest]
|
||||
addopts = --ds=config.settings.test --reuse-db
|
||||
python_files = tests.py test_*.py
|
40
setup.cfg
Normal file
40
setup.cfg
Normal file
|
@ -0,0 +1,40 @@
|
|||
[flake8]
|
||||
max-line-length = 120
|
||||
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 120
|
||||
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
|
||||
|
||||
[isort]
|
||||
line_length = 88
|
||||
known_first_party = marking_hack,config
|
||||
multi_line_output = 3
|
||||
default_section = THIRDPARTY
|
||||
skip = venv/
|
||||
skip_glob = **/migrations/*.py
|
||||
include_trailing_comma = true
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = true
|
||||
|
||||
[mypy]
|
||||
python_version = 3.10
|
||||
check_untyped_defs = True
|
||||
ignore_missing_imports = True
|
||||
warn_unused_ignores = True
|
||||
warn_redundant_casts = True
|
||||
warn_unused_configs = True
|
||||
plugins = mypy_django_plugin.main, mypy_drf_plugin.main
|
||||
|
||||
[mypy.plugins.django-stubs]
|
||||
django_settings_module = config.settings.test
|
||||
|
||||
[mypy-*.migrations.*]
|
||||
# Django migrations should not produce any errors:
|
||||
ignore_errors = True
|
||||
|
||||
[coverage:run]
|
||||
include = marking_hack/**
|
||||
omit = *migrations*, *tests*
|
||||
plugins =
|
||||
django_coverage_plugin
|
96
utility/install_os_dependencies.sh
Executable file
96
utility/install_os_dependencies.sh
Executable file
|
@ -0,0 +1,96 @@
|
|||
#!/bin/bash
|
||||
|
||||
WORK_DIR="$(dirname "$0")"
|
||||
DISTRO_NAME=$(lsb_release -sc)
|
||||
OS_REQUIREMENTS_FILENAME="requirements-$DISTRO_NAME.apt"
|
||||
|
||||
cd $WORK_DIR
|
||||
|
||||
# Check if a requirements file exist for the current distribution.
|
||||
if [ ! -r "$OS_REQUIREMENTS_FILENAME" ]; then
|
||||
cat <<-EOF >&2
|
||||
There is no requirements file for your distribution.
|
||||
You can see one of the files listed below to help search the equivalent package in your system:
|
||||
$(find ./ -name "requirements-*.apt" -printf " - %f\n")
|
||||
EOF
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Handle call with wrong command
|
||||
function wrong_command()
|
||||
{
|
||||
echo "${0##*/} - unknown command: '${1}'" >&2
|
||||
usage_message
|
||||
}
|
||||
|
||||
# Print help / script usage
|
||||
function usage_message()
|
||||
{
|
||||
cat <<-EOF
|
||||
Usage: $WORK_DIR/${0##*/} <command>
|
||||
Available commands are:
|
||||
list Print a list of all packages defined on ${OS_REQUIREMENTS_FILENAME} file
|
||||
help Print this help
|
||||
|
||||
Commands that require superuser permission:
|
||||
install Install packages defined on ${OS_REQUIREMENTS_FILENAME} file. Note: This
|
||||
does not upgrade the packages already installed for new versions, even if
|
||||
new version is available in the repository.
|
||||
upgrade Same that install, but upgrade the already installed packages, if new
|
||||
version is available.
|
||||
EOF
|
||||
}
|
||||
|
||||
# Read the requirements.apt file, and remove comments and blank lines
|
||||
function list_packages(){
|
||||
grep -v "#" "${OS_REQUIREMENTS_FILENAME}" | grep -v "^$";
|
||||
}
|
||||
|
||||
function install_packages()
|
||||
{
|
||||
list_packages | xargs apt-get --no-upgrade install -y;
|
||||
}
|
||||
|
||||
function upgrade_packages()
|
||||
{
|
||||
list_packages | xargs apt-get install -y;
|
||||
}
|
||||
|
||||
function install_or_upgrade()
|
||||
{
|
||||
P=${1}
|
||||
PARAN=${P:-"install"}
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
cat <<-EOF >&2
|
||||
You must run this script with root privilege
|
||||
Please do:
|
||||
sudo $WORK_DIR/${0##*/} $PARAN
|
||||
EOF
|
||||
exit 1
|
||||
else
|
||||
|
||||
apt-get update
|
||||
|
||||
# Install the basic compilation dependencies and other required libraries of this project
|
||||
if [ "$PARAN" == "install" ]; then
|
||||
install_packages;
|
||||
else
|
||||
upgrade_packages;
|
||||
fi
|
||||
|
||||
# cleaning downloaded packages from apt-get cache
|
||||
apt-get clean
|
||||
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Handle command argument
|
||||
case "$1" in
|
||||
install) install_or_upgrade;;
|
||||
upgrade) install_or_upgrade "upgrade";;
|
||||
list) list_packages;;
|
||||
help|"") usage_message;;
|
||||
*) wrong_command "$1";;
|
||||
esac
|
37
utility/install_python_dependencies.sh
Executable file
37
utility/install_python_dependencies.sh
Executable file
|
@ -0,0 +1,37 @@
|
|||
#!/bin/bash
|
||||
|
||||
WORK_DIR="$(dirname "$0")"
|
||||
PROJECT_DIR="$(dirname "$WORK_DIR")"
|
||||
|
||||
pip --version >/dev/null 2>&1 || {
|
||||
echo >&2 -e "\npip is required but it's not installed."
|
||||
echo >&2 -e "You can install it by running the following command:\n"
|
||||
echo >&2 "wget https://bootstrap.pypa.io/get-pip.py --output-document=get-pip.py; chmod +x get-pip.py; sudo -H python3 get-pip.py"
|
||||
echo >&2 -e "\n"
|
||||
echo >&2 -e "\nFor more information, see pip documentation: https://pip.pypa.io/en/latest/"
|
||||
exit 1;
|
||||
}
|
||||
|
||||
virtualenv --version >/dev/null 2>&1 || {
|
||||
echo >&2 -e "\nvirtualenv is required but it's not installed."
|
||||
echo >&2 -e "You can install it by running the following command:\n"
|
||||
echo >&2 "sudo -H pip3 install virtualenv"
|
||||
echo >&2 -e "\n"
|
||||
echo >&2 -e "\nFor more information, see virtualenv documentation: https://virtualenv.pypa.io/en/latest/"
|
||||
exit 1;
|
||||
}
|
||||
|
||||
if [ -z "$VIRTUAL_ENV" ]; then
|
||||
echo >&2 -e "\nYou need activate a virtualenv first"
|
||||
echo >&2 -e 'If you do not have a virtualenv created, run the following command to create and automatically activate a new virtualenv named "venv" on current folder:\n'
|
||||
echo >&2 -e "virtualenv venv --python=\`which python3\`"
|
||||
echo >&2 -e "\nTo leave/disable the currently active virtualenv, run the following command:\n"
|
||||
echo >&2 "deactivate"
|
||||
echo >&2 -e "\nTo activate the virtualenv again, run the following command:\n"
|
||||
echo >&2 "source venv/bin/activate"
|
||||
echo >&2 -e "\nFor more information, see virtualenv documentation: https://virtualenv.pypa.io/en/latest/"
|
||||
echo >&2 -e "\n"
|
||||
exit 1;
|
||||
else
|
||||
pip install -r $PROJECT_DIR/requirements/local.txt
|
||||
fi
|
23
utility/requirements-bionic.apt
Normal file
23
utility/requirements-bionic.apt
Normal file
|
@ -0,0 +1,23 @@
|
|||
##basic build dependencies of various Django apps for Ubuntu Bionic 18.04
|
||||
#build-essential metapackage install: make, gcc, g++,
|
||||
build-essential
|
||||
#required to translate
|
||||
gettext
|
||||
python3-dev
|
||||
|
||||
##shared dependencies of:
|
||||
##Pillow, pylibmc
|
||||
zlib1g-dev
|
||||
|
||||
##Postgresql and psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
##Pillow dependencies
|
||||
libtiff5-dev
|
||||
libjpeg8-dev
|
||||
libfreetype6-dev
|
||||
liblcms2-dev
|
||||
libwebp-dev
|
||||
|
||||
##django-extensions
|
||||
libgraphviz-dev
|
23
utility/requirements-bullseye.apt
Normal file
23
utility/requirements-bullseye.apt
Normal file
|
@ -0,0 +1,23 @@
|
|||
##basic build dependencies of various Django apps for Debian Bullseye 11.x
|
||||
#build-essential metapackage install: make, gcc, g++,
|
||||
build-essential
|
||||
#required to translate
|
||||
gettext
|
||||
python3-dev
|
||||
|
||||
##shared dependencies of:
|
||||
##Pillow, pylibmc
|
||||
zlib1g-dev
|
||||
|
||||
##Postgresql and psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
##Pillow dependencies
|
||||
libtiff5-dev
|
||||
libjpeg62-turbo-dev
|
||||
libfreetype6-dev
|
||||
liblcms2-dev
|
||||
libwebp-dev
|
||||
|
||||
##django-extensions
|
||||
libgraphviz-dev
|
23
utility/requirements-buster.apt
Normal file
23
utility/requirements-buster.apt
Normal file
|
@ -0,0 +1,23 @@
|
|||
##basic build dependencies of various Django apps for Debian Jessie 10.x
|
||||
#build-essential metapackage install: make, gcc, g++,
|
||||
build-essential
|
||||
#required to translate
|
||||
gettext
|
||||
python3-dev
|
||||
|
||||
##shared dependencies of:
|
||||
##Pillow, pylibmc
|
||||
zlib1g-dev
|
||||
|
||||
##Postgresql and psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
##Pillow dependencies
|
||||
libtiff5-dev
|
||||
libjpeg62-turbo-dev
|
||||
libfreetype6-dev
|
||||
liblcms2-dev
|
||||
libwebp-dev
|
||||
|
||||
##django-extensions
|
||||
libgraphviz-dev
|
23
utility/requirements-focal.apt
Normal file
23
utility/requirements-focal.apt
Normal file
|
@ -0,0 +1,23 @@
|
|||
##basic build dependencies of various Django apps for Ubuntu Focal 20.04
|
||||
#build-essential metapackage install: make, gcc, g++,
|
||||
build-essential
|
||||
#required to translate
|
||||
gettext
|
||||
python3-dev
|
||||
|
||||
##shared dependencies of:
|
||||
##Pillow, pylibmc
|
||||
zlib1g-dev
|
||||
|
||||
##Postgresql and psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
##Pillow dependencies
|
||||
libtiff5-dev
|
||||
libjpeg8-dev
|
||||
libfreetype6-dev
|
||||
liblcms2-dev
|
||||
libwebp-dev
|
||||
|
||||
##django-extensions
|
||||
graphviz-dev
|
23
utility/requirements-jammy.apt
Normal file
23
utility/requirements-jammy.apt
Normal file
|
@ -0,0 +1,23 @@
|
|||
##basic build dependencies of various Django apps for Ubuntu Jammy 22.04
|
||||
#build-essential metapackage install: make, gcc, g++,
|
||||
build-essential
|
||||
#required to translate
|
||||
gettext
|
||||
python3-dev
|
||||
|
||||
##shared dependencies of:
|
||||
##Pillow, pylibmc
|
||||
zlib1g-dev
|
||||
|
||||
##Postgresql and psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
##Pillow dependencies
|
||||
libtiff5-dev
|
||||
libjpeg8-dev
|
||||
libfreetype6-dev
|
||||
liblcms2-dev
|
||||
libwebp-dev
|
||||
|
||||
##django-extensions
|
||||
graphviz-dev
|
23
utility/requirements-jessie.apt
Normal file
23
utility/requirements-jessie.apt
Normal file
|
@ -0,0 +1,23 @@
|
|||
##basic build dependencies of various Django apps for Debian Jessie 8.x
|
||||
#build-essential metapackage install: make, gcc, g++,
|
||||
build-essential
|
||||
#required to translate
|
||||
gettext
|
||||
python3-dev
|
||||
|
||||
##shared dependencies of:
|
||||
##Pillow, pylibmc
|
||||
zlib1g-dev
|
||||
|
||||
##Postgresql and psycopg2 dependencies
|
||||
libpq-dev
|
||||
|
||||
##Pillow dependencies
|
||||
libtiff5-dev
|
||||
libjpeg62-turbo-dev
|
||||
libfreetype6-dev
|
||||
liblcms2-dev
|
||||
libwebp-dev
|
||||
|
||||
##django-extensions
|
||||
graphviz-dev
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user