initial commit

This commit is contained in:
Alexander Karpov 2023-03-26 14:16:18 +03:00
commit 137caccebf
103 changed files with 6457 additions and 0 deletions

6
.env.template Normal file
View 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
View 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
View 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.

View 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"]

View 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'

View 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}\""

View 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'

View 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

View 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

View 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

View 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}"

View 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

View 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 "$@"

View 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

View 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

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
BACKUP_DIR_PATH='/backups'
BACKUP_FILE_PREFIX='backup'

View File

@ -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
}

View 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: ${@}"
}

View 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
}

View 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}'."

View 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}"

View 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."

View 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

View 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
View 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
View 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
View 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()

View File

337
config/settings/base.py Normal file
View 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
View 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...
# ------------------------------------------------------------------------------

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
Translations
============
Translations will be placed in this folder when running::
python manage.py makemessages

0
logs/.gitkeep Normal file
View File

31
manage.py Executable file
View 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
View 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(".")
)

View File

View 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

View 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
View 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()

View File

View File

View 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())],
)
]

View File

@ -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",
),
)
]

View File

@ -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)]

View File

@ -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",
},
),
]

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

View 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)

View 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()
),
]

View 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)

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MarketConfig(AppConfig):
name = "marking_hack.market"

View 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",
),
),
],
),
]

View 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
),
),
]

View File

@ -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),
),
]

View 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(),
),
]

View File

@ -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),
),
]

View File

@ -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",
),
),
],
),
]

View 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",
),
),
],
),
]

View File

@ -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",
),
]

View File

@ -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"]},
),
]

View File

@ -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,
),
]

View 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"]

View 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")

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

View File

View 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"}
}

View 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)

View 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

View 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()),
],
),
]

View 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

View File

@ -0,0 +1 @@

View File

View 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)
]

View 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()

View 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}"

View 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])]
)

View 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

File diff suppressed because it is too large Load Diff

63
production.yml Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
[pytest]
addopts = --ds=config.settings.test --reuse-db
python_files = tests.py test_*.py

40
setup.cfg Normal file
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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