mirror of
				https://github.com/mistakes-23/backend.git
				synced 2025-10-31 15:37:27 +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