mirror of
https://github.com/mistakes-23/backend.git
synced 2024-11-22 00:06:33 +03:00
Initial commit
This commit is contained in:
commit
e35215f22a
10
.dockerignore
Normal file
10
.dockerignore
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
orconfig
|
||||||
|
.gitattributes
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.idea
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
.readthedocs.yml
|
||||||
|
.travis.yml
|
||||||
|
venv
|
27
.editorconfig
Normal file
27
.editorconfig
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{py,rst,ini}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{html,css,scss,json,yml,xml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[nginx.conf]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
4
.env
Normal file
4
.env
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/dock_checker
|
||||||
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||||
|
REDIS_URL=redis://localhost:6379/1
|
||||||
|
USE_DOCKER=no
|
4
.env.template
Normal file
4
.env.template
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/dock_checker
|
||||||
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||||
|
REDIS_URL=redis://localhost:6379/1
|
||||||
|
USE_DOCKER=no
|
25
.gitattributes
vendored
Normal file
25
.gitattributes
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Source files
|
||||||
|
# ============
|
||||||
|
*.pxd text diff=python
|
||||||
|
*.py text diff=python
|
||||||
|
*.py3 text diff=python
|
||||||
|
*.pyw text diff=python
|
||||||
|
*.pyx text diff=python
|
||||||
|
*.pyz text diff=python
|
||||||
|
*.pyi text diff=python
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
# ============
|
||||||
|
*.db binary
|
||||||
|
*.p binary
|
||||||
|
*.pkl binary
|
||||||
|
*.pickle binary
|
||||||
|
*.pyc binary export-ignore
|
||||||
|
*.pyo binary export-ignore
|
||||||
|
*.pyd binary
|
||||||
|
|
||||||
|
# Jupyter notebook
|
||||||
|
*.ipynb text eol=lf
|
||||||
|
|
||||||
|
# ignore static
|
||||||
|
dock_checker/static/** linguist-vendored
|
331
.gitignore
vendored
Normal file
331
.gitignore
vendored
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
### 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/dock_checker.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
|
||||||
|
dock_checker/media/
|
||||||
|
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
.ipython/
|
39
.pre-commit-config.yaml
Normal file
39
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
exclude: "^docs/|/migrations/"
|
||||||
|
default_stages: [commit]
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.4.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.3.1
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [--py310-plus]
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.12.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
|
||||||
|
- repo: https://github.com/PyCQA/isort
|
||||||
|
rev: 5.11.4
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
args: ["--config=setup.cfg"]
|
||||||
|
additional_dependencies: [flake8-isort]
|
||||||
|
|
||||||
|
# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date
|
||||||
|
ci:
|
||||||
|
autoupdate_schedule: weekly
|
||||||
|
skip: []
|
||||||
|
submodules: false
|
14
.pylintrc
Normal file
14
.pylintrc
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[MASTER]
|
||||||
|
load-plugins=pylint_django, pylint_celery
|
||||||
|
django-settings-module=config.settings.local
|
||||||
|
[FORMAT]
|
||||||
|
max-line-length=120
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=missing-docstring,invalid-name
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
max-parents=13
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete
|
40
README.md
Normal file
40
README.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Capital Dock Checker
|
||||||
|
|
||||||
|
Detection and comparison with the reference name of the capital construction object in the project documentation
|
||||||
|
## Basic Commands
|
||||||
|
|
||||||
|
### Runserver
|
||||||
|
|
||||||
|
$ ./manage.py runserver_plus
|
||||||
|
|
||||||
|
### Type checks
|
||||||
|
|
||||||
|
Running type checks with mypy:
|
||||||
|
|
||||||
|
$ mypy dock_checker
|
||||||
|
|
||||||
|
#### Running tests with pytest
|
||||||
|
|
||||||
|
$ pytest
|
||||||
|
|
||||||
|
### Setting Up Your Users
|
||||||
|
|
||||||
|
- To create a **superuser account**, use this command:
|
||||||
|
|
||||||
|
$ python manage.py createsuperuser
|
||||||
|
|
||||||
|
### Celery
|
||||||
|
|
||||||
|
This app comes with Celery.
|
||||||
|
|
||||||
|
To run a celery worker:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
cd dock_checker
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
made with [cookiecutter-django](https://github.com/Alexander-D-Karpov/cookiecutter-django)
|
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@dev2.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(`dev2.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(`dev2.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("dock_checker.processor.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("dock_checker")
|
||||||
|
|
||||||
|
# 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
350
config/settings/base.py
Normal file
350
config/settings/base.py
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
# dock_checker/
|
||||||
|
APPS_DIR = ROOT_DIR / "dock_checker"
|
||||||
|
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 = [
|
||||||
|
"dock_checker.users",
|
||||||
|
"dock_checker.processor",
|
||||||
|
]
|
||||||
|
# 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": "dock_checker.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@dev2.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 = 20 * 60
|
||||||
|
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-soft-time-limit
|
||||||
|
CELERY_TASK_SOFT_TIME_LIMIT = 10 * 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": "Capital Dock Checker API",
|
||||||
|
"DESCRIPTION": "Documentation of API endpoints of Capital Dock Checker",
|
||||||
|
"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"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
CACHE_TTL = 60 * 1500
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": "redis://127.0.0.1:6379/1",
|
||||||
|
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
||||||
|
"KEY_PREFIX": "dock_checker",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||||
|
SESSION_CACHE_ALIAS = "default"
|
51
config/settings/local.py
Normal file
51
config/settings/local.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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="dmvHcXHczWrqiCensgEL5buzNKpjzZk6YvZRUa6ALnxEJUWdiCeBbyYp19E4iEJJ",
|
||||||
|
)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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=["dev2.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://dev2.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="NxhpmQEDiN98ffqCJXUzEImtr0vUoxPYMOVinwbD7Yk7HyzZ4k4LssOuSlcrO5mW",
|
||||||
|
)
|
||||||
|
# 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...
|
||||||
|
# ------------------------------------------------------------------------------
|
36
config/urls.py
Normal file
36
config/urls.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
from django.views import defaults as default_views
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
|
from rest_framework.authtoken.views import obtain_auth_token
|
||||||
|
|
||||||
|
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 Capital Dock Checker 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
|
||||||
|
# dock_checker directory.
|
||||||
|
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||||
|
sys.path.append(str(ROOT_DIR / "dock_checker"))
|
||||||
|
# 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)
|
5
dock_checker/__init__.py
Normal file
5
dock_checker/__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
dock_checker/common/__init__.py
Normal file
0
dock_checker/common/__init__.py
Normal file
19
dock_checker/common/api.py
Normal file
19
dock_checker/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
|
25
dock_checker/common/cache.py
Normal file
25
dock_checker/common/cache.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
def incr_key(key, value, timeout=None):
|
||||||
|
return cache.incr(key, delta=value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_key(key, value, timeout=None):
|
||||||
|
return cache.set(key, value, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def add_key(key, value, timeout=None):
|
||||||
|
return cache.add(key, value, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_key_exists(key):
|
||||||
|
return cache.get(key) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def get_key(key):
|
||||||
|
return cache.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_key(key):
|
||||||
|
return cache.delete(key)
|
18
dock_checker/common/tasks.py
Normal file
18
dock_checker/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 dock_checker.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
dock_checker/conftest.py
Normal file
14
dock_checker/conftest.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dock_checker.users.models import User
|
||||||
|
from dock_checker.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
dock_checker/contrib/__init__.py
Normal file
0
dock_checker/contrib/__init__.py
Normal file
0
dock_checker/contrib/sites/__init__.py
Normal file
0
dock_checker/contrib/sites/__init__.py
Normal file
42
dock_checker/contrib/sites/migrations/0001_initial.py
Normal file
42
dock_checker/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,
|
||||||
|
"dock_checker",
|
||||||
|
"Detection and comparison with the reference name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
dock_checker/contrib/sites/migrations/__init__.py
Normal file
0
dock_checker/contrib/sites/migrations/__init__.py
Normal file
0
dock_checker/processor/__init__.py
Normal file
0
dock_checker/processor/__init__.py
Normal file
3
dock_checker/processor/admin.py
Normal file
3
dock_checker/processor/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
0
dock_checker/processor/api/__init__.py
Normal file
0
dock_checker/processor/api/__init__.py
Normal file
68
dock_checker/processor/api/serializers.py
Normal file
68
dock_checker/processor/api/serializers.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dock_checker.processor.models import File, FileImage
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSerializer(serializers.Serializer):
|
||||||
|
processed = serializers.IntegerField()
|
||||||
|
total = serializers.IntegerField()
|
||||||
|
features_loaded = serializers.BooleanField()
|
||||||
|
error = serializers.BooleanField()
|
||||||
|
error_description = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class FileImageSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FileImage
|
||||||
|
fields = ["order", "image"]
|
||||||
|
|
||||||
|
|
||||||
|
class FileSerializer(serializers.ModelSerializer):
|
||||||
|
status = serializers.SerializerMethodField(method_name="get_status")
|
||||||
|
file_url = serializers.SerializerMethodField(method_name="get_file_url")
|
||||||
|
preview = serializers.SerializerMethodField(method_name="get_preview")
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.URLField)
|
||||||
|
def get_status(self, obj):
|
||||||
|
return reverse("api:status", kwargs={"pk": obj.id})
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.FileField)
|
||||||
|
def get_preview(self, obj):
|
||||||
|
if obj.images.exists():
|
||||||
|
return obj.images.first().image.url
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.URLField)
|
||||||
|
def get_file_url(self, obj):
|
||||||
|
return reverse("api:file", kwargs={"pk": obj.id})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = File
|
||||||
|
fields = ["name", "ideal_title", "file", "file_url", "preview", "status"]
|
||||||
|
extra_kwargs = {
|
||||||
|
"ideal_title": {"read_only": True},
|
||||||
|
"status": {"read_only": True},
|
||||||
|
"name": {"read_only": True},
|
||||||
|
"preview": {"read_only": True},
|
||||||
|
"file_url": {"read_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
obj = File.objects.create(
|
||||||
|
file=validated_data["file"], name=validated_data["file"].name
|
||||||
|
)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class FullFileSerializer(FileSerializer):
|
||||||
|
images = FileImageSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = File
|
||||||
|
fields = ["name", "ideal_title", "file", "images", "text_locations"]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFileTitleSerializer(serializers.Serializer):
|
||||||
|
title = serializers.CharField()
|
17
dock_checker/processor/api/urls.py
Normal file
17
dock_checker/processor/api/urls.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from dock_checker.processor.api.views import (
|
||||||
|
CreateFileApiView,
|
||||||
|
RetrieveTaskApiView,
|
||||||
|
ListFileApiView,
|
||||||
|
RetrieveFileApiView,
|
||||||
|
UpdateFileTitleApiView,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("list", ListFileApiView.as_view()),
|
||||||
|
path("upload/", CreateFileApiView.as_view()),
|
||||||
|
path("status/<str:pk>", RetrieveTaskApiView.as_view(), name="status"),
|
||||||
|
path("file/<str:pk>", RetrieveFileApiView.as_view(), name="file"),
|
||||||
|
path("file/<str:pk>/update/", UpdateFileTitleApiView.as_view()),
|
||||||
|
]
|
56
dock_checker/processor/api/views.py
Normal file
56
dock_checker/processor/api/views.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.parsers import FormParser, MultiPartParser
|
||||||
|
from rest_framework.generics import (
|
||||||
|
GenericAPIView,
|
||||||
|
CreateAPIView,
|
||||||
|
ListAPIView,
|
||||||
|
RetrieveAPIView,
|
||||||
|
get_object_or_404,
|
||||||
|
)
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from dock_checker.processor.api.serializers import (
|
||||||
|
TaskSerializer,
|
||||||
|
FileSerializer,
|
||||||
|
FullFileSerializer,
|
||||||
|
UpdateFileTitleSerializer,
|
||||||
|
)
|
||||||
|
from dock_checker.processor.models import File
|
||||||
|
from dock_checker.processor.services import get_task_status
|
||||||
|
from dock_checker.processor.tasks import update_pdf_features
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveTaskApiView(GenericAPIView):
|
||||||
|
serializer_class = TaskSerializer
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
data = get_task_status(pk)
|
||||||
|
return Response(data=data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFileTitleApiView(GenericAPIView):
|
||||||
|
serializer_class = UpdateFileTitleSerializer
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
file = get_object_or_404(File, pk=pk)
|
||||||
|
update_pdf_features.apply_async(
|
||||||
|
kwargs={"pk": file.pk, "target": request.data["title"]},
|
||||||
|
countdown=1,
|
||||||
|
)
|
||||||
|
data = FileSerializer().to_representation(file)
|
||||||
|
return Response(data=data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveFileApiView(RetrieveAPIView):
|
||||||
|
queryset = File.objects.all()
|
||||||
|
serializer_class = FullFileSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFileApiView(CreateAPIView):
|
||||||
|
parser_classes = [FormParser, MultiPartParser]
|
||||||
|
serializer_class = FileSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ListFileApiView(ListAPIView):
|
||||||
|
serializer_class = FileSerializer
|
||||||
|
queryset = File.objects.all()
|
9
dock_checker/processor/apps.py
Normal file
9
dock_checker/processor/apps.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessorConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "dock_checker.processor"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import dock_checker.processor.signals
|
50
dock_checker/processor/migrations/0001_initial.py
Normal file
50
dock_checker/processor/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 08:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="File",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("uploaded", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("file", models.FileField(upload_to="uploads/")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("uploaded",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Task",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("processed", models.IntegerField(default=0)),
|
||||||
|
("total", models.IntegerField(default=0)),
|
||||||
|
("next_url", models.URLField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 13:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("processor", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="file",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="file",
|
||||||
|
name="preview",
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to="preview/"),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 14:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("processor", "0002_file_name_file_preview"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="file",
|
||||||
|
options={"ordering": ("-uploaded",)},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FileImage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField()),
|
||||||
|
("image", models.ImageField(upload_to="pages/")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("order",),
|
||||||
|
"unique_together": {("order", "image")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 14:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("processor", "0003_alter_file_options_fileimage"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="fileimage",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="fileimage",
|
||||||
|
name="file",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=1,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="images",
|
||||||
|
to="processor.file",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="fileimage",
|
||||||
|
unique_together={("order", "file")},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 14:57
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("processor", "0004_alter_fileimage_unique_together_fileimage_file_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="file",
|
||||||
|
name="preview",
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 17:57
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("processor", "0005_remove_file_preview"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="fileimage",
|
||||||
|
name="text",
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="file",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
upload_to="uploads/",
|
||||||
|
validators=[
|
||||||
|
django.core.validators.FileExtensionValidator(
|
||||||
|
allowed_extensions=["pdf"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-06-24 22:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("processor", "0006_fileimage_text_alter_file_file"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="Task",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="fileimage",
|
||||||
|
name="text",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="file",
|
||||||
|
name="ideal_title",
|
||||||
|
field=models.CharField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="file",
|
||||||
|
name="text_locations",
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
0
dock_checker/processor/migrations/__init__.py
Normal file
0
dock_checker/processor/migrations/__init__.py
Normal file
29
dock_checker/processor/models.py
Normal file
29
dock_checker/processor/models.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.core.validators import FileExtensionValidator
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class File(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
name = models.CharField(null=True, blank=True, max_length=500)
|
||||||
|
ideal_title = models.CharField(null=True, blank=True, max_length=500)
|
||||||
|
text_locations = models.JSONField(default=dict)
|
||||||
|
uploaded = models.DateTimeField(auto_now_add=True)
|
||||||
|
file = models.FileField(
|
||||||
|
upload_to="uploads/",
|
||||||
|
validators=[FileExtensionValidator(allowed_extensions=["pdf"])],
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("-uploaded",)
|
||||||
|
|
||||||
|
|
||||||
|
class FileImage(models.Model):
|
||||||
|
file = models.ForeignKey("File", related_name="images", on_delete=models.CASCADE)
|
||||||
|
order = models.IntegerField()
|
||||||
|
image = models.ImageField(upload_to="pages/")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("order", "file")
|
||||||
|
ordering = ("order",)
|
19
dock_checker/processor/services.py
Normal file
19
dock_checker/processor/services.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from django.core.cache import cache
|
||||||
|
from rest_framework.exceptions import NotFound
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_status(pk: str) -> dict:
|
||||||
|
if cache.get(f"{pk}-processed") is None:
|
||||||
|
raise NotFound("given task does not exist")
|
||||||
|
created = cache.get_or_set(f"{pk}-processed", 0)
|
||||||
|
total = cache.get_or_set(f"{pk}-total", 0)
|
||||||
|
features_loaded = cache.get_or_set(f"{pk}-features_loaded", False)
|
||||||
|
error = cache.get_or_set(f"{pk}-error", False)
|
||||||
|
error_description = cache.get_or_set(f"{pk}-error_description", "")
|
||||||
|
return {
|
||||||
|
"processed": created,
|
||||||
|
"total": total,
|
||||||
|
"features_loaded": features_loaded,
|
||||||
|
"error": error,
|
||||||
|
"error_description": error_description,
|
||||||
|
}
|
17
dock_checker/processor/signals.py
Normal file
17
dock_checker/processor/signals.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from dock_checker.processor.models import File
|
||||||
|
from .tasks import process_pdf
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=File)
|
||||||
|
def file_on_create(sender, instance: File, **kwargs):
|
||||||
|
if instance.id and not instance.text_locations:
|
||||||
|
cache.set(f"{instance.id}-processed", 0)
|
||||||
|
cache.set(f"{instance.id}-total", 1)
|
||||||
|
process_pdf.apply_async(
|
||||||
|
kwargs={"pk": instance.pk},
|
||||||
|
countdown=1,
|
||||||
|
)
|
122
dock_checker/processor/tasks.py
Normal file
122
dock_checker/processor/tasks.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
from django.core.files import File
|
||||||
|
from pdf2image import convert_from_path
|
||||||
|
from django.core.cache import cache
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
|
from dock_checker.processor.models import File as FileModel, FileImage
|
||||||
|
from ml.main import (
|
||||||
|
extract_test_features,
|
||||||
|
inference_models,
|
||||||
|
create_test_features,
|
||||||
|
get_matches,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def process_pdf(pk: str):
|
||||||
|
file = FileModel.objects.get(pk=pk)
|
||||||
|
reader = PdfReader(file.file.path)
|
||||||
|
cache.set(f"{pk}-total", len(reader.pages))
|
||||||
|
cache.set(f"{pk}-features_loaded", False)
|
||||||
|
cache.set(f"{pk}-processed", 1)
|
||||||
|
extract_pdf_features.apply_async(kwargs={"pk": pk})
|
||||||
|
split_pdf_into_images.apply_async(kwargs={"pk": pk})
|
||||||
|
load_pdf.apply_async(kwargs={"pk": pk})
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def extract_pdf_features(pk: str):
|
||||||
|
file = FileModel.objects.get(pk=pk)
|
||||||
|
data, status = extract_test_features(file.file.path)
|
||||||
|
if not status:
|
||||||
|
print(data)
|
||||||
|
cache.set(f"{pk}-error", True)
|
||||||
|
cache.set(f"{pk}-error_description", data)
|
||||||
|
else:
|
||||||
|
# TODO: create new file for download
|
||||||
|
data = create_test_features(data)
|
||||||
|
_, target = inference_models("ml/checkpoints/models.pkl", data)
|
||||||
|
text_locations = get_matches(file.file.path, target)
|
||||||
|
file.ideal_title = target
|
||||||
|
file.text_locations = text_locations
|
||||||
|
file.save()
|
||||||
|
cache.set(f"{pk}-features_loaded", True)
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def update_pdf_features(pk: str, target: str):
|
||||||
|
file = FileModel.objects.get(pk=pk)
|
||||||
|
cache.set(f"{pk}-features_loaded", False)
|
||||||
|
data, status = extract_test_features(file.file.path)
|
||||||
|
if not status:
|
||||||
|
print(data)
|
||||||
|
cache.set(f"{pk}-error", True)
|
||||||
|
cache.set(f"{pk}-error_description", data)
|
||||||
|
else:
|
||||||
|
# TODO: create new file for download
|
||||||
|
text_locations = get_matches(file.file.path, target)
|
||||||
|
file.ideal_title = target
|
||||||
|
file.text_locations = text_locations
|
||||||
|
file.save()
|
||||||
|
cache.set(f"{pk}-features_loaded", True)
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def split_pdf_into_images(pk: str):
|
||||||
|
file = FileModel.objects.get(pk=pk)
|
||||||
|
os.mkdir(str(pk))
|
||||||
|
convert_from_path(file.file.path, output_folder=str(pk), paths_only=True, fmt="png")
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
def get_file(pk: str, number: int):
|
||||||
|
res = {}
|
||||||
|
for e in os.listdir(str(pk)):
|
||||||
|
p = int(e.split("-")[-1].split(".")[0])
|
||||||
|
res[p] = e
|
||||||
|
|
||||||
|
if number == len(os.listdir(str(pk))):
|
||||||
|
sleep(1)
|
||||||
|
return res[number]
|
||||||
|
if number + 1 in res:
|
||||||
|
return res[number]
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def load_pdf(pk: str):
|
||||||
|
file = FileModel.objects.get(pk=pk)
|
||||||
|
if not os.path.isdir(str(pk)):
|
||||||
|
load_pdf.apply_async(
|
||||||
|
kwargs={"pk": pk},
|
||||||
|
countdown=1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(cache.get(f"{pk}-processed"), cache.get(f"{pk}-total") + 1):
|
||||||
|
cache.set(f"{pk}-processed", i)
|
||||||
|
f_path = get_file(pk, i)
|
||||||
|
if f_path:
|
||||||
|
with open(str(pk) + "/" + f_path, "rb") as f:
|
||||||
|
FileImage.objects.create(
|
||||||
|
image=File(f, name=f"{pk}-{i}.png"), file=file, order=i
|
||||||
|
)
|
||||||
|
print(i)
|
||||||
|
else:
|
||||||
|
load_pdf.apply_async(
|
||||||
|
kwargs={"pk": pk},
|
||||||
|
countdown=1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
shutil.rmtree(str(pk))
|
||||||
|
return pk
|
0
dock_checker/static/.gitkeep
vendored
Normal file
0
dock_checker/static/.gitkeep
vendored
Normal file
0
dock_checker/users/__init__.py
Normal file
0
dock_checker/users/__init__.py
Normal file
0
dock_checker/users/admin.py
Normal file
0
dock_checker/users/admin.py
Normal file
14
dock_checker/users/api/serializers.py
Normal file
14
dock_checker/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
dock_checker/users/api/views.py
Normal file
25
dock_checker/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
dock_checker/users/apps.py
Normal file
13
dock_checker/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 = "dock_checker.users"
|
||||||
|
verbose_name = _("Users")
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
try:
|
||||||
|
import dock_checker.users.signals # noqa F401
|
||||||
|
except ImportError:
|
||||||
|
pass
|
118
dock_checker/users/migrations/0001_initial.py
Normal file
118
dock_checker/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
dock_checker/users/migrations/__init__.py
Normal file
0
dock_checker/users/migrations/__init__.py
Normal file
13
dock_checker/users/models.py
Normal file
13
dock_checker/users/models.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
"""
|
||||||
|
Default custom user model for Capital Dock Checker.
|
||||||
|
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
dock_checker/users/views.py
Normal file
1
dock_checker/users/views.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
0
dock_checker/utils/__init__.py
Normal file
0
dock_checker/utils/__init__.py
Normal file
19
dock_checker/utils/base.py
Normal file
19
dock_checker/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)
|
||||||
|
]
|
24
dock_checker/utils/channels.py
Normal file
24
dock_checker/utils/channels.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
if not self.scope.get("user", False) or not self.scope["user"].is_authenticated:
|
||||||
|
self.send_error("Login is required")
|
||||||
|
else:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConsumer(AsyncJsonWebsocketConsumer):
|
||||||
|
async def send_error(self, msg):
|
||||||
|
await self.send_json({"type": "error", "data": {"msg": msg}})
|
||||||
|
|
||||||
|
|
||||||
|
class SyncBaseConsumer(JsonWebsocketConsumer):
|
||||||
|
def send_error(self, msg):
|
||||||
|
self.send_json({"type": "error", "data": {"msg": msg}})
|
9
dock_checker/utils/choices.py
Normal file
9
dock_checker/utils/choices.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from django.db.models.enums import ChoicesMeta
|
||||||
|
|
||||||
|
|
||||||
|
def count_max_length(choices: Iterable | ChoicesMeta):
|
||||||
|
if isinstance(choices, ChoicesMeta):
|
||||||
|
return max([len(val) for val in choices.values])
|
||||||
|
return max([len(val) for val, _ in choices])
|
75
dock_checker/utils/files.py
Normal file
75
dock_checker/utils/files.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from dock_checker.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
dock_checker/utils/generators.py
Normal file
15
dock_checker/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
dock_checker/utils/nums.py
Normal file
27
dock_checker/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
dock_checker/utils/string.py
Normal file
8
dock_checker/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
|
15
dock_checker/utils/zip.py
Normal file
15
dock_checker/utils/zip.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .validators import validate_zip
|
||||||
|
|
||||||
|
|
||||||
|
class ZipfileField(forms.FileField):
|
||||||
|
file_validators = [validate_zip]
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
for validator in self.file_validators:
|
||||||
|
validator(value)
|
||||||
|
return zipfile.ZipFile(value)
|
68
local.yml
Normal file
68
local.yml
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dock_checker_local_postgres_data: {}
|
||||||
|
dock_checker_local_postgres_data_backups: {}
|
||||||
|
|
||||||
|
services:
|
||||||
|
django: &django
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./compose/local/django/Dockerfile
|
||||||
|
image: dock_checker_local_django
|
||||||
|
container_name: dock_checker_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: dock_checker_production_postgres
|
||||||
|
container_name: dock_checker_local_postgres
|
||||||
|
volumes:
|
||||||
|
- dock_checker_local_postgres_data:/var/lib/postgresql/data
|
||||||
|
- dock_checker_local_postgres_data_backups:/backups
|
||||||
|
env_file:
|
||||||
|
- ./.envs/.local/.postgres
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
container_name: dock_checker_local_redis
|
||||||
|
|
||||||
|
celeryworker:
|
||||||
|
<<: *django
|
||||||
|
image: dock_checker_local_celeryworker
|
||||||
|
container_name: dock_checker_local_celeryworker
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- postgres
|
||||||
|
ports: []
|
||||||
|
command: /start-celeryworker
|
||||||
|
|
||||||
|
celerybeat:
|
||||||
|
<<: *django
|
||||||
|
image: dock_checker_local_celerybeat
|
||||||
|
container_name: dock_checker_local_celerybeat
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- postgres
|
||||||
|
ports: []
|
||||||
|
command: /start-celerybeat
|
||||||
|
|
||||||
|
flower:
|
||||||
|
<<: *django
|
||||||
|
image: dock_checker_local_flower
|
||||||
|
container_name: dock_checker_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
|
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
|
||||||
|
# dock_checker directory.
|
||||||
|
current_path = Path(__file__).parent.resolve()
|
||||||
|
sys.path.append(str(current_path / "dock_checker"))
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
0
ml/__init__.py
Normal file
0
ml/__init__.py
Normal file
BIN
ml/checkpoints/models.pkl
Normal file
BIN
ml/checkpoints/models.pkl
Normal file
Binary file not shown.
226
ml/main.py
Normal file
226
ml/main.py
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
import re
|
||||||
|
import pickle
|
||||||
|
import warnings
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import Levenshtein as lev
|
||||||
|
|
||||||
|
from catboost import Pool
|
||||||
|
from pdfminer.high_level import extract_pages
|
||||||
|
from tqdm import tqdm
|
||||||
|
from pdfminer.layout import LTTextContainer, LTChar
|
||||||
|
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_test_features(file):
|
||||||
|
texts = []
|
||||||
|
fonts = []
|
||||||
|
squares = []
|
||||||
|
ids = []
|
||||||
|
coords = []
|
||||||
|
relative_coords = []
|
||||||
|
for page_layout in extract_pages(file):
|
||||||
|
_x1, _y1, _x2, _y2 = page_layout.bbox
|
||||||
|
for i, element in enumerate(page_layout):
|
||||||
|
if isinstance(element, LTTextContainer):
|
||||||
|
text = element.get_text().replace("\n", "")
|
||||||
|
|
||||||
|
if "(cid:" in text:
|
||||||
|
return "Неправильная кодировка файла", False
|
||||||
|
|
||||||
|
if text.split() != [] and len(text) > 4:
|
||||||
|
texts.append(text)
|
||||||
|
|
||||||
|
end = False
|
||||||
|
for text_line in element:
|
||||||
|
if end:
|
||||||
|
break
|
||||||
|
for character in text_line:
|
||||||
|
if isinstance(character, LTChar):
|
||||||
|
if "bold" in character.fontname.lower():
|
||||||
|
fonts.append(1)
|
||||||
|
elif "italic" in character.fontname.lower():
|
||||||
|
fonts.append(2)
|
||||||
|
else:
|
||||||
|
fonts.append(0)
|
||||||
|
end = True
|
||||||
|
break
|
||||||
|
|
||||||
|
x1, y1, x2, y2 = element.bbox
|
||||||
|
coords.append([x1, y1, x2, y2])
|
||||||
|
relative_coords.append(
|
||||||
|
[x1 / _x2, y1 / _y2, (x2 - x1) / _x2, (y2 - y1) / _y2]
|
||||||
|
)
|
||||||
|
|
||||||
|
squares.append((int(x2) - int(x1)) * (int(y2) - int(y1)))
|
||||||
|
|
||||||
|
match = re.search(r"LTTextBoxHorizontal\((\d+)\)", str(element))
|
||||||
|
if match:
|
||||||
|
id = int(match.group(1))
|
||||||
|
ids.append(id)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not texts:
|
||||||
|
return "Файл состоит из сканов", False
|
||||||
|
if len(texts) < 3:
|
||||||
|
return "Главная страница состоит из сканов", False
|
||||||
|
if len(texts) > 25:
|
||||||
|
return "Произошла ошибка", False
|
||||||
|
|
||||||
|
test_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"text": texts,
|
||||||
|
"font": fonts,
|
||||||
|
"file": file,
|
||||||
|
"squares": squares,
|
||||||
|
"ids": ids,
|
||||||
|
"coords": coords,
|
||||||
|
"relative_coords": relative_coords,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return test_df, True
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_features(df):
|
||||||
|
df["len_of_text"] = df["text"].apply(len)
|
||||||
|
# df['len_of_text'] = df['text'].apply(lambda x: len(x.split()))
|
||||||
|
|
||||||
|
df["rank"] = (
|
||||||
|
df.groupby("file")["len_of_text"]
|
||||||
|
.rank(ascending=False, method="min")
|
||||||
|
.astype(int)
|
||||||
|
)
|
||||||
|
df["rank_squares"] = (
|
||||||
|
df.groupby("file")["squares"].rank(ascending=False, method="min").astype(int)
|
||||||
|
)
|
||||||
|
df["font"] = df["font"].astype(
|
||||||
|
object
|
||||||
|
) # Convert boolean to int for computation, True will be 1 and False will be 0
|
||||||
|
df["bold"] = (df["font"] == 1).astype(int)
|
||||||
|
df["bold_percentage"] = (
|
||||||
|
df.groupby("file")["font"].transform(lambda x: x.mean() * 100).astype(int)
|
||||||
|
)
|
||||||
|
df["id_percentage"] = (
|
||||||
|
df.groupby("file")["ids"].transform(lambda x: (x / x.max()) * 100).astype(int)
|
||||||
|
)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def inference_models(checkpoint_name, test_df):
|
||||||
|
columns_to_use = [
|
||||||
|
"font",
|
||||||
|
"rank",
|
||||||
|
"rank_squares",
|
||||||
|
"bold_percentage",
|
||||||
|
"id_percentage",
|
||||||
|
]
|
||||||
|
with open(checkpoint_name, "rb") as f:
|
||||||
|
models = pickle.load(f)
|
||||||
|
|
||||||
|
test_pool = Pool(data=test_df[columns_to_use])
|
||||||
|
preds = []
|
||||||
|
for model in models:
|
||||||
|
preds.append(model.predict_proba(test_pool)[:, 1])
|
||||||
|
test_df["pred"] = np.mean(preds, axis=0)
|
||||||
|
return test_df, test_df.loc[test_df["pred"].idxmax(), "text"].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_distances(target, list_of_strings):
|
||||||
|
target_length = len(target.split())
|
||||||
|
distances = {}
|
||||||
|
|
||||||
|
for string in list_of_strings:
|
||||||
|
string_words = string.split()
|
||||||
|
|
||||||
|
# If the string has at least as many words as the target
|
||||||
|
if len(string_words) >= target_length:
|
||||||
|
for i in range(len(string_words) - target_length + 1):
|
||||||
|
window = " ".join(string_words[i : i + target_length])
|
||||||
|
distance = lev.distance(target, window)
|
||||||
|
|
||||||
|
# Save the distance for this window
|
||||||
|
distances[window] = (distance / len(target)) * 100
|
||||||
|
else:
|
||||||
|
# If the string has fewer words than the target
|
||||||
|
distance = lev.distance(target, string)
|
||||||
|
distances[string] = (distance / len(target)) * 100
|
||||||
|
|
||||||
|
return distances
|
||||||
|
|
||||||
|
|
||||||
|
def replace_multiple_spaces(text):
|
||||||
|
return re.sub(" +", " ", text)
|
||||||
|
|
||||||
|
|
||||||
|
def get_matches(file, target):
|
||||||
|
result = []
|
||||||
|
for i, page_layout in enumerate(tqdm(extract_pages(file))):
|
||||||
|
_x1, _y1, _x2, _y2 = page_layout.bbox
|
||||||
|
texts = []
|
||||||
|
relative_coords = []
|
||||||
|
for element in page_layout:
|
||||||
|
if isinstance(element, LTTextContainer):
|
||||||
|
# print(element.get_text())
|
||||||
|
x1, y1, x2, y2 = element.bbox
|
||||||
|
relative_coords.append(
|
||||||
|
[x1 / _x2, y1 / _y2, (x2 - x1) / _x2, (y2 - y1) / _y2]
|
||||||
|
)
|
||||||
|
|
||||||
|
texts.append(
|
||||||
|
replace_multiple_spaces(element.get_text().replace("\n", ""))
|
||||||
|
)
|
||||||
|
distances = calculate_distances(target, texts)
|
||||||
|
|
||||||
|
for window, distance in distances.items():
|
||||||
|
if distance / len(target) < 0.2:
|
||||||
|
# print(i)
|
||||||
|
# print(window)
|
||||||
|
for j in range(len(texts)):
|
||||||
|
if window in texts[j]:
|
||||||
|
rel_coord = relative_coords[j]
|
||||||
|
break
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"page": i + 1,
|
||||||
|
"window": window,
|
||||||
|
"coordinates": rel_coord,
|
||||||
|
"distance": distance / len(target),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# file = "some.pdf"
|
||||||
|
# columns_to_use = [
|
||||||
|
# "font",
|
||||||
|
# "rank",
|
||||||
|
# "rank_squares",
|
||||||
|
# "bold_percentage",
|
||||||
|
# "id_percentage",
|
||||||
|
# ]
|
||||||
|
# checkpoint_name = "checkpoints/models.pkl"
|
||||||
|
#
|
||||||
|
# test_df, result = extract_test_features(file)
|
||||||
|
#
|
||||||
|
# if isinstance(test_df, pd.DataFrame):
|
||||||
|
# test_df = create_test_features(test_df)
|
||||||
|
# else:
|
||||||
|
# print(result)
|
||||||
|
#
|
||||||
|
# _, target = inference_models(checkpoint_name, test_df, columns_to_use)
|
||||||
|
#
|
||||||
|
# result = []
|
||||||
|
# for page_layout in tqdm(extract_pages(file)):
|
||||||
|
# texts = []
|
||||||
|
# for element in page_layout:
|
||||||
|
# if isinstance(element, LTTextContainer):
|
||||||
|
# texts.append(element.get_text().replace("\n", ""))
|
||||||
|
# distances = calculate_distances(target, texts)
|
||||||
|
#
|
||||||
|
# for window, distance in distances.items():
|
||||||
|
# if distance < 20:
|
||||||
|
# result.append(window)
|
5
ml/requirements.txt
Normal file
5
ml/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
catboost
|
||||||
|
Levenshtein
|
||||||
|
pdfminer.six
|
||||||
|
numpy
|
||||||
|
pandas
|
4156
poetry.lock
generated
Normal file
4156
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: dock_checker_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: dock_checker_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: dock_checker_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: dock_checker_production_celeryworker
|
||||||
|
command: /start-celeryworker
|
||||||
|
|
||||||
|
celerybeat:
|
||||||
|
<<: *django
|
||||||
|
image: dock_checker_production_celerybeat
|
||||||
|
command: /start-celerybeat
|
||||||
|
|
||||||
|
flower:
|
||||||
|
<<: *django
|
||||||
|
image: dock_checker_production_flower
|
||||||
|
command: /start-flower
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user