Compare commits
41 Commits
9ae11345d5
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 405d7da754 | |||
| c316104d94 | |||
| eaeecc926a | |||
| 51950cb7d2 | |||
| 08380b2ca3 | |||
| b2bea677ef | |||
| 431541b3cb | |||
| a7a85b1816 | |||
| a1679b69c3 | |||
| 919bf6e69c | |||
| feb579ca31 | |||
| 57a758f93e | |||
| 5893920d69 | |||
| b7aab16a00 | |||
| 9844bccd65 | |||
| 2cba6321c2 | |||
| 305001c9ab | |||
| bfe13039a3 | |||
| f28b44b7fd | |||
| 158c381ac4 | |||
| 045cc3641f | |||
| ff602097be | |||
| 0f9f3afbb4 | |||
| 3672d8b321 | |||
| 016994d594 | |||
| e95de1b553 | |||
| 8971752e5b | |||
| 3aa887c5a4 | |||
| d236f3a14a | |||
| a3d29cd867 | |||
| eec55b803d | |||
| 332764fca2 | |||
| d314fe7e4e | |||
| 40a07bf78d | |||
| ebf8c83bc4 | |||
| 7963e2fad5 | |||
| 790be86f13 | |||
| 20aa084c90 | |||
| 0834474a59 | |||
| fbacdfd590 | |||
| 2b32caa585 |
153
.dockerignore
Normal file
153
.dockerignore
Normal file
@@ -0,0 +1,153 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### 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/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# 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/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
#*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
**/*.env
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Diagrams
|
||||
diagrams/
|
||||
|
||||
# nixpkgs
|
||||
flake.nix
|
||||
flake.lock
|
||||
|
||||
# README
|
||||
*.md
|
||||
|
||||
**/archives
|
||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0] - 2023-09-16
|
||||
### Added
|
||||
- Web interface
|
||||
- Fully featured RestFullAPI v1;
|
||||
- Monitoring free space in storage;
|
||||
- Deleting an archive or ticket also deletes physical files;
|
||||
- Flexible deployment configuration using environment variables;
|
||||
- Dockerized app, the image size is less than 150mb;
|
||||
- Support sqlite3 and PostgreSQL^15;
|
||||
- Whitenoise Static management;
|
||||
- healthcheck checking application availability;
|
||||
64
Dockerfile
Normal file
64
Dockerfile
Normal file
@@ -0,0 +1,64 @@
|
||||
# app/Dockerfile
|
||||
|
||||
# pull the official docker image
|
||||
FROM python:3.10-alpine as base
|
||||
|
||||
# set env variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=on
|
||||
|
||||
# install app dependences
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
|
||||
|
||||
# now multistage builds
|
||||
FROM python:3.10-alpine
|
||||
|
||||
# set env variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# default build args
|
||||
ARG VERSION=0.1.0 \
|
||||
APP_DIR=/app \
|
||||
DATA_DIR=/app/data \
|
||||
SRC_DIR=./logs_collector \
|
||||
SCRIPTS_DIR=./scripts \
|
||||
WEB_PORT=8000 \
|
||||
USER_NAME=collector \
|
||||
USER_GROUP=collector \
|
||||
APP_UID=1000 \
|
||||
APP_GID=1000
|
||||
|
||||
# copy app dependences
|
||||
COPY --from=base /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
|
||||
COPY --from=base /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
# add curl and createa user to avoid running container as root &&
|
||||
# create storage dir
|
||||
RUN apk add --no-cache --upgrade curl && \
|
||||
addgroup --system ${USER_GROUP} --gid ${APP_GID} && \
|
||||
adduser --system --uid ${APP_UID} --ingroup ${USER_GROUP} ${USER_NAME} && \
|
||||
mkdir -p ${APP_DIR}/data && \
|
||||
chown -R ${USER_NAME}:${USER_GROUP} ${DATA_DIR}
|
||||
|
||||
# switch to user
|
||||
USER ${USER_NAME}
|
||||
|
||||
# copy src and entrypoint.sh to app dir
|
||||
COPY --chown=${USER_NAME}:${USER_GROUP} ${SRC_DIR} ${APP_DIR}
|
||||
COPY --chown=${USER_NAME}:${USER_GROUP} ${SCRIPTS_DIR}/entrypoint.sh ${APP_DIR}
|
||||
|
||||
# set workdir
|
||||
WORKDIR ${APP_DIR}
|
||||
|
||||
# app listens on this port by default
|
||||
EXPOSE ${WEB_PORT}
|
||||
|
||||
# set lables about app
|
||||
LABEL maintainer="s.zhukovskii@ispsystem.com"
|
||||
LABEL me.zhukovsky.logs-collector.version=v${VERSION}
|
||||
|
||||
# run app
|
||||
ENTRYPOINT [ "sh", "entrypoint.sh" ]
|
||||
324
README-ru.md
Normal file
324
README-ru.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# LOGS-COLLECTOR
|
||||
|
||||
```sh
|
||||
█░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█
|
||||
█▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄
|
||||
```
|
||||
### [English lang: README.md](README.md)
|
||||
|
||||
### [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
|
||||
## Цель
|
||||
|
||||
Если вы являетесь разработчиком ПО которое в дальнейшем клиенты используют в своей инфраструктуре, вы должны понимать, как иногда бывает трудно изучить проблему с ПО не имея доступа к серверу на котором это ПО работает.
|
||||
|
||||
|
||||
Для решения этой задачи вы можете настраивать ПО на автоматическую отправку обезличенных отчетов о сбоях например использовать Sentry. Это не всегда приемлемо для клиента, к тому же информация может быть не полной или клиенту требуется повышенная конфиденциальность.
|
||||
|
||||
|
||||
В таком случае вы можете попросить клиента отправить вам нужные лог файлы и изучить их в последствии. Но тут возникает другая проблема вам нужен безопасный способ передачи этих файлов как для вас так и для клиента.
|
||||
Это мог быть FTP, SFTP, облако etc. Но что если вы не хотите давать клиенту данные для аутентификации и авторизации?
|
||||
|
||||
Возможно у вас есть доступ к серверу клиента и вы можете прочитать лог файлы на месте. И казалось бы проблема решена. Но на сервере клиента могут отсутствовать инструменты для удобного изучения лог файлов.
|
||||
Даже если сотрудник поддержки может забрать себе нужные файлы и изучить их локально, возникает проблема распространения этих файлов между другими сотрудниками.
|
||||
|
||||
Logs-collector позволяет решить эти задачи.
|
||||
|
||||
Logs-collector является удаленным хранилищем и может принимать и отдавать файлы.
|
||||
|
||||
|
||||
## Термины
|
||||
- Платформа: это ПО разработанное вашей компанией
|
||||
- Тикет: это номер связанный с тикетом в вашей help desk системе
|
||||
- Архив: это загруженный лог файл (поддерживается любой формат)
|
||||
|
||||
## Как это работает?
|
||||
|
||||
- Создаете платформы
|
||||
- Создаете тикет связанный с платформой и номером
|
||||
- Передаете клиенту уникальный токен тикета
|
||||
- Клиент загружает архив лог файлов
|
||||
- Скачиваете архив (находите решение проблемы)
|
||||
- Удаляете архив или тикет или отмечаете тикет решенным
|
||||
|
||||
## Особенности
|
||||
|
||||
- Централизованное хранилище
|
||||
- Для загрузки файла не нужно давать auth credentials
|
||||
- Каждый токен на загрузку уникален и связан только с одним тикетом
|
||||
- Токен имеет ограничение на количество попыток и время жизни
|
||||
- Загрузить файл можно из консоли или через веб
|
||||
- Полнофункциональный RestFullAPI v1
|
||||
- Мониторинг свободного пространства в хранилище
|
||||
- Удаление архива или тикета так же удаляет физические файлы
|
||||
- Приложение соответствует архитектуре приложения 12 факторов
|
||||
- Гибкая настройка развертывания переменными окружения
|
||||
- Приложение докеризировано, размер образа меньше 150mb
|
||||
- Может работать как с sqlite3 так и с PostgreSQL^15
|
||||
- Управление статикой без настройки для этого веб сервера
|
||||
- healthcheck проверка доступности приложения
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Токен на загрузку не связан с авторизацией
|
||||
- Токен на загрузку обладает высокой энтропией.
|
||||
- Двухфакторная аутентификация для пользователей
|
||||
- Для скачивания файла - 2FA должна быть принудительно включена
|
||||
- Админ панель пропатчена на принудительное использование 2FA
|
||||
- Пользователь в контейнере является не привилегированным
|
||||
- Стандартные методы защиты Django и DRF
|
||||
|
||||
## Установка
|
||||
|
||||
### Из docker образа:
|
||||
- Создайте директорию для приложения где вам удобно
|
||||
- Создайте файл docker-compose.yml в директории приложения
|
||||
- Создайте файл .env в директории приложения
|
||||
- Наполните файл .env требуемыми переменными окружения см. ниже
|
||||
|
||||
>Пример файла с использованием хранилища докер и sqlite как база данных по умолчанию:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
# to set environment variables:
|
||||
# create a .env file in the same directory as docker-compose.yaml
|
||||
|
||||
services:
|
||||
server:
|
||||
image: mois3y/logs_collector:0.1.0
|
||||
container_name: logs-collector
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
ports:
|
||||
- "80:8000"
|
||||
volumes:
|
||||
- /etc/timezone:/etc/timezone:ro # optional
|
||||
- /etc/localtime:/etc/localtime:ro # optional
|
||||
- logs_collector_data:/data
|
||||
|
||||
volumes:
|
||||
logs_collector_data:
|
||||
```
|
||||
|
||||
### Из исходников:
|
||||
- Клонируйте репозиторий
|
||||
- docker-compose.yaml уже есть в директории с проектом
|
||||
- создайте в корне проекта файл .env
|
||||
- наполните .env требуемыми переменными окружения см. ниже
|
||||
- соберите образ и запустите контейнер в фоне:
|
||||
|
||||
```sh
|
||||
docker-compose up -d --build
|
||||
```
|
||||
- Вы можете создать свой файл и внести нужные правки:
|
||||
#### docker-compose-example-psql.yaml c PostgreSQL по умолчанию:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
logs_collector:
|
||||
container_name: logs-collector
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VERSION=${VERSION}
|
||||
- SRC_DIR=${SRC_DIR}
|
||||
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||
- APP_DIR=${APP_DIR}
|
||||
- DATA_DIR=${DATA_DIR}
|
||||
- WEB_PORT=${WEB_PORT}
|
||||
- USER_NAME=${USER_NAME}
|
||||
- USER_GROUP=${USER_GROUP}
|
||||
- APP_UID=${APP_UID}
|
||||
- APP_GID=${APP_GID}
|
||||
ports:
|
||||
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||
volumes:
|
||||
- type: volume
|
||||
source: logs_collector_data
|
||||
target: ${APP_DIR}/data
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine3.18
|
||||
container_name: psql-collector
|
||||
volumes:
|
||||
- logs_collector_psql_data:/var/lib/postgresql/data/
|
||||
env_file:
|
||||
- ./.env
|
||||
|
||||
|
||||
volumes:
|
||||
logs_collector_data:
|
||||
logs_collector_psql_data:
|
||||
```
|
||||
|
||||
#### docker-compose-example-psql.yaml c sqlite и bind-mount:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
# to set environment variables:
|
||||
# create a .env file in the same directory as docker-compose.yaml
|
||||
|
||||
services:
|
||||
logs_collector:
|
||||
container_name: logs-collector
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VERSION=${VERSION}
|
||||
- SRC_DIR=${SRC_DIR}
|
||||
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||
- APP_DIR=${APP_DIR}
|
||||
- DATA_DIR=${DATA_DIR}
|
||||
- WEB_PORT=${WEB_PORT}
|
||||
- USER_NAME=${USER_NAME}
|
||||
- USER_GROUP=${USER_GROUP}
|
||||
- APP_UID=${APP_UID}
|
||||
- APP_GID=${APP_GID}
|
||||
ports:
|
||||
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||
volumes:
|
||||
- "/opt/collector/data:${DATA_DIR}"
|
||||
- "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3"
|
||||
env_file:
|
||||
- /.env
|
||||
```
|
||||
|
||||
🔴
|
||||
|
||||
❗ВАЖНО❗
|
||||
|
||||
|
||||
Если вы используете bind-mount и монтируете его в хранилище приложения, помните
|
||||
пользователь в контейнере не привилегирован UID 1000 если примонтированный файл
|
||||
или директория будет принадлежать root приложение не сможет его прочитать и
|
||||
следовательно работать.
|
||||
|
||||
В продакшн среде используйте приложение за вашим любимым обратным прокси.
|
||||
|
||||
Просто добавьте его в стек docker-compose.yaml
|
||||
|
||||
>Можно этого не делать, но Gunicorn рекомендуют придерживаться этого правила.
|
||||
>
|
||||
>Я солидарен с ними, так что вас предупредили)
|
||||
|
||||
🔴
|
||||
|
||||
## Переменные окружения:
|
||||
>Приложение можно настроить, для этого передайте следующие возможные переменные
|
||||
>окружения.
|
||||
>Если переменная не передана, будет использоваться переменная окружения по умолчанию
|
||||
|
||||
```
|
||||
█▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀
|
||||
█▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄
|
||||
```
|
||||
|
||||
| ENV | DEFAULT | INFO |
|
||||
| -------------------- | --------------- | ------------------------ |
|
||||
| SECRET_KEY | j9QGbvM9Z4otb47 | ❗change this immediately|
|
||||
| DEBUG | False | use only False in prod |
|
||||
| ALLOWED_HOSTS | '*' | list separated by commas |
|
||||
| CSRF_TRUSTED_ORIGINS | | list separated by commas |
|
||||
| DB_URL | | url for connect db |
|
||||
| TZ | 'UTC' | server timezone |
|
||||
|
||||
|
||||
|
||||
[CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins)
|
||||
|
||||
Требуется в среде докер в продакшн окружении
|
||||
принимает список url разделенных запятой
|
||||
>http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0
|
||||
|
||||
|
||||
[DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html)
|
||||
|
||||
Нужно указывать если вы хотите использовать PostgreSQL
|
||||
Эти данные должны совпадать с переменными контейнера PostgreSQL
|
||||
|
||||
| ENV | VALUE |
|
||||
| ----------------- | -------------- |
|
||||
| POSTGRES_USER | admin |
|
||||
| POSTGRES_PASSWORD | ddkwndkjdX7RrP |
|
||||
| POSTGRES_DB | collector |
|
||||
|
||||
Пример:
|
||||
|
||||
#### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector
|
||||
- Протокол: **psql://**
|
||||
- Пользователь: **admin**
|
||||
- Пароль: **ddkwndkjdX7RrP**
|
||||
- IP адрес: **psql-collector**
|
||||
- Порт: **5432**
|
||||
- Имя БД: **collector**
|
||||
|
||||
```
|
||||
█▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀
|
||||
█▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄
|
||||
```
|
||||
|
||||
| ENV | DEFAULT |
|
||||
| --------------------------- | -------------- |
|
||||
| GUNICORN_BIND | '0.0.0.0:8000' |
|
||||
| GUNICORN_BACKLOG | 2048 |
|
||||
| GUNICORN_WORKERS | 2 |
|
||||
| GUNICORN_WORKER_CLASS | 'sync' |
|
||||
| GUNICORN_WORKER_CONNECTIONS | 1000 |
|
||||
| GUNICORN_THREADS | 1 |
|
||||
| GUNICORN_TIMEOUT | 3600 |
|
||||
| GUNICORN_KEEPALIVE | 2 |
|
||||
| GUNICORN_LOGLEVEL | 'info' |
|
||||
|
||||
[GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html)
|
||||
|
||||
Подробная информация о каждой переменной окружения доступна в официальной документации.
|
||||
|
||||
GUNICORN_BIND не изменяйте это так как переменная отвечает за прослушиваемый адрес и порт внутри контейнера.
|
||||
|
||||
GUNICORN_TIMEOUT по умолчанию установлена в 3600. Такой большой таймаут нужен для загрузки больших файлов.
|
||||
Поскольку я старался сделать приложение минималистичным и не использовать менеджер задач загрузка файла идет в один поток.
|
||||
|
||||
Если время загрузки будет больше часа соединение разорвется, это особенность синхронной работы воркеров gunicorn если вам не хватает времени на загрузку вы можете увеличить это значение.
|
||||
|
||||
❗ВАЖНО❗
|
||||
|
||||
Gunicorn настроен писать в лог в следующем формате:
|
||||
```python
|
||||
'%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
```
|
||||
Это значит что в логе будет видно IP адрес запроса только из заголовка
|
||||
|
||||
**X-Forwarded-For**
|
||||
|
||||
В продакшн среде приложение должно быть за обратным прокси
|
||||
|
||||
|
||||
## Помощники
|
||||
В корне репозитория проекта есть директория scripts в ней лежит скрипт uploader.sh с помощью которого можно отправить файлы из консоли используя curl.
|
||||
|
||||
Синтаксис простой:
|
||||
|
||||
```cmd
|
||||
Usage: ./uploader.sh [options [parameters]]
|
||||
|
||||
Options:
|
||||
|
||||
-f | --file full path to upload file required
|
||||
-t | --token access token required
|
||||
-u | --url target url required
|
||||
-v | --version print version
|
||||
-h | --help print help
|
||||
```
|
||||
|
||||
|
||||
## Лицензия
|
||||
|
||||
GNU GPL 3.0
|
||||
330
README.md
330
README.md
@@ -1,3 +1,329 @@
|
||||
# logs-collector
|
||||
# LOGS-COLLECTOR
|
||||
|
||||
Серверная сторона для получения и хранения лог файлов
|
||||
```sh
|
||||
█░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█
|
||||
█▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄
|
||||
```
|
||||
|
||||
### [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
### [Russian lang: README.md](README-ru.md)
|
||||
|
||||
|
||||
## Purpose
|
||||
|
||||
If you are a developer of software that clients later use in their infrastructure,
|
||||
you must understand how sometimes it can be difficult to research a problem
|
||||
with software without access to the server on which this software runs.
|
||||
|
||||
To solve this problem, you can configure the software to automatically send
|
||||
anonymized crash reports, for example, use Sentry.
|
||||
This is not always acceptable to the client;
|
||||
Moreover, the information may not be complete or the client
|
||||
requires increased confidentiality.
|
||||
|
||||
|
||||
## Terms
|
||||
- Platform: this is software developed by your company
|
||||
- Ticket: this is the number associated with the ticket in your help desk system
|
||||
- Archive: this is an uploaded log file (any format is supported)
|
||||
|
||||
## How it works?
|
||||
|
||||
- Create platforms
|
||||
- Create a ticket associated with the platform and number
|
||||
- Transfer a unique ticket token to the client
|
||||
- The client downloads an archive of log files
|
||||
- Download the archive (find a solution to the problem)
|
||||
- Delete the archive or ticket or mark the ticket as resolved
|
||||
|
||||
## Features
|
||||
|
||||
- Centralized storage;
|
||||
- To download a file you do not need to provide auth credentials;
|
||||
- Each download token is unique and associated with only one ticket;
|
||||
- The token has a limit on the number of attempts and lifetime;
|
||||
- You can download the file from the console or via the web;
|
||||
- Fully featured RestFullAPI v1;
|
||||
- Monitoring free space in storage;
|
||||
- Deleting an archive or ticket also deletes physical files;
|
||||
- The application follows the 12 factors application architecture;
|
||||
- Flexible deployment configuration using environment variables;
|
||||
- The application is dockerized, the image size is less than 150mb;
|
||||
- Can work with both sqlite3 and PostgreSQL^15;
|
||||
- Static management without configuration for this web server;
|
||||
- healthcheck checking application availability;
|
||||
|
||||
## Security
|
||||
|
||||
- The download token is not associated with authorization
|
||||
- The download token has high entropy.
|
||||
- Two-factor authentication for users
|
||||
- To download a file - 2FA must be forcibly enabled
|
||||
- The admin panel has been patched to force the use of 2FA
|
||||
- The user in the container is not privileged
|
||||
- Standard Django and DRF protection methods
|
||||
|
||||
## Install
|
||||
|
||||
### From the docker image:
|
||||
- Create a directory for the application wherever it is convenient for you
|
||||
- Create a docker-compose.yml file in the application directory
|
||||
- Create a .env file in the application directory
|
||||
- Fill the .env file with the required environment variables, see below
|
||||
|
||||
>Example file using docker store and sqlite as default database:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
# to set environment variables:
|
||||
# create a .env file in the same directory as docker-compose.yaml
|
||||
|
||||
services:
|
||||
server:
|
||||
image: mois3y/logs_collector:0.1.0
|
||||
container_name: logs-collector
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
ports:
|
||||
- "80:8000"
|
||||
volumes:
|
||||
- /etc/timezone:/etc/timezone:ro # optional
|
||||
- /etc/localtime:/etc/localtime:ro # optional
|
||||
- logs_collector_data:/data
|
||||
|
||||
volumes:
|
||||
logs_collector_data:
|
||||
```
|
||||
|
||||
### From the source:
|
||||
- Clone the repository
|
||||
- docker-compose.yaml is already in the project directory
|
||||
- create a .env file in the project root
|
||||
- fill .env with the required environment variables, see below
|
||||
- build the image and run the container in the background:
|
||||
|
||||
```sh
|
||||
docker-compose up -d --build
|
||||
```
|
||||
- You can create your own file and make the necessary edits:
|
||||
#### docker-compose.yaml PostgreSQL by default:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
logs_collector:
|
||||
container_name: logs-collector
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VERSION=${VERSION}
|
||||
- SRC_DIR=${SRC_DIR}
|
||||
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||
- APP_DIR=${APP_DIR}
|
||||
- DATA_DIR=${DATA_DIR}
|
||||
- WEB_PORT=${WEB_PORT}
|
||||
- USER_NAME=${USER_NAME}
|
||||
- USER_GROUP=${USER_GROUP}
|
||||
- APP_UID=${APP_UID}
|
||||
- APP_GID=${APP_GID}
|
||||
ports:
|
||||
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||
volumes:
|
||||
- type: volume
|
||||
source: logs_collector_data
|
||||
target: ${APP_DIR}/data
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine3.18
|
||||
container_name: psql-collector
|
||||
volumes:
|
||||
- logs_collector_psql_data:/var/lib/postgresql/data/
|
||||
env_file:
|
||||
- ./.env
|
||||
|
||||
|
||||
volumes:
|
||||
logs_collector_data:
|
||||
logs_collector_psql_data:
|
||||
```
|
||||
|
||||
#### docker-compose-example-psql.yaml c sqlite и bind-mount:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
# to set environment variables:
|
||||
# create a .env file in the same directory as docker-compose.yaml
|
||||
|
||||
services:
|
||||
logs_collector:
|
||||
container_name: logs-collector
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VERSION=${VERSION}
|
||||
- SRC_DIR=${SRC_DIR}
|
||||
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||
- APP_DIR=${APP_DIR}
|
||||
- DATA_DIR=${DATA_DIR}
|
||||
- WEB_PORT=${WEB_PORT}
|
||||
- USER_NAME=${USER_NAME}
|
||||
- USER_GROUP=${USER_GROUP}
|
||||
- APP_UID=${APP_UID}
|
||||
- APP_GID=${APP_GID}
|
||||
ports:
|
||||
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||
volumes:
|
||||
- "/opt/collector/data:${DATA_DIR}"
|
||||
- "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3"
|
||||
env_file:
|
||||
- /.env
|
||||
```
|
||||
|
||||
🔴
|
||||
|
||||
❗IMPORTANT❗
|
||||
|
||||
If you are using bind-mount and mounting it to your application's storage,
|
||||
remember user in container is not privileged UID 1000 if mounted file
|
||||
or the directory will belong to the root
|
||||
application will not be able to read it and therefore work.
|
||||
|
||||
In a production environment, use the application behind your favorite reverse proxy.
|
||||
|
||||
Just add it to the docker-compose.yaml stack
|
||||
|
||||
>You don't have to do this, but Gunicorn recommends following this rule.
|
||||
>
|
||||
>I agree with them, so you have been warned)
|
||||
|
||||
🔴
|
||||
|
||||
## Environment:
|
||||
>The application can be configured,
|
||||
>to do this, pass the following possible variables surroundings.
|
||||
>If no variable is passed, the default environment variable will be used
|
||||
|
||||
```
|
||||
█▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀
|
||||
█▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄
|
||||
```
|
||||
|
||||
| ENV | DEFAULT | INFO |
|
||||
| -------------------- | --------------- | ------------------------ |
|
||||
| SECRET_KEY | j9QGbvM9Z4otb47 | ❗change this immediately|
|
||||
| DEBUG | False | use only False in prod |
|
||||
| ALLOWED_HOSTS | '*' | list separated by commas |
|
||||
| CSRF_TRUSTED_ORIGINS | | list separated by commas |
|
||||
| DB_URL | | url for connect db |
|
||||
| TZ | 'UTC' | server timezone |
|
||||
|
||||
|
||||
|
||||
[CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins)
|
||||
|
||||
Required in a Docker environment in a production environment
|
||||
accepts a list of urls separated by commas
|
||||
>http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0
|
||||
|
||||
|
||||
[DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html)
|
||||
|
||||
Must be specified if you want to use PostgreSQL
|
||||
This data must match the PostgreSQL container variables
|
||||
|
||||
| ENV | VALUE |
|
||||
| ----------------- | -------------- |
|
||||
| POSTGRES_USER | admin |
|
||||
| POSTGRES_PASSWORD | ddkwndkjdX7RrP |
|
||||
| POSTGRES_DB | collector |
|
||||
|
||||
Example:
|
||||
|
||||
#### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector
|
||||
- Protocol: **psql://**
|
||||
- User: **admin**
|
||||
- Password: **ddkwndkjdX7RrP**
|
||||
- Address: **psql-collector**
|
||||
- Port: **5432**
|
||||
- Database name: **collector**
|
||||
|
||||
```
|
||||
█▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀
|
||||
█▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄
|
||||
```
|
||||
|
||||
| ENV | DEFAULT |
|
||||
| --------------------------- | -------------- |
|
||||
| GUNICORN_BIND | '0.0.0.0:8000' |
|
||||
| GUNICORN_BACKLOG | 2048 |
|
||||
| GUNICORN_WORKERS | 2 |
|
||||
| GUNICORN_WORKER_CLASS | 'sync' |
|
||||
| GUNICORN_WORKER_CONNECTIONS | 1000 |
|
||||
| GUNICORN_THREADS | 1 |
|
||||
| GUNICORN_TIMEOUT | 3600 |
|
||||
| GUNICORN_KEEPALIVE | 2 |
|
||||
| GUNICORN_LOGLEVEL | 'info' |
|
||||
|
||||
[GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html)
|
||||
|
||||
Detailed information about each environment variable is available in
|
||||
the official documentation.
|
||||
|
||||
**GUNICORN_BIND** do not change this since the variable
|
||||
is responsible for the listening address and port inside the container.
|
||||
|
||||
**GUNICORN_TIMEOUT** is set to 3600 by default.
|
||||
Such a large timeout is needed to download large files.
|
||||
Since I tried to make the application minimalistic and not use a task manager,
|
||||
the file is downloaded in one thread.
|
||||
|
||||
If the loading time is more than an hour, the connection will be broken,
|
||||
this is a feature of the synchronous operation of gunicorn workers;
|
||||
if you do not have enough time to load, you can increase this value.
|
||||
|
||||
|
||||
❗IMPORTANT❗
|
||||
|
||||
Gunicorn is configured to write to the log in the following format:
|
||||
```python
|
||||
'%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
```
|
||||
This means that the log will show the IP address of the request only from the header
|
||||
|
||||
**X-Forwarded-For**
|
||||
|
||||
In a production environment, the application must be behind a reverse proxy
|
||||
|
||||
|
||||
## Helpers
|
||||
At the root of the project repository there is a scripts directory,
|
||||
it contains the uploader.sh script with which you can send files
|
||||
from the console using **curl**.
|
||||
|
||||
The syntax is simple:
|
||||
|
||||
```cmd
|
||||
Usage: ./uploader.sh [options [parameters]]
|
||||
|
||||
Options:
|
||||
|
||||
-f | --file full path to upload file required
|
||||
-t | --token access token required
|
||||
-u | --url target url required
|
||||
-v | --version print version
|
||||
-h | --help print help
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
GNU GPL 3.0
|
||||
|
||||
38
docker-compose.yaml
Normal file
38
docker-compose.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
version: "3"
|
||||
|
||||
# to set environment variables:
|
||||
# create a .env file in the same directory as docker-compose.yaml
|
||||
|
||||
services:
|
||||
logs_collector:
|
||||
container_name: logs-collector
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VERSION=${VERSION}
|
||||
- SRC_DIR=${SRC_DIR}
|
||||
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||
- APP_DIR=${APP_DIR}
|
||||
- DATA_DIR=${DATA_DIR}
|
||||
- WEB_PORT=${WEB_PORT}
|
||||
- USER_NAME=${USER_NAME}
|
||||
- USER_GROUP=${USER_GROUP}
|
||||
- APP_UID=${APP_UID}
|
||||
- APP_GID=${APP_GID}
|
||||
ports:
|
||||
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||
volumes:
|
||||
- type: volume
|
||||
source: logs_collector_data
|
||||
target: ${APP_DIR}/data
|
||||
env_file:
|
||||
- ./.env
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://${WEB_HOST}:${WEB_PORT}/${HEALTHCHECK_URL}"]
|
||||
interval: 1m30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
logs_collector_data:
|
||||
@@ -1,3 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import User
|
||||
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
36
logs_collector/account/forms.py
Normal file
36
logs_collector/account/forms.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Submit, Div
|
||||
from crispy_forms.bootstrap import PrependedText
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserProfileForm, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper(self)
|
||||
self.helper.form_show_labels = False
|
||||
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
PrependedText(
|
||||
'email',
|
||||
mark_safe('<i class="bi bi-envelope-at"></i>'),
|
||||
placeholder="email"
|
||||
),
|
||||
PrependedText('first_name', 'First name:'),
|
||||
PrependedText('last_name', 'Last name:'),
|
||||
),
|
||||
Submit('submit', 'Save', css_class='btn btn-primary'),
|
||||
)
|
||||
44
logs_collector/account/migrations/0001_initial.py
Normal file
44
logs_collector/account/migrations/0001_initial.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 4.2 on 2023-09-08 12:27
|
||||
|
||||
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')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,10 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
# Create your models here.
|
||||
|
||||
# using-a-custom-user-model-when-starting-a-project
|
||||
# https://docs.djangoproject.com/en/4.2/topics/auth/customizing/
|
||||
class User(AbstractUser):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('account:show_profile')
|
||||
|
||||
30
logs_collector/account/templates/account/base.html
Normal file
30
logs_collector/account/templates/account/base.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block account_head %}
|
||||
<title>{% block title %}{% endblock title %}</title>
|
||||
{% endblock account_head %}
|
||||
|
||||
{% block account_content %}
|
||||
<header class="sticky-top">
|
||||
<section>
|
||||
{% include 'includes/navigation.html' %}
|
||||
</section>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
{% block main %}{% endblock main %}
|
||||
</section>
|
||||
</main>
|
||||
<footer class="footer mt-auto">
|
||||
<section>
|
||||
{% include 'includes/footer.html' %}
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock account_content %}
|
||||
|
||||
{% block account_scripts %}
|
||||
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
|
||||
{% block bs %}{% endblock bs %}
|
||||
{% block jquery %}{% endblock jquery %}
|
||||
{% endblock account_scripts %}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="container">
|
||||
<h5 class="card-title">Authentication</h5>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="bi bi-person-circle"></i></span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="{{ request.user.username }}"
|
||||
aria-label="Username"
|
||||
disabled
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="bi bi-shield-lock"></i></span>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="●●●●●●●●"
|
||||
aria-label="Password"
|
||||
disabled
|
||||
readonly
|
||||
>
|
||||
<a
|
||||
type="button"
|
||||
class="btn btn-outline-danger"
|
||||
href="{% url 'account:password_change' %}"
|
||||
>
|
||||
<i class="bi bi-pencil-square"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="container">
|
||||
<h5 class="card-title">Profile</h5>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="bi bi-envelope-at"></i></span>
|
||||
<input type="text" class="form-control" placeholder="{{ request.user.email }}" aria-label="Email" disabled readonly>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">First name:</span>
|
||||
<input type="text" class="form-control" placeholder="{{ request.user.first_name }}" aria-label="Username" disabled readonly>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Last name:</i></span>
|
||||
<input type="text" class="form-control" placeholder="{{ request.user.last_name }}" aria-label="Email" disabled readonly>
|
||||
</div>
|
||||
<a
|
||||
href="{% url 'account:update_profile' %}"
|
||||
class="btn btn-outline-warning"
|
||||
>
|
||||
<i class="bi bi-pencil-square"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'account/profile.html' %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block password_change %}
|
||||
<form method="post">
|
||||
<div class="col-lg-6">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<p><input class="btn btn-primary" type="submit" value="Change" /></p>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock password_change %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'account/profile_info.html' %}
|
||||
{% load static %}
|
||||
{% block profile_alerts %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<div class=" d-flex align-items-center mt-1">
|
||||
<h5><i class="bi bi-check-circle-fill"></i> Password changed</h5>
|
||||
</div>
|
||||
Your password has been successfully changed.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endblock profile_alerts %}
|
||||
17
logs_collector/account/templates/account/profile.html
Normal file
17
logs_collector/account/templates/account/profile.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'account/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Account:</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% block profile_info %}{% endblock profile_info %}
|
||||
{% block profile_update %}{% endblock profile_update %}
|
||||
{% block password_change %}{% endblock password_change %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock main %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'account/profile.html' %}
|
||||
{% load static %}
|
||||
{% block profile_info %}
|
||||
{% block profile_alerts %}{% endblock profile_alerts %}
|
||||
{% include 'account/includes/auth_credentials.html' %}
|
||||
{% include 'account/includes/profile_credentials.html' %}
|
||||
{% endblock profile_info %}
|
||||
15
logs_collector/account/templates/account/profile_update.html
Normal file
15
logs_collector/account/templates/account/profile_update.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'account/profile.html' %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block profile_update %}
|
||||
{% include 'account/includes/auth_credentials.html' %}
|
||||
<div class="container">
|
||||
<h5 class="card-title">Profile</h5>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{% crispy form %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock profile_update %}
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
0
logs_collector/account/tests/__init__.py
Normal file
0
logs_collector/account/tests/__init__.py
Normal file
36
logs_collector/account/tests/test_urls.py
Normal file
36
logs_collector/account/tests/test_urls.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import resolve, reverse
|
||||
from django.contrib.auth.views import (
|
||||
LogoutView,
|
||||
PasswordChangeView,
|
||||
PasswordChangeDoneView
|
||||
)
|
||||
|
||||
from account import views
|
||||
|
||||
|
||||
class TestUrls(TestCase):
|
||||
|
||||
# READ:
|
||||
def test_account_logout_url_is_resolved(self):
|
||||
url = reverse('account:logout')
|
||||
self.assertEquals(resolve(url).func.view_class, LogoutView)
|
||||
|
||||
def test_account_show_url_is_resolved(self):
|
||||
url = reverse('account:show_profile')
|
||||
self.assertEquals(resolve(url).func.view_class, views.DetailProfile)
|
||||
|
||||
def test_password_change_done_url_is_resolved(self):
|
||||
url = reverse('account:password_change_done')
|
||||
self.assertEquals(
|
||||
resolve(url).func.view_class, PasswordChangeDoneView
|
||||
)
|
||||
|
||||
# UPDATE:
|
||||
def test_password_change_url_is_resolved(self):
|
||||
url = reverse('account:password_change')
|
||||
self.assertEquals(resolve(url).func.view_class, PasswordChangeView)
|
||||
|
||||
def test_account_update_url_is_resolved(self):
|
||||
url = reverse('account:update_profile')
|
||||
self.assertEquals(resolve(url).func.view_class, views.UpdateProfile)
|
||||
@@ -1,6 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.urls import path
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.urls import path, reverse_lazy
|
||||
from django.contrib.auth.views import (
|
||||
LogoutView,
|
||||
PasswordChangeView,
|
||||
PasswordChangeDoneView
|
||||
)
|
||||
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
@@ -8,6 +12,8 @@ from rest_framework_simplejwt.views import (
|
||||
TokenVerifyView
|
||||
)
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'account'
|
||||
|
||||
@@ -17,7 +23,35 @@ urlpatterns = [
|
||||
'account/logout/',
|
||||
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
|
||||
name='logout'
|
||||
)
|
||||
),
|
||||
# CHANGE PASSWORD:
|
||||
path(
|
||||
'account/password-change/',
|
||||
PasswordChangeView.as_view(
|
||||
template_name='account/password_change.html',
|
||||
success_url=reverse_lazy('account:password_change_done'),
|
||||
),
|
||||
name='password_change'
|
||||
),
|
||||
path(
|
||||
'account/password-change/done/',
|
||||
PasswordChangeDoneView.as_view(
|
||||
template_name='account/password_change_done.html'
|
||||
),
|
||||
name='password_change_done'
|
||||
),
|
||||
# UPDATE:
|
||||
path(
|
||||
'account/update/',
|
||||
views.UpdateProfile.as_view(),
|
||||
name='update_profile'
|
||||
),
|
||||
# READ:
|
||||
path(
|
||||
'account/show/',
|
||||
views.DetailProfile.as_view(),
|
||||
name='show_profile'
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views import generic
|
||||
|
||||
from collector.utils.mixins import ExtraContextMixin
|
||||
|
||||
from .forms import UserProfileForm
|
||||
from .models import User
|
||||
|
||||
|
||||
class DetailProfile(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
|
||||
model = User
|
||||
template_name = 'account/profile_info.html'
|
||||
context_object_name = 'profile'
|
||||
|
||||
def get_title(self, **kwargs):
|
||||
return f'{self.title} - {self.request.user}'
|
||||
|
||||
def get_object(self):
|
||||
return self.model.objects.get(username=self.request.user)
|
||||
|
||||
|
||||
class UpdateProfile(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||
model = User
|
||||
template_name = 'account/profile_update.html'
|
||||
context_object_name = 'profile'
|
||||
form_class = UserProfileForm
|
||||
|
||||
def get_object(self):
|
||||
return self.model.objects.get(username=self.request.user)
|
||||
|
||||
def get_title(self, **kwargs):
|
||||
return f'{self.title} - {self.kwargs.get("username", "account")}'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# █▀▄▀█ █▀▀ ▀█▀ ▄▀█ ▀
|
||||
# █░▀░█ ██▄ ░█░ █▀█ ▄
|
||||
# -------------------
|
||||
__author__ = "MOIS3Y"
|
||||
__credits__ = ["Stepan Zhukovsky"]
|
||||
__license__ = "GPL v3.0"
|
||||
__version__ = "0.1.0"
|
||||
__maintainer__ = "Stepan Zhukovsky"
|
||||
__email__ = "stepan@zhukovsky.me"
|
||||
__status__ = "Production"
|
||||
|
||||
@@ -1,25 +1,103 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
from django.db.models import F
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from .models import Platform, Archive, Ticket
|
||||
from .utils.helpers import sizify
|
||||
|
||||
|
||||
# Register your models here.
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
list_display = ('id', 'name', 'pretty_name')
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
list_display = (
|
||||
'id',
|
||||
'number',
|
||||
'token',
|
||||
'platform',
|
||||
'user',
|
||||
'time_create',
|
||||
'time_update',
|
||||
'resolved',
|
||||
)
|
||||
search_fields = ('number',)
|
||||
list_filter = ('platform', 'resolved', 'time_create',)
|
||||
actions = ['make_resolved', 'make_unresolved']
|
||||
|
||||
@admin.action(description='Mark selected ticket(s) as resolved')
|
||||
def make_resolved(self, request, queryset):
|
||||
updated = queryset.update(resolved=True)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
f'{updated} ticket was successfully marked as resolved.',
|
||||
f'{updated} tickets were successfully marked as resolved.',
|
||||
number=updated,
|
||||
),
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
@admin.action(description='Mark selected ticket(s) as unresolved')
|
||||
def make_unresolved(self, request, queryset):
|
||||
updated = queryset.update(resolved=False)
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
f'{updated} ticket was successfully marked as unresolved.',
|
||||
f'{updated} tickets were successfully marked as unresolved.',
|
||||
number=updated,
|
||||
),
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
|
||||
class ArchiveAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
list_display = (
|
||||
'id',
|
||||
'file_link',
|
||||
'file_size',
|
||||
'md5',
|
||||
'ticket',
|
||||
'time_create'
|
||||
)
|
||||
search_fields = ('ticket',)
|
||||
list_filter = ('time_create', 'ticket')
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(ArchiveAdmin, self).get_queryset(request)
|
||||
qs = qs.annotate(file_size=F('size'))
|
||||
return qs
|
||||
|
||||
class TokenAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
def file_size(self, obj):
|
||||
return sizify(obj.size)
|
||||
|
||||
def file_link(self, obj):
|
||||
if obj.file:
|
||||
file_name = obj.file.name.rpartition('/')[-1]
|
||||
file_path = reverse(
|
||||
'collector:download',
|
||||
kwargs={'path': obj.file}
|
||||
)
|
||||
return format_html(
|
||||
'<a href="{file_path}">{file_name}</a>',
|
||||
file_path=file_path,
|
||||
file_name=file_name
|
||||
)
|
||||
else:
|
||||
return "No attachment"
|
||||
|
||||
file_link.allow_tags = True
|
||||
file_link.short_description = 'File Download'
|
||||
file_size.admin_order_field = 'file_size'
|
||||
|
||||
|
||||
admin.site.register(Platform, PlatformAdmin)
|
||||
admin.site.register(Ticket, TicketAdmin)
|
||||
admin.site.register(Archive, ArchiveAdmin)
|
||||
|
||||
admin.site.site_title = 'Logs Collector'
|
||||
admin.site.site_header = 'LOGS COLLECTOR'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django import forms
|
||||
from django_filters.rest_framework import (
|
||||
CharFilter,
|
||||
FilterSet,
|
||||
NumberFilter,
|
||||
BaseInFilter,
|
||||
)
|
||||
from django_filters import widgets
|
||||
|
||||
@@ -9,21 +11,46 @@ from collector.models import Archive, Ticket
|
||||
from .utils import DateTimeFilterMixin
|
||||
|
||||
|
||||
class TextareaCSVWidget(widgets.BaseCSVWidget, forms.Textarea):
|
||||
"""
|
||||
The widget should create textarea.
|
||||
"""
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
print("Row value: ", value)
|
||||
if not self._isiterable(value):
|
||||
value = [value]
|
||||
|
||||
if len(value) <= 1:
|
||||
# delegate to main widget (Select, etc...) if not multiple values
|
||||
value = value[0] if value else ''
|
||||
return super(TextareaCSVWidget, self).render(name, value, attrs)
|
||||
|
||||
value = ','.join(value)
|
||||
return super(TextareaCSVWidget, self).render(name, value, attrs)
|
||||
|
||||
|
||||
class NumberInFilter(BaseInFilter, NumberFilter):
|
||||
"""
|
||||
The filter should accept coma separated integers.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ArchiveFilter(DateTimeFilterMixin, FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Archive
|
||||
fields = {
|
||||
'id': ['exact', 'in', 'lte', 'gte'],
|
||||
'ticket': ['exact', 'in', 'lte', 'gte'],
|
||||
'id': ['exact', 'lte', 'gte'],
|
||||
'ticket': ['exact', 'lte', 'gte'],
|
||||
'time_create': ['exact', 'lte', 'gte']
|
||||
}
|
||||
|
||||
|
||||
class TicketFilter(DateTimeFilterMixin, FilterSet):
|
||||
number = NumberFilter(
|
||||
number = NumberInFilter(
|
||||
field_name='number',
|
||||
widget=widgets.CSVWidget(),
|
||||
widget=TextareaCSVWidget(),
|
||||
)
|
||||
user = CharFilter(
|
||||
field_name='user__username'
|
||||
|
||||
@@ -11,3 +11,15 @@ class IsGuestUpload(permissions.BasePermission):
|
||||
return True
|
||||
|
||||
return request.user.is_authenticated
|
||||
|
||||
|
||||
class IsGuestCheckUrls(permissions.BasePermission):
|
||||
"""
|
||||
Special permission class for the ability to upload attachments
|
||||
to an unauthorized user using a ticket token
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if request.method in ('HEAD', 'OPTIONS', 'GET',):
|
||||
return True
|
||||
|
||||
return request.user.is_authenticated
|
||||
|
||||
@@ -6,13 +6,13 @@ from drf_spectacular.openapi import OpenApiTypes
|
||||
from collector.models import Archive, Platform, Ticket
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.NUMBER)
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
class TimestampField(serializers.Field):
|
||||
def to_representation(self, value) -> int:
|
||||
return value.timestamp()
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.NUMBER)
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
class JsTimestampField(serializers.Field):
|
||||
def to_representation(self, value) -> int:
|
||||
return round(value.timestamp()*1000)
|
||||
@@ -26,7 +26,7 @@ class PublicArchiveUploadSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class ArchiveSerializer(serializers.ModelSerializer):
|
||||
time_create = JsTimestampField(read_only=True)
|
||||
time_create = serializers.DateTimeField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Archive
|
||||
@@ -41,8 +41,8 @@ class PlatformSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class TicketSerializer(serializers.ModelSerializer):
|
||||
time_create = JsTimestampField(read_only=True)
|
||||
time_update = JsTimestampField(read_only=True)
|
||||
time_create = serializers.DateTimeField(read_only=True)
|
||||
time_update = serializers.DateTimeField(read_only=True)
|
||||
token = serializers.UUIDField(read_only=True)
|
||||
user = serializers.ReadOnlyField(source='user.username')
|
||||
|
||||
@@ -59,3 +59,33 @@ class TicketSerializer(serializers.ModelSerializer):
|
||||
'time_update',
|
||||
'user'
|
||||
]
|
||||
|
||||
|
||||
class StorageInfoSerializer(serializers.Serializer):
|
||||
total = serializers.IntegerField(read_only=True)
|
||||
used = serializers.IntegerField(read_only=True)
|
||||
free = serializers.IntegerField(read_only=True)
|
||||
used_percent = serializers.IntegerField(read_only=True)
|
||||
status = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class TokenStateRootSerializer(serializers.Serializer):
|
||||
info = serializers.CharField(read_only=True, default="manual message")
|
||||
|
||||
|
||||
class TokenStateSerializer(serializers.ModelSerializer):
|
||||
token = serializers.UUIDField(read_only=True)
|
||||
attempts = serializers.IntegerField(read_only=True)
|
||||
resolved = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = [
|
||||
'token',
|
||||
'attempts',
|
||||
'resolved'
|
||||
]
|
||||
|
||||
|
||||
class AppHealthInfoSerializer(serializers.Serializer):
|
||||
status = serializers.CharField(read_only=True, default="ok")
|
||||
|
||||
@@ -29,3 +29,7 @@ class TestUrls(TestCase):
|
||||
def test_detail_platform_url_is_resolved(self):
|
||||
url = reverse('collector_api:platform-detail', args=['vm6'])
|
||||
self.assertEquals(resolve(url).func.cls, views.PlatformViewSet)
|
||||
|
||||
def test_storage_info_url_is_resolved(self):
|
||||
url = reverse('collector_api:storage-info')
|
||||
self.assertEquals(resolve(url).func.cls, views.StorageInfo)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from pathlib import Path
|
||||
from django.core.files.base import ContentFile
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from account.models import User
|
||||
|
||||
from collector.models import Archive, Platform, Ticket
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,31 @@ router.register(r'archives', views.ArchiveViewSet)
|
||||
router.register(r'platforms', views.PlatformViewSet)
|
||||
router.register(r'tickets', views.TicketViewSet)
|
||||
|
||||
check_urlpatterns = [
|
||||
path(
|
||||
'health/',
|
||||
views.AppHealthInfo.as_view(),
|
||||
name='app-info'
|
||||
),
|
||||
path(
|
||||
'storage/',
|
||||
views.StorageInfo.as_view(),
|
||||
name='storage-info'
|
||||
),
|
||||
path(
|
||||
'token/',
|
||||
views.TokenStateRoot.as_view(),
|
||||
name='token-root'
|
||||
),
|
||||
path(
|
||||
'token/<str:token>',
|
||||
views.TokenStateInfo.as_view(),
|
||||
name='token-info'
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
# CRUD:
|
||||
path('v1/', include(router.urls)),
|
||||
path('v1/check/', include(check_urlpatterns)),
|
||||
]
|
||||
|
||||
@@ -1,34 +1,54 @@
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import status
|
||||
# from rest_framework.decorators import action
|
||||
from rest_framework.parsers import FormParser, MultiPartParser
|
||||
from rest_framework.parsers import (
|
||||
FormParser,
|
||||
MultiPartParser,
|
||||
FileUploadParser
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import filters
|
||||
from rest_framework import filters, generics, views, viewsets
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from drf_spectacular.openapi import OpenApiParameter
|
||||
|
||||
from collector.models import Archive, Ticket, Platform
|
||||
from collector.utils.helpers import get_mount_fs_info
|
||||
|
||||
from .filters import ArchiveFilter, TicketFilter
|
||||
from .permissions import IsGuestUpload
|
||||
from .permissions import IsGuestUpload, IsGuestCheckUrls
|
||||
from .serializers import (
|
||||
PublicArchiveUploadSerializer,
|
||||
ArchiveSerializer,
|
||||
PlatformSerializer,
|
||||
TicketSerializer
|
||||
TicketSerializer,
|
||||
StorageInfoSerializer,
|
||||
TokenStateSerializer,
|
||||
AppHealthInfoSerializer,
|
||||
TokenStateRootSerializer,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
description='Archives that contains log files for checking',
|
||||
summary='Show all archives'
|
||||
),
|
||||
create=extend_schema(summary='Create (upload) a new archive'),
|
||||
retrieve=extend_schema(summary='Show archive by id'),
|
||||
update=extend_schema(summary='Update archive'),
|
||||
partial_update=extend_schema(summary='Update archive field'),
|
||||
destroy=extend_schema(summary='Delete archive'),
|
||||
)
|
||||
class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
queryset = Archive.objects.order_by('-time_create')
|
||||
serializer_class = ArchiveSerializer
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
|
||||
permission_classes = (IsGuestUpload, )
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_class = ArchiveFilter
|
||||
@@ -48,10 +68,10 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
},
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name='Upload-Token',
|
||||
type=str,
|
||||
location=OpenApiParameter.HEADER,
|
||||
description="upload permission token",
|
||||
name='Upload-Token',
|
||||
type=str,
|
||||
location=OpenApiParameter.HEADER,
|
||||
description="upload permission token",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -63,29 +83,35 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
bound_ticket = Ticket.objects.get(token=upload_token)
|
||||
if bound_ticket.resolved:
|
||||
return Response(
|
||||
{'error': f'ticket {bound_ticket} already resolved'},
|
||||
{'detail': f'ticket {bound_ticket} already resolved'},
|
||||
status=status.HTTP_423_LOCKED
|
||||
)
|
||||
if bound_ticket.attempts <= 0:
|
||||
return Response(
|
||||
{'error': f'token {upload_token} expired'},
|
||||
{'detail': f'token {upload_token} expired'},
|
||||
status=status.HTTP_423_LOCKED
|
||||
)
|
||||
bound_ticket.attempts -= 1
|
||||
bound_ticket.save()
|
||||
# ? mixin bound ticket number to request.data from user
|
||||
request.data['ticket'] = bound_ticket.number
|
||||
try:
|
||||
request.data['ticket'] = bound_ticket.number
|
||||
except AttributeError:
|
||||
return Response(
|
||||
{'detail': 'Bad Request'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
# ? change serializer for guest user
|
||||
if not request.user.is_authenticated:
|
||||
self.serializer_class = PublicArchiveUploadSerializer
|
||||
except (ValidationError, ObjectDoesNotExist,):
|
||||
return Response(
|
||||
{'error': f'token {upload_token} is not valid'},
|
||||
{'detail': f'token {upload_token} is not valid'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Header Upload-Token is required'},
|
||||
{'detail': 'Header Upload-Token is required'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
# ! default create method:
|
||||
@@ -100,6 +126,17 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
description='Platforms are needed to relative the ticket and software',
|
||||
summary='Show all platforms'
|
||||
),
|
||||
create=extend_schema(summary='Create a new platform'),
|
||||
retrieve=extend_schema(summary='Show platform by internal name'),
|
||||
update=extend_schema(summary='Update platform'),
|
||||
partial_update=extend_schema(summary='Update platform field'),
|
||||
destroy=extend_schema(summary='Delete platform'),
|
||||
)
|
||||
class PlatformViewSet(viewsets.ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
lookup_field = 'name'
|
||||
@@ -107,6 +144,17 @@ class PlatformViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = (IsAuthenticated, )
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
description='Tickets that will be related with the uploaded archive',
|
||||
summary='Show all tickets'
|
||||
),
|
||||
create=extend_schema(summary='Create a new ticket'),
|
||||
retrieve=extend_schema(summary='Show ticket by number'),
|
||||
update=extend_schema(summary='Update ticket'),
|
||||
partial_update=extend_schema(summary='Update ticket field'),
|
||||
destroy=extend_schema(summary='Delete ticket'),
|
||||
)
|
||||
class TicketViewSet(viewsets.ModelViewSet):
|
||||
queryset = Ticket.objects.order_by('-time_create')
|
||||
lookup_field = 'number'
|
||||
@@ -118,3 +166,53 @@ class TicketViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class StorageInfo(views.APIView):
|
||||
"""Info about storage total/used/free space"""
|
||||
|
||||
permission_classes = (IsAuthenticated, )
|
||||
|
||||
@extend_schema(
|
||||
responses=StorageInfoSerializer,
|
||||
summary='Show storage space in bytes'
|
||||
)
|
||||
def get(self, request):
|
||||
return Response(get_mount_fs_info(settings.DATA_DIR))
|
||||
|
||||
|
||||
class TokenStateRoot(views.APIView):
|
||||
""" Show the message of a specific upload token URL"""
|
||||
permission_classes = (IsGuestCheckUrls,)
|
||||
|
||||
@extend_schema(
|
||||
responses=TokenStateRootSerializer,
|
||||
summary='Show info message how get token status'
|
||||
)
|
||||
def get(self, request):
|
||||
message = "to find out the status of the token, place it in the URL"
|
||||
return Response({"detail": message}, status=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary='Show the status of a specific upload token'
|
||||
)
|
||||
)
|
||||
class TokenStateInfo(generics.RetrieveAPIView):
|
||||
""" Show the status of a specific upload token"""
|
||||
queryset = Ticket.objects.order_by('-time_create')
|
||||
lookup_field = 'token'
|
||||
serializer_class = TokenStateSerializer
|
||||
permission_classes = (IsGuestCheckUrls,)
|
||||
|
||||
|
||||
class AppHealthInfo(views.APIView):
|
||||
permission_classes = (IsGuestCheckUrls,)
|
||||
|
||||
@extend_schema(
|
||||
responses=AppHealthInfoSerializer,
|
||||
summary='Show app status'
|
||||
)
|
||||
def get(self, request):
|
||||
return Response({'status': 'ok'}, status=status.HTTP_200_OK)
|
||||
|
||||
16
logs_collector/collector/context_processors.py
Normal file
16
logs_collector/collector/context_processors.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.conf import settings
|
||||
|
||||
from . import __author__
|
||||
from .utils.helpers import get_mount_fs_info
|
||||
|
||||
|
||||
def metadata(request):
|
||||
return {
|
||||
"version": settings.VERSION,
|
||||
"environment": settings.ENVIRONMENT,
|
||||
"author": __author__,
|
||||
}
|
||||
|
||||
|
||||
def storage_info(request):
|
||||
return {'storage': get_mount_fs_info(settings.DATA_DIR)}
|
||||
0
logs_collector/collector/management/__init__.py
Normal file
0
logs_collector/collector/management/__init__.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import logging
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.apps import apps
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.db.models import FileField
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logging.config.dictConfig({
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'console': {
|
||||
'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'console'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['console']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
# HELP MESSAGE:
|
||||
help_part1 = 'This command deletes all media files from'
|
||||
help_part2 = 'the MEDIA_ROOT directory which are no longer referenced'
|
||||
help_part3 = 'by any of the models from installed_apps'
|
||||
help = f'{help_part1} {help_part2} {help_part3}'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
logger.info('Start cleanup storage....')
|
||||
all_models = apps.get_models()
|
||||
physical_files = set()
|
||||
db_files = set()
|
||||
# Get all files from the database
|
||||
logger.info('Get all files from the database....')
|
||||
for model in all_models:
|
||||
file_fields = []
|
||||
filters = Q()
|
||||
for f_ in model._meta.fields:
|
||||
if isinstance(f_, FileField):
|
||||
file_fields.append(f_.name)
|
||||
is_null = {'{}__isnull'.format(f_.name): True}
|
||||
is_empty = {'{}__exact'.format(f_.name): ''}
|
||||
filters &= Q(**is_null) | Q(**is_empty)
|
||||
# only retrieve the models which have non-empty,
|
||||
# non-null file fields
|
||||
if file_fields:
|
||||
files = model.objects.exclude(filters).values_list(
|
||||
*file_fields,
|
||||
flat=True
|
||||
).distinct()
|
||||
db_files.update(files)
|
||||
logger.info(f'Find: {len(db_files)} files from the database')
|
||||
# Get all files from the MEDIA_ROOT, recursively
|
||||
logger.info('Get all files from the MEDIA_ROOT, recursively....')
|
||||
media_root = getattr(settings, 'MEDIA_ROOT', None)
|
||||
if media_root is not None:
|
||||
for relative_root, dirs, files in os.walk(media_root):
|
||||
for file_ in files:
|
||||
# Compute the relative file path to the media directory,
|
||||
# so it can be compared to the values from the db
|
||||
relative_file = os.path.join(
|
||||
os.path.relpath(relative_root, media_root), file_
|
||||
)
|
||||
physical_files.add(relative_file)
|
||||
logger.info(f'Find: {len(physical_files)} files from the MEDIA_ROOT')
|
||||
# Compute the difference and delete those files
|
||||
logger.info('Compute the difference and delete those files....')
|
||||
deletables = physical_files - db_files
|
||||
logger.info(f'Find: {len(deletables)} orphan files')
|
||||
if deletables:
|
||||
for file_ in deletables:
|
||||
logger.info(f"Delete orphan file: {file_}")
|
||||
os.remove(os.path.join(media_root, file_))
|
||||
# Bottom-up - delete all empty folders
|
||||
logger.info('Bottom-up - delete all empty folders....')
|
||||
for relative_root, dirs, files in os.walk(
|
||||
media_root, topdown=False):
|
||||
for dir_ in dirs:
|
||||
if not os.listdir(os.path.join(relative_root, dir_)):
|
||||
os.rmdir(os.path.join(relative_root, dir_))
|
||||
logger.info('Done! Storage has been cleaned up')
|
||||
logger.info('Done! Nothing to delete')
|
||||
24
logs_collector/collector/middleware.py
Normal file
24
logs_collector/collector/middleware.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.http import HttpResponse
|
||||
from django.template import loader
|
||||
|
||||
|
||||
class HttpResponseNotAllowedMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
# One-time configuration and initialization.
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
# Code to be executed for each request before
|
||||
# the view (and later middleware) are called.
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
# Code to be executed for each request/response after
|
||||
# the view is called.
|
||||
if response.status_code == 405:
|
||||
context = {}
|
||||
template = loader.get_template('405.html')
|
||||
return HttpResponse(template.render(context, request))
|
||||
|
||||
return response
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.2 on 2023-08-30 05:52
|
||||
# Generated by Django 4.2 on 2023-09-02 09:30
|
||||
|
||||
import collector.utils
|
||||
from django.conf import settings
|
||||
@@ -47,7 +47,8 @@ class Migration(migrations.Migration):
|
||||
name='Archive',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=collector.utils.logs_dir_path)),
|
||||
('file', models.FileField(upload_to=collector.utils.helpers.logs_dir_path)),
|
||||
('size', models.BigIntegerField(editable=False)),
|
||||
('md5', models.CharField(editable=False, max_length=1024)),
|
||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||
('time_update', models.DateTimeField(auto_now=True)),
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 4.2 on 2023-08-30 08:40
|
||||
|
||||
import collector.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('collector', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='archive',
|
||||
name='file',
|
||||
field=models.FileField(upload_to=collector.utils.logs_dir_path),
|
||||
),
|
||||
]
|
||||
@@ -2,15 +2,17 @@ import uuid
|
||||
import hashlib
|
||||
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from .utils import logs_dir_path
|
||||
from account.models import User
|
||||
|
||||
from .utils.helpers import logs_dir_path
|
||||
|
||||
|
||||
class Archive(models.Model):
|
||||
file = models.FileField(upload_to=logs_dir_path)
|
||||
size = models.BigIntegerField(editable=False)
|
||||
md5 = models.CharField(max_length=1024, editable=False)
|
||||
time_create = models.DateTimeField(auto_now_add=True)
|
||||
time_update = models.DateTimeField(auto_now=True)
|
||||
@@ -22,6 +24,8 @@ class Archive(models.Model):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# add file size in bytes
|
||||
self.size = self.file.size
|
||||
# calculate md5 hash sum and write md5 field to db
|
||||
with self.file.open('rb') as f:
|
||||
md5 = hashlib.md5()
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
const tooltipTriggerList = document.querySelectorAll(
|
||||
'[data-bs-toggle="tooltip"]'
|
||||
)
|
||||
const tooltipList = [...tooltipTriggerList].map(
|
||||
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
)
|
||||
83
logs_collector/collector/static/collector/js/helpers.js
Normal file
83
logs_collector/collector/static/collector/js/helpers.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// formatted byte size to human readable:
|
||||
const sizify = (value) => {
|
||||
let ext = ''
|
||||
if (value < 512000) {
|
||||
value = value / 1024.0
|
||||
ext = 'KB'
|
||||
} else if (value < 4194304000) {
|
||||
value = value / 1048576.0
|
||||
ext = 'MB'
|
||||
} else {
|
||||
value = value / 1073741824.0
|
||||
ext = 'GB'
|
||||
};
|
||||
return `${Math.round(value * 10) / 10} ${ext}`
|
||||
};
|
||||
|
||||
// fix update bootstrap tooltip func:
|
||||
const updateBsTooltip = (instance) => {
|
||||
let tt = bootstrap.Tooltip.getInstance(instance);
|
||||
tt.dispose();
|
||||
bootstrap.Tooltip.getOrCreateInstance(instance);
|
||||
};
|
||||
|
||||
// update storage info widget:
|
||||
const updateStorageInfo = () => {
|
||||
// set storage items vars:
|
||||
let storageIcon = $("#storage_icon")
|
||||
let storageProgressContainer = $("#storage_progress_container")
|
||||
let storage_progress = $("#storage_progress")
|
||||
// set API url:
|
||||
const storageUrl = storage_progress.attr("storage-url")
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: storageUrl,
|
||||
headers: {
|
||||
"Content-Type":"application/json"
|
||||
},
|
||||
dataType: "json",
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
// JSON answer:
|
||||
let storage = data;
|
||||
// set updated fields:
|
||||
let storageInfoNewFields = [
|
||||
`Total: ${sizify(storage.total)}`,
|
||||
'<br>',
|
||||
`Used: ${sizify(storage.used)}`,
|
||||
'<br>',
|
||||
`Free: ${sizify(storage.free)}`
|
||||
].join('')
|
||||
// progress bar update:
|
||||
storage_progress.attr("style", `width:${storage.used_percent}%`)
|
||||
// progress bar color update:
|
||||
if (storage.used_percent > 90) {
|
||||
storage_progress.attr("class", "progress-bar bg-danger");
|
||||
} else if (storage.used_percent > 80) {
|
||||
storage_progress.attr("class", "progress-bar bg-warning");
|
||||
} else {
|
||||
storage_progress.attr("class", "progress-bar bg-success");
|
||||
};
|
||||
// tooltips update:
|
||||
storageIcon.attr("data-bs-title", `Storage used: ${storage.used_percent}%`)
|
||||
storageProgressContainer.attr("data-bs-title", storageInfoNewFields)
|
||||
updateBsTooltip(storageIcon)
|
||||
updateBsTooltip(storageProgressContainer)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const genAlertMessage = (
|
||||
alertMessage='Success message',
|
||||
alertType='success',
|
||||
extraClass=''
|
||||
) => {
|
||||
let alertMessageHTML = [
|
||||
`<div class="alert alert-${alertType} alert-dismissible ${extraClass}" role="alert">`,
|
||||
` <div>${alertMessage}</div>`,
|
||||
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||
'</div>'
|
||||
].join('')
|
||||
return alertMessageHTML
|
||||
}
|
||||
|
||||
export {sizify, updateBsTooltip, updateStorageInfo, genAlertMessage};
|
||||
@@ -1,3 +1,6 @@
|
||||
import {updateStorageInfo} from "./helpers.js";
|
||||
|
||||
|
||||
$(function () {
|
||||
console.log("JQ is ready to work");
|
||||
|
||||
@@ -24,8 +27,11 @@ $(function () {
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
$(archiveListElement).hide(1500);
|
||||
setTimeout(() => {
|
||||
updateStorageInfo();
|
||||
}, 3000);
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
@@ -57,7 +63,7 @@ $(function () {
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status)
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(data)
|
||||
console.log(jqXHR.status)
|
||||
}
|
||||
@@ -82,11 +88,14 @@ $(function () {
|
||||
console.log(jqXHR.status);
|
||||
if (delDiv.length) {
|
||||
delDiv.hide(1500);
|
||||
setTimeout(() => {
|
||||
updateStorageInfo();
|
||||
}, 3000);
|
||||
} else {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,80 +1,138 @@
|
||||
import {updateStorageInfo, genAlertMessage} from "./helpers.js";
|
||||
|
||||
$(function () {
|
||||
// set global variables:
|
||||
const uploadForm = document.getElementById('upload_form');
|
||||
const input_file = document.getElementById('id_file');
|
||||
const progress_bar = document.getElementById('progress');
|
||||
const alert_container = document.getElementById('alert');
|
||||
|
||||
const inputFile = document.getElementById('id_file');
|
||||
const progressBar = document.getElementById('progress');
|
||||
const alertContainer = document.getElementById('alert');
|
||||
// get upload form:
|
||||
$("#upload_form").submit(function(e){
|
||||
e.preventDefault();
|
||||
$form = $(this)
|
||||
// collect request data:
|
||||
let formData = new FormData(this);
|
||||
let upload_token = formData.get("token")
|
||||
const media_data = input_file.files[0];
|
||||
if(media_data != null){
|
||||
progress_bar.classList.remove("not-visible");
|
||||
}
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: progress_bar.getAttribute("upload-url"),
|
||||
data: formData,
|
||||
dataType: 'json',
|
||||
xhr:function(){
|
||||
const xhr = new window.XMLHttpRequest();
|
||||
xhr.upload.addEventListener('progress', e=>{
|
||||
if(e.lengthComputable){
|
||||
const percentProgress = (e.loaded/e.total)*100;
|
||||
console.log(percentProgress);
|
||||
progress_bar.innerHTML = `
|
||||
<div
|
||||
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: ${percentProgress}%"
|
||||
>
|
||||
</div>`
|
||||
let uploadToken = formData.get("token")
|
||||
// generate the URL for token validation:
|
||||
let tokenStatusUrl = [
|
||||
progressBar.getAttribute('token-status-url'),
|
||||
uploadToken
|
||||
].join('')
|
||||
// init upload file func:
|
||||
const uploadFile = () => {
|
||||
// toggle visible progress bar:
|
||||
const mediaData = inputFile.files[0];
|
||||
if(mediaData != null){
|
||||
progressBar.classList.remove("not-visible");
|
||||
}
|
||||
// upload file (chunk) xrh request:
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: progressBar.getAttribute("upload-url"),
|
||||
data: formData,
|
||||
dataType: 'json',
|
||||
xhr:function(){
|
||||
const xhr = new window.XMLHttpRequest();
|
||||
xhr.timeout = 3600000; // increase request timeout to 1 hour
|
||||
xhr.upload.addEventListener('progress', e=>{
|
||||
if(e.lengthComputable){
|
||||
const percentProgress = (e.loaded/e.total)*100;
|
||||
console.log(percentProgress);
|
||||
progressBar.innerHTML = `
|
||||
<div
|
||||
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: ${percentProgress}%"
|
||||
>
|
||||
</div>`
|
||||
}
|
||||
});
|
||||
return xhr
|
||||
},
|
||||
// set auth method:
|
||||
beforeSend: function(xhr) {
|
||||
if (uploadToken) {
|
||||
xhr.setRequestHeader("Upload-Token", uploadToken);
|
||||
}
|
||||
});
|
||||
return xhr
|
||||
},
|
||||
success: function(data, textStatus, jqXHR){
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
'The file has been successfully uploaded to the server. Thank you!',
|
||||
'success',
|
||||
'col-lg-6'
|
||||
)
|
||||
uploadForm.reset()
|
||||
progressBar.classList.add('not-visible')
|
||||
try {
|
||||
updateStorageInfo();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
};
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown){
|
||||
let errorMessage = "Unexpected error. Try again please"
|
||||
if (jqXHR.status === 423 || jqXHR.status === 403) {
|
||||
errorMessage = `Error ${jqXHR.status} <br> ${jqXHR.responseJSON.detail}`
|
||||
}
|
||||
if (jqXHR.status === 401) {
|
||||
errorMessage = `Error ${jqXHR.status} <br> The token field cannot be empty`
|
||||
}
|
||||
if (jqXHR.status === 400) {
|
||||
errorMessage = `Error ${jqXHR.status} <br> ${jqXHR.responseJSON.detail}`
|
||||
}
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
errorMessage,
|
||||
'danger',
|
||||
'col-lg-6'
|
||||
)
|
||||
progressBar.classList.add('not-visible')
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
});
|
||||
}
|
||||
// check token status and upload file if token valid:
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: tokenStatusUrl,
|
||||
dataType: "json",
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
if (data.attempts === 0) {
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
`Error 423 <br> Token: ${uploadToken} expired`,
|
||||
'danger',
|
||||
'col-lg-6'
|
||||
);
|
||||
}
|
||||
else if (data.resolved === true) {
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
`Error 423 <br> Ticket bound with token: ${uploadToken} <br> already resolved`,
|
||||
'danger',
|
||||
'col-lg-6'
|
||||
);
|
||||
} else {
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
`Token: ${uploadToken} is valid. <br> Starting to upload...`,
|
||||
'success',
|
||||
'col-lg-6'
|
||||
);
|
||||
uploadFile();
|
||||
};
|
||||
},
|
||||
beforeSend: function(xhr) {
|
||||
if (upload_token) {
|
||||
xhr.setRequestHeader("Upload-Token", upload_token);
|
||||
error: function(jqXHR){
|
||||
if (jqXHR.responseJSON.detail) {
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
`Error 403 <br> Token: ${uploadToken} is not valid`,
|
||||
'danger',
|
||||
'col-lg-6'
|
||||
)
|
||||
} else {
|
||||
alertContainer.innerHTML = genAlertMessage(
|
||||
`Unexpected error. Try again please`,
|
||||
'danger',
|
||||
'col-lg-6'
|
||||
)
|
||||
}
|
||||
},
|
||||
success: function(data, textStatus, jqXHR){
|
||||
console.log(jqXHR.status);
|
||||
let type = "success";
|
||||
alert_container.innerHTML = [
|
||||
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
|
||||
` <div>The file has been successfully uploaded to the server. Thank you!</div>`,
|
||||
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||
'</div>'
|
||||
].join('')
|
||||
uploadForm.reset()
|
||||
progress_bar.classList.add('not-visible')
|
||||
},
|
||||
error: function(data, textStatus, jqXHR){
|
||||
console.log(data.responseJSON.error);
|
||||
let type = "danger";
|
||||
let error_message = "Unexpected error. Try again please"
|
||||
if (data.status === 423) {
|
||||
error_message = `Error ${data.status}: ${data.responseJSON.error}`
|
||||
}
|
||||
if (data.status === 403) {
|
||||
error_message = `Error ${data.status}: ${data.responseJSON.error}`
|
||||
}
|
||||
if (data.status === 401) {
|
||||
error_message = 'The token field cannot be empty'
|
||||
}
|
||||
alert_container.innerHTML = [
|
||||
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
|
||||
` <div>${error_message}</div>`,
|
||||
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||
'</div>'
|
||||
].join('')
|
||||
progress_bar.classList.add('not-visible')
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,9 +12,20 @@
|
||||
<div class="container mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Archive upload:</h3>
|
||||
<h3 class="card-title">Archive upload:</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="col-lg-6 alert alert-light alert-dismissible fade show" role="alert">
|
||||
<strong>Note!</strong>
|
||||
<br>
|
||||
Please do not close this page
|
||||
until you receive an alert about a successful upload or an error.
|
||||
<br>
|
||||
Thank you!
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert" class="container"></div>
|
||||
<div class="container">
|
||||
{% crispy form %}
|
||||
@@ -24,7 +35,8 @@
|
||||
<div
|
||||
id="progress"
|
||||
upload-url="{% url 'collector_api:archive-list' %}"
|
||||
class="progress"
|
||||
token-status-url="{% url 'collector_api:token-root' %}"
|
||||
class="progress not-visible"
|
||||
role="progressbar"
|
||||
aria-label="Example 20px high"
|
||||
aria-valuenow="25"
|
||||
@@ -40,5 +52,5 @@
|
||||
{% endblock main %}
|
||||
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
||||
<script type="module" src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
||||
{% endblock jquery %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% endblock collector_head %}
|
||||
|
||||
{% block collector_content %}
|
||||
<header>
|
||||
<header class="sticky-top">
|
||||
<section>
|
||||
{% include 'includes/navigation.html' %}
|
||||
</section>
|
||||
@@ -16,9 +16,9 @@
|
||||
{% block main %}{% endblock main %}
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<footer class="footer mt-auto">
|
||||
<section>
|
||||
{% block footer %}{% endblock footer %}
|
||||
{% include 'includes/footer.html' %}
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock collector_content %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="LabelTicket">
|
||||
Delete ticket #{{ ticket.number }} ?
|
||||
Delete ticket {{ ticket.number }}?
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -2,37 +2,32 @@
|
||||
<li
|
||||
id="li-archive-{{ archive.id }}"
|
||||
class="list-group-item list-group-item-action">
|
||||
<smal>
|
||||
<b>File:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.file.name|clean_filename }}</span>
|
||||
</small>
|
||||
<small>
|
||||
<br>
|
||||
<b>MD5:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.md5 }}</span>
|
||||
</small>
|
||||
<small>
|
||||
<br>
|
||||
<b>Uploaded:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.time_update|date:"D d.m.y H:i" }}</span>
|
||||
</small>
|
||||
<br>
|
||||
<small>
|
||||
<b>Size:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.file.size|sizify }}</span>
|
||||
</small>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-sm-start justify-content-between" >
|
||||
<a
|
||||
class="btn btn-outline-success btn-sm mt-2"
|
||||
href="{{ archive.get_absolute_url }}"
|
||||
><i class="bi bi-download"></i> GET</a>
|
||||
<button
|
||||
button type="button"
|
||||
class="btn btn-outline-danger btn-sm ms-2 mt-2"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-archive-del-{{ archive.id }}"
|
||||
><i class="bi bi-trash"></i> DEL</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text" style="width: 60px">File</span>
|
||||
<input type="text" class="form-control" value="{{ archive.file.name|clean_filename }}" disabled readonly>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text" style="width: 60px">MD5</span>
|
||||
<input type="text" class="form-control" value="{{ archive.md5 }}" disabled readonly>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text" style="width: 60px">Time</span>
|
||||
<input type="text" class="form-control" value="{{ archive.time_update|date:"D d.m.y H:i" }}" disabled readonly>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text" style="width: 60px">Size</span>
|
||||
<input type="text" class="form-control" value="{{ archive.size|filesizeformat }}" disabled readonly>
|
||||
</div>
|
||||
<div class="d-flex justify-content-sm-start justify-content-between" >
|
||||
<a
|
||||
class="btn btn-outline-success btn-sm mt-2"
|
||||
href="{{ archive.get_absolute_url }}"
|
||||
><i class="bi bi-download"></i> GET</a>
|
||||
<button
|
||||
button type="button"
|
||||
class="btn btn-outline-danger btn-sm ms-2 mt-2"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-archive-del-{{ archive.id }}"
|
||||
><i class="bi bi-trash"></i> DEL</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<div class="d-sm-flex w-100 justify-content-between mb-2">
|
||||
<h4 class="card-title mb-1">Ticket: {{ ticket.number }}</h4>
|
||||
<h4 class="card-title mb-1"><i class="bi bi-bookmarks"></i> Ticket: {{ ticket.number }}</h4>
|
||||
<small><i class="bi bi-clock-history"></i> {{ ticket.time_create|date:"D d.m.y H:i" }}</small>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-reverse d-flex w-100 justify-content-left">
|
||||
<label class="form-check-label" for="ticket-state">Resolved:</label>
|
||||
<div class="form-check form-switch form-check-reverse d-flex w-100 justify-content-left mb-2">
|
||||
<label class="form-check-label" for="ticket-state"><i class="bi bi-award"></i> Resolved:</label>
|
||||
<input
|
||||
class="form-check-input ms-2 mb-2"
|
||||
type="checkbox"
|
||||
@@ -14,13 +14,29 @@
|
||||
{% if ticket.resolved %} checked {% endif %}>
|
||||
</div>
|
||||
<div class="col-xl-6 mb-2">
|
||||
<h6 class="card-title mb-1">Platform: {{ ticket.platform.pretty_name }}</h6>
|
||||
<h6 class="card-title mb-3">Owner: {{ ticket.user.username }}</h6>
|
||||
<h6 class="card-title mb-1">
|
||||
<small class="me-1"><i class="bi bi-box"></i></small>
|
||||
Platform: {{ ticket.platform.pretty_name }}
|
||||
</h6>
|
||||
<h6 class="card-title mb-1">
|
||||
<small class="me-1"><i class="bi bi-person-circle"></i></small>
|
||||
User: {{ ticket.user.username }}
|
||||
</h6>
|
||||
<h6 class="card-title mb-3">
|
||||
<small class="me-1"><i class="bi bi-clock-history"></i></small>
|
||||
Last modified: {{ ticket.time_update|date:"d.m.y H:i" }}
|
||||
</h6>
|
||||
<!-- Token -->
|
||||
<div class="input-group input-group mb-3">
|
||||
<span class="input-group-text" id="inputGroup-sizing-sm"><i class="bi bi-key"></i></span>
|
||||
<!--Token attempts-->
|
||||
<span class="input-group-text" id="inputGroup-sizing-sm">
|
||||
<a
|
||||
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-title="Attempts for upload archive"
|
||||
>
|
||||
<span
|
||||
class="badge
|
||||
{% if ticket.attempts <= 0 %}
|
||||
@@ -31,6 +47,7 @@
|
||||
bg-primary
|
||||
{% endif %} rounded-pill">{{ ticket.attempts }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<input
|
||||
name="ticket-token"
|
||||
|
||||
39
logs_collector/collector/templates/collector/storage.html
Normal file
39
logs_collector/collector/templates/collector/storage.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<li class="nav-item col-lg-auto d-flex align-items-center">
|
||||
<i
|
||||
class="nav-link me-1 bi bi-sd-card"
|
||||
aria-current="page"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="Storage used: {{ storage.used_percent }}%"
|
||||
>
|
||||
</i>
|
||||
<div
|
||||
class="progress"
|
||||
role="progressbar"
|
||||
aria-label="storage used"
|
||||
aria-valuenow="25"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style="width: 125px"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-html="true"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="
|
||||
Total: {{ storage.total|filesizeformat }}
|
||||
<br>
|
||||
Used: {{ storage.used|filesizeformat }}
|
||||
<br>
|
||||
Free: {{ storage.free|filesizeformat }}
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="progress-bar
|
||||
{% if storage.used_percent > 90 %} bg-danger
|
||||
{% elif storage.used_percent > 80 %} bg-warning
|
||||
{% else %} bg-success
|
||||
{% endif %}"
|
||||
style="width: {{ storage.used_percent }}%"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -4,46 +4,44 @@
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
<div class="row">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body" aria-current="true">
|
||||
{% include 'collector/includes/ticket_info.html' %}
|
||||
<div class="col-xl-6 mt-1 mb-2">
|
||||
{% if ticket.note %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Note:
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body" aria-current="true">
|
||||
{% include 'collector/includes/ticket_info.html' %}
|
||||
<div class="col-xl-6 mt-1 mb-2">
|
||||
{% if ticket.note %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Note:
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-text">
|
||||
{{ ticket.note | markdown | safe }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-text">
|
||||
{{ ticket.note | markdown | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Logs -->
|
||||
{% if ticket.archive_set.all %}
|
||||
<ul class="list-group col-xl-6 mb-2 mt-2">
|
||||
{% for archive in ticket.archive_set.all %}
|
||||
{% include 'collector/includes/ticket_archives.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Card buttons -->
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<a
|
||||
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
||||
class="btn btn-outline-warning mb-1 mt-1"
|
||||
><i class="bi bi-pencil-square"></i> Edit</a>
|
||||
<button
|
||||
class="btn btn-outline-danger mb-1 mt-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-ticket-del-{{ ticket.number }}"
|
||||
><i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Logs -->
|
||||
{% if ticket.archive_set.all %}
|
||||
<ul class="list-group col-xl-6 mb-2 mt-2">
|
||||
{% for archive in ticket.archive_set.all %}
|
||||
{% include 'collector/includes/ticket_archives.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<!-- Card buttons -->
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<a
|
||||
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
||||
class="btn btn-outline-warning mb-1 mt-1"
|
||||
><i class="bi bi-pencil-square"></i> Edit</a>
|
||||
<button
|
||||
class="btn btn-outline-danger mb-1 mt-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-ticket-del-{{ ticket.number }}"
|
||||
><i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,5 +54,5 @@
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
<script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
{% endblock jquery %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="container mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Ticket:</h3>
|
||||
<h3 class="card-title">Ticket:</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% crispy form %}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block content %}
|
||||
<div class="container mt-3">
|
||||
<div class="row">
|
||||
<form method="post" action="{% url 'collector:delete' ticket.number %}">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger btn-archive-eraser"
|
||||
data-bs-dismiss="modal"
|
||||
>Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -86,9 +86,6 @@
|
||||
{% include 'collector/includes/pagination.html' %}
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% block bs %}
|
||||
<script src="{% static 'collector/js/bs.tooltip.js' %}"></script>
|
||||
{% endblock bs %}
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
<script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
{% endblock jquery %}
|
||||
|
||||
@@ -13,31 +13,6 @@ def get_platforms():
|
||||
return Platform.objects.all()
|
||||
|
||||
|
||||
@register.filter(name='sizify')
|
||||
def sizify(value: int) -> str:
|
||||
"""Simple kb/mb/gb size snippet for templates:
|
||||
|
||||
{{ Archive.file.size|sizify }}
|
||||
|
||||
Args:
|
||||
value (int): size of file from Filefield
|
||||
|
||||
Returns:
|
||||
str: format human readable size like 4.2 Gb
|
||||
"""
|
||||
|
||||
if value < 512000:
|
||||
value = value / 1024.0
|
||||
ext = 'Kb'
|
||||
elif value < 4194304000:
|
||||
value = value / 1048576.0
|
||||
ext = 'Mb'
|
||||
else:
|
||||
value = value / 1073741824.0
|
||||
ext = 'Gb'
|
||||
return f'{round(value, 2)} {ext}'
|
||||
|
||||
|
||||
@register.filter(name='clean_filename')
|
||||
def clean_filename(filename: str) -> str:
|
||||
"""delete prefix ticket number folder for template
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from pathlib import Path
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.base import ContentFile
|
||||
from django.conf import settings
|
||||
|
||||
from account.models import User
|
||||
from collector.models import Platform, Ticket, Archive
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import resolve, reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from account.models import User
|
||||
|
||||
from collector import views
|
||||
from collector.models import Ticket, Platform
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from account.models import User
|
||||
|
||||
from collector.models import Ticket, Platform
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import os
|
||||
|
||||
|
||||
def logs_dir_path(instance, filename):
|
||||
"""
|
||||
file will be uploaded to
|
||||
MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename>
|
||||
"""
|
||||
return f'{instance.ticket.number}/{filename}'
|
||||
|
||||
|
||||
# deprecated
|
||||
def get_file_size(file_path, unit='bytes'):
|
||||
file_size = os.path.getsize(file_path)
|
||||
exponents_map = {'bytes': 0, 'kb': 1, 'mb': 2, 'gb': 3}
|
||||
if unit not in exponents_map:
|
||||
raise ValueError("Must select from \
|
||||
['bytes', 'kb', 'mb', 'gb']")
|
||||
else:
|
||||
size = file_size / 1024 ** exponents_map[unit]
|
||||
return round(size, 3)
|
||||
|
||||
|
||||
# deprecated
|
||||
def is_ajax(request):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return True
|
||||
|
||||
|
||||
class PageTitleViewMixin:
|
||||
title = 'Collector'
|
||||
|
||||
def get_title(self, *args, **kwargs):
|
||||
"""
|
||||
Return the class title attr by default,
|
||||
but you can override this method to further customize
|
||||
"""
|
||||
return self.title
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = self.get_title()
|
||||
return context
|
||||
0
logs_collector/collector/utils/__init__.py
Normal file
0
logs_collector/collector/utils/__init__.py
Normal file
61
logs_collector/collector/utils/helpers.py
Normal file
61
logs_collector/collector/utils/helpers.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import shutil
|
||||
import pathlib
|
||||
|
||||
|
||||
def logs_dir_path(instance, filename: str) -> str:
|
||||
"""
|
||||
file will be uploaded to
|
||||
MEDIA_ROOT/view/<filename>
|
||||
"""
|
||||
return f'{instance.ticket.number}/{filename}'
|
||||
|
||||
|
||||
def sizify(value: int) -> str:
|
||||
"""Simple kb/mb/gb size snippet for admin panel custom field:
|
||||
|
||||
Args:
|
||||
value (int): size of file from Filefield
|
||||
|
||||
Returns:
|
||||
str: format human readable size like 4.2 Gb
|
||||
"""
|
||||
if value < 512000:
|
||||
value = value / 1024.0
|
||||
ext = 'KB'
|
||||
elif value < 4194304000:
|
||||
value = value / 1048576.0
|
||||
ext = 'MB'
|
||||
else:
|
||||
value = value / 1073741824.0
|
||||
ext = 'GB'
|
||||
return f'{round(value, 1)} {ext}'
|
||||
|
||||
|
||||
def get_mount_fs_info(path: type[pathlib.PosixPath]) -> dict:
|
||||
"""
|
||||
Get directory information for storing uploaded files.
|
||||
Includes information total/used/free space on mount device
|
||||
|
||||
Args:
|
||||
path (pathlib.PosixPath): path to storage dir
|
||||
|
||||
Returns:
|
||||
dict: storage mount info
|
||||
"""
|
||||
mount_info: dict = {}
|
||||
try:
|
||||
mount_info = shutil.disk_usage(path)._asdict()
|
||||
mount_info['used_percent'] = round(
|
||||
mount_info['used'] / mount_info['total'] * 100,
|
||||
)
|
||||
mount_info['status'] = 'mount'
|
||||
except Exception as error: # expected FileNotFoundError
|
||||
mount_info = {
|
||||
'total': 0,
|
||||
'used': 0,
|
||||
'free': 0,
|
||||
'used_percent': 0,
|
||||
'status': 'error',
|
||||
'traceback': f'{error}'
|
||||
}
|
||||
return mount_info
|
||||
23
logs_collector/collector/utils/mixins.py
Normal file
23
logs_collector/collector/utils/mixins.py
Normal file
@@ -0,0 +1,23 @@
|
||||
class ExtraContextMixin:
|
||||
"""The class adds additional context
|
||||
to all child view classes that inherit from it.
|
||||
Overrides the get_context_data method for CBV
|
||||
"""
|
||||
|
||||
title = 'Collector'
|
||||
|
||||
def get_title(self, *args, **kwargs):
|
||||
"""
|
||||
Return the class title attr by default,
|
||||
but you can override this method to further customize
|
||||
"""
|
||||
return self.title
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {}
|
||||
try:
|
||||
context = super().get_context_data(**kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
context['title'] = self.get_title()
|
||||
return context
|
||||
@@ -1,27 +1,24 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import FileResponse
|
||||
from django.http import FileResponse, Http404
|
||||
from django.views import generic
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
|
||||
from two_factor.views import OTPRequiredMixin
|
||||
|
||||
from .forms import TicketForm, ArchiveForm
|
||||
from .models import Archive, Ticket
|
||||
from .utils import PageTitleViewMixin
|
||||
from .utils.mixins import ExtraContextMixin
|
||||
|
||||
|
||||
class ArchiveUploadView(PageTitleViewMixin, generic.View):
|
||||
class ArchiveUploadView(ExtraContextMixin, generic.TemplateView):
|
||||
form_class = ArchiveForm()
|
||||
template = 'collector/archive_upload.html',
|
||||
template_name = 'collector/archive_upload.html'
|
||||
|
||||
def get(self, request):
|
||||
return render(
|
||||
request,
|
||||
self.template,
|
||||
context={'form': self.form_class}
|
||||
)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'] = self.form_class
|
||||
return context
|
||||
|
||||
def get_title(self):
|
||||
return f'{self.title} - upload'
|
||||
@@ -38,10 +35,14 @@ class ArchiveHandlerView(
|
||||
|
||||
def get(self, request, path):
|
||||
self.object = self.get_object()
|
||||
try:
|
||||
self.object.file.size
|
||||
except FileNotFoundError:
|
||||
raise Http404(f'File: {self.object.file} not found')
|
||||
return FileResponse(self.object.file)
|
||||
|
||||
|
||||
class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
class CreateTicket(LoginRequiredMixin, ExtraContextMixin, generic.CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
@@ -54,7 +55,7 @@ class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
class UpdateTicket(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
@@ -69,7 +70,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
class ListAllTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
@@ -78,27 +79,27 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
search_query = self.request.GET.get('search', '')
|
||||
if search_query:
|
||||
query_list = []
|
||||
try:
|
||||
for item in search_query.split(','):
|
||||
query_list.append(int(item))
|
||||
except ValueError:
|
||||
return super().get_queryset()
|
||||
queryset = self.model.objects.filter(
|
||||
Q(number__in=query_list) | Q(number__icontains=query_list[0])
|
||||
)
|
||||
resolved_status_query = self.request.GET.get('resolved', '')
|
||||
if search_query or resolved_status_query:
|
||||
self.paginate_by = 100 # ? fake disable pagination)
|
||||
if search_query:
|
||||
query_list = []
|
||||
try:
|
||||
for item in search_query.split(','):
|
||||
query_list.append(int(item))
|
||||
except ValueError:
|
||||
return super().get_queryset()
|
||||
queryset = self.model.objects.filter(
|
||||
Q(number__in=query_list) | Q(number__icontains=query_list[0]) # noqa:E501
|
||||
)
|
||||
if resolved_status_query:
|
||||
queryset = self.model.objects.filter(Q(resolved=True))
|
||||
return queryset
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class ListPlatformTickets(
|
||||
LoginRequiredMixin,
|
||||
PageTitleViewMixin,
|
||||
generic.ListView
|
||||
):
|
||||
class ListPlatformTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): # noqa:E501
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
@@ -114,7 +115,7 @@ class ListPlatformTickets(
|
||||
)
|
||||
|
||||
|
||||
class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView):
|
||||
class DetailTicket(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
|
||||
model = Ticket
|
||||
template_name = 'collector/ticket.html'
|
||||
context_object_name = 'ticket'
|
||||
|
||||
49
logs_collector/gunicorn.conf.py
Normal file
49
logs_collector/gunicorn.conf.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import environ
|
||||
import gunicorn
|
||||
|
||||
from logs_collector.settings import BASE_DIR
|
||||
|
||||
# █▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ █▀▀ █▀█ █▄░█ █▀▀ █ █▀▀ ▀
|
||||
# █▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ █▄▄ █▄█ █░▀█ █▀░ █ █▄█ ▄
|
||||
# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
# https://docs.gunicorn.org/en/stable/settings.html
|
||||
# https://github.com/benoitc/gunicorn/blob/master/examples/example_config.py
|
||||
|
||||
env = environ.Env(
|
||||
# set casting, default value:
|
||||
GUNICORN_SERVER_NAME=(str, 'undisclosed'),
|
||||
GUNICORN_BIND=(str, '0.0.0.0:8000'),
|
||||
GUNICORN_BACKLOG=(int, 2048),
|
||||
|
||||
GUNICORN_WORKERS=(int, 2),
|
||||
GUNICORN_WORKER_CLASS=(str, 'sync'),
|
||||
GUNICORN_WORKER_CONNECTIONS=(int, 1000),
|
||||
GUNICORN_THREADS=(int, 2),
|
||||
GUNICORN_TIMEOUT=(int, 3600),
|
||||
GUNICORN_KEEPALIVE=(int, 2),
|
||||
|
||||
GUNICORN_LOGLEVEL=(str, 'info'),
|
||||
)
|
||||
|
||||
environ.Env.read_env(BASE_DIR / '.env')
|
||||
|
||||
# Server name:
|
||||
gunicorn.SERVER = env('GUNICORN_SERVER_NAME')
|
||||
|
||||
# Server socket:
|
||||
bind = env('GUNICORN_BIND')
|
||||
backlog = env('GUNICORN_BACKLOG')
|
||||
|
||||
# Worker processes:
|
||||
workers = env('GUNICORN_WORKERS')
|
||||
worker_class = env('GUNICORN_WORKER_CLASS')
|
||||
worker_connections = env('GUNICORN_WORKER_CONNECTIONS')
|
||||
threads = env('GUNICORN_THREADS')
|
||||
timeout = env('GUNICORN_TIMEOUT')
|
||||
keepalive = env('GUNICORN_KEEPALIVE')
|
||||
|
||||
# Logging:
|
||||
loglevel = env('GUNICORN_LOGLEVEL')
|
||||
errorlog = '-'
|
||||
accesslog = '-'
|
||||
access_log_format = '%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' # noqa:E501
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
An application for uploading archives with log files
|
||||
for their subsequent download and check issues
|
||||
that have arisen with software products.
|
||||
The purpose of creating this application is
|
||||
the ability to securely exchange and store log files containing sensitive data.
|
||||
I have not found an application that would allow an unauthorized client
|
||||
to upload data without providing him with authorization credentials.
|
||||
You can use other applications for this,
|
||||
such as Google cloud, Yandex cloud, DropBox etc, but in this case,
|
||||
you do not have a tool that would allow you to automatically restrict uploads
|
||||
later until you explicitly deny access to the shared link.
|
||||
This app allows you to upload files using a unique token
|
||||
associated with a support ticket.
|
||||
This token has a limit on the number of file upload attempts.
|
||||
Also, if the ticket is resolved, then the token is invalid.
|
||||
"""
|
||||
|
||||
|
||||
# █▀▄▀█ █▀▀ ▀█▀ ▄▀█ ▀
|
||||
# █░▀░█ ██▄ ░█░ █▀█ ▄
|
||||
# -------------------
|
||||
__author__ = "MOIS3Y"
|
||||
__credits__ = ["Stepan Zhukovsky"]
|
||||
__license__ = "GPL v3.0"
|
||||
__version__ = "0.1.0"
|
||||
__maintainer__ = "Stepan Zhukovsky"
|
||||
__email__ = "stepan@zhukovsky.me"
|
||||
__status__ = "Production"
|
||||
|
||||
@@ -2,32 +2,59 @@ import environ
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
|
||||
from . import __version__, __status__
|
||||
|
||||
|
||||
# █▀█ █▀█ █▀█ ▀█▀ ▀
|
||||
# █▀▄ █▄█ █▄█ ░█░ ▄
|
||||
# -- -- -- -- -- --
|
||||
|
||||
env = environ.Env(
|
||||
# set casting default value
|
||||
DEBUG=(bool, False),
|
||||
SECRET_KEY=(str, 'j9QGbvM9Z4otb47')
|
||||
)
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# █▀▀ █▄░█ █░█ ▀
|
||||
# ██▄ █░▀█ ▀▄▀ ▄
|
||||
# -- -- -- -- --
|
||||
|
||||
# Set default environ variables:
|
||||
env = environ.Env(
|
||||
# set casting default value
|
||||
VERSION=(str, __version__),
|
||||
ENVIRONMENT=(str, __status__),
|
||||
DEBUG=(bool, False),
|
||||
SECRET_KEY=(str, 'j9QGbvM9Z4otb47'),
|
||||
DATA_DIR=(Path, BASE_DIR / 'data'),
|
||||
CSRF_TRUSTED_ORIGINS=(list, []),
|
||||
ALLOWED_HOSTS=(list, ['*']),
|
||||
TZ=(str, 'UTC'),
|
||||
)
|
||||
|
||||
# Read .env file if exist:
|
||||
environ.Env.read_env(BASE_DIR / '.env')
|
||||
|
||||
|
||||
# █▀▀ █▀█ █▀█ █▀▀ ▀
|
||||
# █▄▄ █▄█ █▀▄ ██▄ ▄
|
||||
# -- -- -- -- -- -
|
||||
|
||||
VERSION = env('VERSION')
|
||||
|
||||
ENVIRONMENT = env('ENVIRONMENT')
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env('DEBUG')
|
||||
|
||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
|
||||
|
||||
# TODO: required for docker image
|
||||
# CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=["*"])
|
||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
|
||||
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
|
||||
if not DEBUG:
|
||||
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS")
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
@@ -62,6 +89,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'collector.middleware.HttpResponseNotAllowedMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'logs_collector.urls'
|
||||
@@ -73,10 +101,14 @@ TEMPLATES = [
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
# default:
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# collector:
|
||||
'collector.context_processors.metadata',
|
||||
'collector.context_processors.storage_info',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -84,18 +116,6 @@ TEMPLATES = [
|
||||
|
||||
WSGI_APPLICATION = 'logs_collector.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': env.db_url(
|
||||
'SQLITE_URL',
|
||||
default=f'sqlite:///{BASE_DIR / "db.sqlite3"}'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
@@ -114,19 +134,22 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = env('TZ', default='UTC')
|
||||
TIME_ZONE = env('TZ')
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# █▀ ▀█▀ ▄▀█ ▀█▀ █ █▀▀ ▀
|
||||
# ▄█ ░█░ █▀█ ░█░ █ █▄▄ ▄
|
||||
# -- -- -- -- -- -- -- -
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
# Whitenoise:
|
||||
@@ -134,20 +157,26 @@ USE_TZ = True
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'static'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
# █▀▄ ▄▀█ ▀█▀ ▄▀█ ▀
|
||||
# █▄▀ █▀█ ░█░ █▀█ ▄
|
||||
# -- -- -- -- -- --
|
||||
|
||||
MEDIA_ROOT = BASE_DIR / 'archives'
|
||||
# Build paths inside the project for db and storage.
|
||||
DATA_DIR = env('DATA_DIR')
|
||||
|
||||
# Create DATA_DIR ignore if exist:
|
||||
Path(DATA_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Custom file storage path
|
||||
MEDIA_ROOT = DATA_DIR / 'archives'
|
||||
|
||||
STORAGES = {
|
||||
# ...
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
"OPTIONS": {
|
||||
"location": MEDIA_ROOT,
|
||||
"base_url": "/archives/",
|
||||
"base_url": "/download/archives/",
|
||||
},
|
||||
},
|
||||
"staticfiles": {
|
||||
@@ -155,48 +184,76 @@ STORAGES = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# █▀▄ ▄▀█ ▀█▀ ▄▀█ █▄▄ ▄▀█ █▀ █▀▀ ▀
|
||||
# █▄▀ █▀█ ░█░ █▀█ █▄█ █▀█ ▄█ ██▄ ▄
|
||||
# -- -- -- -- -- -- -- -- -- -- --
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': env.db_url(
|
||||
'DB_URL',
|
||||
default=f'sqlite:///{DATA_DIR / "db.sqlite3"}'
|
||||
)
|
||||
}
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
# █▀▀ ▀▄▀ ▀█▀ █▀▀ █▄░█ ▀█▀ █ █▀█ █▄░█ █▀ ▀
|
||||
# ██▄ █░█ ░█░ ██▄ █░▀█ ░█░ █ █▄█ █░▀█ ▄█ ▄
|
||||
# -- -- -- -- -- -- -- -- -- -- -- -- -- -
|
||||
|
||||
# django-crispy-forms and crispy-bootstrap5
|
||||
# https://django-crispy-forms.readthedocs.io/en/latest/
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
# https://www.django-rest-framework.org/api-guide/settings/
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
],
|
||||
'DEFAULT_PARSER_CLASSES': [
|
||||
'rest_framework.parsers.JSONParser',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
'rest_framework.parsers.MultiPartParser'
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication'
|
||||
# 'rest_framework.authentication.BasicAuthentication'
|
||||
],
|
||||
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend', ], # noqa:E501
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501
|
||||
# 'PAGE_SIZE': 3,
|
||||
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
|
||||
}
|
||||
|
||||
# https://drf-spectacular.readthedocs.io/en/latest/readme.html
|
||||
# TODO: set environ vars config!
|
||||
if DEBUG:
|
||||
REST_FRAMEWORK.get(
|
||||
'DEFAULT_RENDERER_CLASSES', []
|
||||
).append('rest_framework.renderers.BrowsableAPIRenderer')
|
||||
REST_FRAMEWORK.get(
|
||||
'DEFAULT_PARSER_CLASSES', []
|
||||
).append('rest_framework.renderers.BrowsableAPIRenderer')
|
||||
|
||||
# https://drf-spectacular.readthedocs.io/en/latest/readme.html
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'Logs collector API',
|
||||
'DESCRIPTION': 'Collector of archives with log files for further analysis',
|
||||
'VERSION': '0.1.0',
|
||||
'VERSION': VERSION,
|
||||
'SERVE_INCLUDE_SCHEMA': True,
|
||||
'SERVE_PUBLIC': False,
|
||||
"SWAGGER_UI_SETTINGS": {
|
||||
"filter": True,
|
||||
},
|
||||
}
|
||||
|
||||
# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||
@@ -237,6 +294,14 @@ SIMPLE_JWT = {
|
||||
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", # noqa:E501
|
||||
}
|
||||
|
||||
|
||||
# ▄▀█ █░█ ▀█▀ █░█ ▀
|
||||
# █▀█ █▄█ ░█░ █▀█ ▄
|
||||
# -- -- -- -- -- --
|
||||
|
||||
LOGIN_URL = 'two_factor:login'
|
||||
LOGIN_REDIRECT_URL = 'collector:index'
|
||||
LOGOUT_REDIRECT_URL = 'two_factor:login'
|
||||
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-user-model
|
||||
AUTH_USER_MODEL = 'account.User'
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView
|
||||
)
|
||||
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
from account.utils import AdminSiteOTPRequiredMixinRedirectSetup
|
||||
|
||||
@@ -48,9 +43,4 @@ urlpatterns += [
|
||||
SpectacularSwaggerView.as_view(url_name='schema'),
|
||||
name='swagger-ui'
|
||||
),
|
||||
path(
|
||||
'api/v1/schema/redoc/',
|
||||
SpectacularRedocView.as_view(url_name='schema'),
|
||||
name='redoc'
|
||||
),
|
||||
]
|
||||
|
||||
10
logs_collector/templates/403_csrf.html
Normal file
10
logs_collector/templates/403_csrf.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends 'errors.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Logs Collector - CSRF error {% endblock title %}
|
||||
|
||||
{% block status_code %}403{% endblock status_code %}
|
||||
{% block error_message %}
|
||||
<p class="fs-3"> <span class="text-danger">Opps!</span> CSRF verification failed.</p>
|
||||
<p class="lead">Request aborted</p>
|
||||
{% endblock error_message %}
|
||||
10
logs_collector/templates/404.html
Normal file
10
logs_collector/templates/404.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends 'errors.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Logs Collector - Not Found {% endblock title %}
|
||||
|
||||
{% block status_code %}404{% endblock status_code %}
|
||||
{% block error_message %}
|
||||
<p class="fs-3"> <span class="text-danger">Opps!</span> Page not found.</p>
|
||||
<p class="lead">The content you're looking for doesn't exist.</p>
|
||||
{% endblock error_message %}
|
||||
14
logs_collector/templates/405.html
Normal file
14
logs_collector/templates/405.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends 'errors.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Logs Collector - Method not allowed {% endblock title %}
|
||||
|
||||
{% block status_code %}405{% endblock status_code %}
|
||||
{% block error_message %}
|
||||
<p class="fs-3"> <span class="text-danger">Opps!</span> Method not allowed</p>
|
||||
<p class="lead">
|
||||
Request method:
|
||||
<span class="text-danger">{{ request.method }}</span>
|
||||
isn't allowed for this URL
|
||||
</p>
|
||||
{% endblock error_message %}
|
||||
10
logs_collector/templates/500.html
Normal file
10
logs_collector/templates/500.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends 'errors.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Logs Collector - Server error {% endblock title %}
|
||||
|
||||
{% block status_code %}500{% endblock status_code %}
|
||||
{% block error_message %}
|
||||
<p class="fs-3"> <span class="text-danger">Opps!</span> Server error</p>
|
||||
<p class="lead">Unexpected error, please try again or contact system admin </p>
|
||||
{% endblock error_message %}
|
||||
5
logs_collector/templates/admin/base_site.html
Normal file
5
logs_collector/templates/admin/base_site.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
{% block extrahead %}
|
||||
<link rel="shortcut icon" href="{% static 'img/favicon.ico' %}" />
|
||||
{% endblock %}
|
||||
@@ -34,16 +34,21 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
|
||||
>
|
||||
{% block errors_head %}{% endblock errors_head %}
|
||||
{% block collector_head %}{% endblock collector_head %}
|
||||
{% block account_head %}{% endblock account_head %}
|
||||
</head>
|
||||
<body>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
{% block http_errors %}{% endblock http_errors %}
|
||||
{% block collector_content %}{% endblock collector_content %}
|
||||
{% block account_content %}{% endblock account_content %}
|
||||
|
||||
<!-- BS dependences JS-->
|
||||
<script src="{% static '/js/bootstrap.bundle.min.js' %}"></script>
|
||||
<!-- Theme switcher JS-->
|
||||
<script src="{% static '/js/bs.theme.mode.js' %}"></script>
|
||||
|
||||
<!-- BS tooltip JS-->
|
||||
<script src="{% static '/js/bs.tooltip.js' %}"></script>
|
||||
<!-- Apps JS-->
|
||||
{% block collector_scripts %}{% endblock collector_scripts %}
|
||||
{% block account_scripts %}{% endblock account_scripts %}
|
||||
</body>
|
||||
|
||||
20
logs_collector/templates/errors.html
Normal file
20
logs_collector/templates/errors.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block errors_head %}
|
||||
<title>{% block title %}{% endblock title %}</title>
|
||||
{% endblock errors_head %}
|
||||
|
||||
{% block http_errors %}
|
||||
<div class="d-flex align-items-center justify-content-center vh-100" >
|
||||
<div class="text-center">
|
||||
<h1 class="display-1 fw-bold">{% block status_code %}{% endblock status_code %}</h1>
|
||||
{% block error_message %}{% endblock error_message %}
|
||||
<a href="{% url 'collector:index' %}" class="btn btn-secondary">Go Home</a>
|
||||
</div>
|
||||
<!-- Theme switcher -->
|
||||
<div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle">
|
||||
{% include 'includes/theme_switcher.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock http_errors %}
|
||||
15
logs_collector/templates/includes/brand.html
Normal file
15
logs_collector/templates/includes/brand.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<a class="navbar-brand" href="{% url 'collector:index' %}">
|
||||
Logs Collector
|
||||
<i class="bi bi-file-earmark-zip-fill"></i>
|
||||
</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="collapse nav fields"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
57
logs_collector/templates/includes/extra_menu.html
Normal file
57
logs_collector/templates/includes/extra_menu.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
href="{% url 'account:show_profile' %}"
|
||||
>
|
||||
<i class="bi bi-person-circle"></i> {{ request.user }}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% if request.user.is_staff %}
|
||||
<li>
|
||||
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
|
||||
><i class="bi bi-shield-shaded"></i> Admin</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a
|
||||
href="{% url 'two_factor:profile' %}"
|
||||
class="dropdown-item"
|
||||
type="button">
|
||||
<i class="bi bi-dice-5"></i> 2FA
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<a
|
||||
href="{% url 'account:logout' %}"
|
||||
class="dropdown-item"
|
||||
type="button"><i class="bi bi-door-closed"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'two_factor:login' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
>
|
||||
<i class="bi bi-box-arrow-in-right"></i></i> Login
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
41
logs_collector/templates/includes/footer.html
Normal file
41
logs_collector/templates/includes/footer.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<button class="btn">
|
||||
v{{ version }}
|
||||
{% if environment != 'Production' %}
|
||||
Staging: {{ environment }}
|
||||
{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
<!-- Separator -->
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
||||
<hr class="d-lg-none my-2 text-white-50">
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class='btn'> © {{ author }} </button>
|
||||
</li>
|
||||
</ul>
|
||||
{% if request.user.is_authenticated %}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<li>
|
||||
<a
|
||||
class="btn"
|
||||
type="button"
|
||||
href="{% url 'swagger-ui' %}"
|
||||
target="_blank"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-html="true"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="Swagger"
|
||||
>
|
||||
<i class="bi bi-braces-asterisk"></i> API
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
47
logs_collector/templates/includes/menu.html
Normal file
47
logs_collector/templates/includes/menu.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% load collector_extras %}
|
||||
{% get_platforms as platforms %}
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link dropdown-toggle"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
><i class="bi bi-filter-circle"></i> Tickets
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" aria-current="page" href="{% url 'collector:create' %}">
|
||||
<i class="bi bi-pencil-square"></i> Create ticket
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<a class="dropdown-item" aria-current="page" href="{% url 'collector:upload' %}">
|
||||
<i class="bi bi-archive"></i>
|
||||
Upload archive
|
||||
</a>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
{% for platform in platforms %}
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
|
||||
href="{{ platform.get_absolute_url }}"
|
||||
>{{ platform.pretty_name}}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item {% if request.GET.resolved %}active{% endif %}"
|
||||
href="{% url 'collector:tickets' %}?resolved=true">
|
||||
<i class="bi bi-check-circle"></i> Resolved
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
|
||||
<i class="bi bi-funnel"></i> Reset filter
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1,150 +1,22 @@
|
||||
{% load collector_extras %}
|
||||
{% get_platforms as platforms %}
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<nav class="navbar navbar-expand-xl bg-body-tertiary">
|
||||
<div class="container">
|
||||
<a
|
||||
class="navbar-brand"
|
||||
href="{% url 'collector:index' %}">Logs Collector <i class="bi bi-file-earmark-zip-fill"></i>
|
||||
</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Переключатель навигации"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<!--Brand logo -->
|
||||
{% include 'includes/brand.html' %}
|
||||
<!-- Left fields -->
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link dropdown-toggle"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>Archives</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'collector:upload' %}">
|
||||
<i class="bi bi-archive"></i> Upload archive
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link dropdown-toggle"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>Tickets</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for platform in platforms %}
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
|
||||
href="{{ platform.get_absolute_url }}"
|
||||
>{{ platform.pretty_name}}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
|
||||
<i class="bi bi-funnel"></i> Reset filter
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="{% url 'collector:create' %}">
|
||||
<i class="bi bi-pencil-square"></i> New
|
||||
</a>
|
||||
</li>
|
||||
<!-- Menu -->
|
||||
{% include 'includes/menu.html' %}
|
||||
<!-- Storage -->
|
||||
{% include 'includes/storage.html' %}
|
||||
</ul>
|
||||
<!-- Search -->
|
||||
<ul class="navbar-nav flex-row flex-wrap me-md-auto">
|
||||
<li class="nav-item py-2 col-12 col-lg-auto">
|
||||
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
|
||||
<input
|
||||
class="form-control me-2"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
name="search"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-success"
|
||||
type="submit">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
{% include 'includes/search.html' %}
|
||||
<!-- Right fields -->
|
||||
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
|
||||
<!-- User settings -->
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-person-square"></i> {{ request.user }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% if request.user.is_staff %}
|
||||
<li>
|
||||
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
|
||||
><i class="bi bi-shield-shaded"></i> Admin</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}"
|
||||
><i class="bi bi-braces-asterisk"></i> Swagger</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="{% url 'two_factor:profile' %}"
|
||||
class="dropdown-item"
|
||||
type="button">
|
||||
<i class="bi bi-gear"></i> Settings
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<a
|
||||
href="{% url 'account:logout' %}"
|
||||
class="dropdown-item"
|
||||
type="button"><i class="bi bi-door-closed"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'two_factor:login' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
>
|
||||
<i class="bi bi-box-arrow-in-right"></i></i> Login
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% include 'includes/extra_menu.html' %}
|
||||
<!-- Separator -->
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
||||
|
||||
21
logs_collector/templates/includes/search.html
Normal file
21
logs_collector/templates/includes/search.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<ul class="navbar-nav flex-row flex-wrap me-md-auto">
|
||||
<li class="nav-item py-2 col-12 col-lg-auto">
|
||||
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
|
||||
<input
|
||||
class="form-control me-2"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
name="search"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="Type the ticket number or comma-separated numbers"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-success"
|
||||
type="submit">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
55
logs_collector/templates/includes/storage.html
Normal file
55
logs_collector/templates/includes/storage.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% if storage %}
|
||||
<li class="nav-item col-lg-auto d-flex align-items-center">
|
||||
<i
|
||||
id="storage_icon"
|
||||
class="nav-link me-1 bi bi-sd-card"
|
||||
aria-current="page"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-html="true"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="
|
||||
<span><u>STORAGE</u><span>
|
||||
<br>
|
||||
Used: {{ storage.used_percent }}%
|
||||
<br>
|
||||
Status:
|
||||
<span class={% if storage.status == 'error' %}text-danger{% else %}text-success{% endif %}>
|
||||
{{ storage.status }}
|
||||
<span>
|
||||
"
|
||||
>
|
||||
</i>
|
||||
<div
|
||||
id="storage_progress_container"
|
||||
class="progress"
|
||||
role="progressbar"
|
||||
aria-label="storage used"
|
||||
aria-valuenow="{{ storage.used_percent }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style="width: 125px"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-html="true"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="
|
||||
Total: {{ storage.total|filesizeformat }}
|
||||
<br>
|
||||
Used: {{ storage.used|filesizeformat }}
|
||||
<br>
|
||||
Free: {{ storage.free|filesizeformat }}
|
||||
"
|
||||
>
|
||||
<div
|
||||
id="storage_progress"
|
||||
class="progress-bar
|
||||
{% if storage.used_percent > 90 %} bg-danger
|
||||
{% elif storage.used_percent > 80 %} bg-warning
|
||||
{% else %} bg-success
|
||||
{% endif %}"
|
||||
style="width: {{ storage.used_percent }}%"
|
||||
storage-url="{% url 'collector_api:storage-info' %}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -1,15 +1,19 @@
|
||||
{% extends "two_factor/_base.html" %}
|
||||
|
||||
{% block content_wrapper %}
|
||||
<section>
|
||||
<header>
|
||||
<header>
|
||||
<section>
|
||||
{% block nav %}{% endblock nav %}
|
||||
</header>
|
||||
</section>
|
||||
<section>
|
||||
<main>
|
||||
<div class="container mt-3">
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<div class="container mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Two-Factor Authentication settings:</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center">
|
||||
{% block content %}{% endblock %}
|
||||
@@ -17,6 +21,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto">
|
||||
<section>
|
||||
{% block footer_nav %}{% endblock footer_nav %}
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
||||
@@ -33,3 +33,5 @@
|
||||
<button class="btn btn-outline-primary" type="submit">{% trans "Generate Tokens" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||
|
||||
@@ -7,10 +7,15 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.asteriskField {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<div class="d-flex min-vh-100 align-items-center py-4" cz-shortcut-listen="true">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3 offset-md-4">
|
||||
<div class="col-md-5 col-xl-3 offset-md-4">
|
||||
<h1>{% block title %}{% trans "Login" %}{% endblock %}</h1>
|
||||
{% if wizard.steps.current == 'auth' %}
|
||||
<p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="d-flex min-vh-100 align-items-center py-4" cz-shortcut-listen="true">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 offset-md-4">
|
||||
<div class="col-md-5 col-xl-4 offset-md-4">
|
||||
<h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
|
||||
|
||||
<p>{% blocktrans trimmed %}The page you requested, enforces users to verify using
|
||||
|
||||
@@ -65,3 +65,5 @@
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||
|
||||
@@ -24,3 +24,5 @@
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
<button class="btn btn-danger" type="submit">{% trans "Disable" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||
@@ -57,3 +57,5 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||
|
||||
297
poetry.lock
generated
297
poetry.lock
generated
@@ -398,13 +398,100 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "phonenumberslite"
|
||||
version = "8.13.19"
|
||||
version = "8.13.20"
|
||||
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "phonenumberslite-8.13.19-py2.py3-none-any.whl", hash = "sha256:163a18e962e4a29ffd0bd523fa7aa60301bc7aa467bf0383bf3c4dac02097573"},
|
||||
{file = "phonenumberslite-8.13.19.tar.gz", hash = "sha256:5d05d0f31f0838b1773c6093925c7c1ab3e98d98747db7c54a9385f8b6df5f71"},
|
||||
{file = "phonenumberslite-8.13.20-py2.py3-none-any.whl", hash = "sha256:f0840de9bfe5b8f10b6883d971dd81f3a53bed8210326a53266f11e8be3428c3"},
|
||||
{file = "phonenumberslite-8.13.20.tar.gz", hash = "sha256:09b07b32861f306dee10f17085afb3e058b4c179edf50deb94db4d4dd14235ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.1.10"
|
||||
description = "PostgreSQL database adapter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "psycopg-3.1.10-py3-none-any.whl", hash = "sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52"},
|
||||
{file = "psycopg-3.1.10.tar.gz", hash = "sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
psycopg-binary = {version = "3.1.10", optional = true, markers = "extra == \"binary\""}
|
||||
typing-extensions = ">=4.1"
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
binary = ["psycopg-binary (==3.1.10)"]
|
||||
c = ["psycopg-c (==3.1.10)"]
|
||||
dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
|
||||
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
pool = ["psycopg-pool"]
|
||||
test = ["anyio (>=3.6.2)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.1.10"
|
||||
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a529c203f6e0f4c67ba27cf8f9739eb3bc880ad70d6ad6c0e56c2230a66b5a09"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd6e14d1aeb12754a43446c77a5ce819b68875cc25ae6538089ef90d7f6dd6f7"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1583ced5948cf88124212c4503dfe5b01ac3e2dd1a2833c083917f4c4aabe8b4"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2098721c486478987be700723b28ec7a48f134eba339de36af0e745f37dfe461"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e61f7b412fca7b15dd043a0b22fd528d2ed8276e76b3764c3889e29fa65082b"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0f33e33a072e3d5af51ee4d4a439e10dbe623fe87ef295d5d688180d529f13f"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f6f7738c59262d8d19154164d99c881ed58ed377fb6f1d685eb0dc43bbcd8022"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:511d38b1e1961d179d47d5103ba9634ecfc7ead431d19a9337ef82f3a2bca807"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:666e7acf2ffdb5e8a58e8b0c1759facdb9688c7e90ee8ca7aed675803b57404d"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57b93c756fee5f7c7bd580c34cd5d244f7d5638f8b2cf25333f97b9b8b2ebfd1"},
|
||||
{file = "psycopg_binary-3.1.10-cp310-cp310-win_amd64.whl", hash = "sha256:a1d61b7724c7215a8ea4495a5c6b704656f4b7bb6165f4cb9989b685886ebc48"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36fff836a7823c9d71fa7faa333c74b2b081af216cebdbb0f481dce55ee2d974"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32caf98cb00881bfcbbbae39a15f2a4e08b79ff983f1c0f13b60a888ef6e8431"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5565a6a86fee8d74f30de89e07f399567cdf59367aeb09624eb690d524339076"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fb0d64520b29bd80a6731476ad8e1c20348dfdee00ab098899d23247b641675"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfc05ed4e74fa8615d7cc2bd57f00f97662f4e865a731dbd43da9a527e289c8c"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5b59c8cff887757ddf438ff9489d79c5e6b717112c96f5c68e16f367ff8724e"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbaf12361136afefc5faab21a174a437e71c803b083f410e5140c7605bc66b"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ff72576061c774bcce5f5440b93e63d4c430032dd056d30f6cb1988e549dd92c"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a4e91e1a8d61c60f592a1dfcebdf55e52a29fe4fdb650c5bd5414c848e77d029"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f7187269d825e84c945be7d93dd5088a4e0b6481a4bdaba3bf7069d4ac13703d"},
|
||||
{file = "psycopg_binary-3.1.10-cp311-cp311-win_amd64.whl", hash = "sha256:ba7812a593c16d9d661844dc8dd4d81548fd1c2a0ee676f3e3d8638369f4c5e4"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88caa5859740507b3596c6c2e00ceaccee2c6ab5317bc535887801ad3cc7f3e1"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a3a7e99ba10c2e83a48d79431560e0d5ca7865f68f2bac3a462dc2b151e9926"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:848f4f4707dc73f4b4e844c92f3de795b2ddb728f75132602bda5e6ba55084fc"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:415961e839bb49cfd75cd961503fb8846c0768f247db1fa7171c1ac61d38711b"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0471869e658d0c6b8c3ed53153794739c18d7dad2dd5b8e6ff023a364c20f7df"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4290060ee0d856caa979ecf675c0e6959325f508272ccf27f64c3801c7bcbde7"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:abf04bc06c8f6a1ac3dc2106d3b79c8661352e9d8a57ca2934ffa6aae8fe600a"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:51fe70708243b83bf16710d8c11b61bd46562e6a24a6300d5434380b35911059"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b658f7f8b49fb60a1c52e3f6692f690a85bdf1ad30aafe0f3f1fd74f6958cf8"},
|
||||
{file = "psycopg_binary-3.1.10-cp37-cp37m-win_amd64.whl", hash = "sha256:ffc8c796194f23b9b07f6d25f927ec4df84a194bbc7a1f9e73316734eef512f9"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74ce92122be34cf0e5f06d79869e1001c8421a68fa7ddf6fe38a717155cf3a64"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:75608a900984061c8898be68fbddc6f3da5eefdffce6e0624f5371645740d172"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6670d160d054466e8fdedfbc749ef8bf7dfdf69296048954d24645dd4d3d3c01"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d32026cfab7ba7ac687a42c33345026a2fb6fc5608a6144077f767af4386be0b"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:908fa388a5b75dfd17a937acb24708bd272e21edefca9a495004c6f70ec2636a"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e46b97073bd4de114f475249d681eaf054e950699c5d7af554d3684db39b82d"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9cf56bb4b115def3a18157f3b3b7d8322ee94a8dea30028db602c8f9ae34ad1e"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3b6c6f90241c4c5a6ca3f0d8827e37ef90fdc4deb9d8cfa5678baa0ea374b391"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:747176a6aeb058079f56c5397bd90339581ab7b3cc0d62e7445654e6a484c7e1"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41a415e78c457b06497fa0084e4ea7245ca1a377b55756dd757034210b64da7e"},
|
||||
{file = "psycopg_binary-3.1.10-cp38-cp38-win_amd64.whl", hash = "sha256:a7bbe9017edd898d7b3a8747700ed045dda96a907dff87f45e642e28d8584481"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f062f20256708929a58c41d44f350efced4c00a603323d1413f6dc0b84d95a5"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dea30f2704337ca2d0322fccfe1fa30f61ce9185de3937eb986321063114a51f"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9d88ac72531034ebf7ec09114e732b066a9078f4ce213cf65cc5e42eb538d30"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2bea0940d69c3e24a72530730952687912893b34c53aa39e79045e7b446174d"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a691dc8e2436d9c1e5cf93902d63e9501688fccc957eb22f952d37886257470"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa92661f99351765673835a4d936d79bd24dfbb358b29b084d83be38229a90e4"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:30eb731ed5525d8df892db6532cc8ffd8a163b73bc355127dee9c49334e16eee"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50bf7a59d3a85a82d466fed341d352b44d09d6adc18656101d163a7cfc6509a0"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f48665947c55f8d6eb3f0be98de80411508e1ec329f354685329b57fced82c7f"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:caa771569da01fc0389ca34920c331a284425a68f92d1ba0a80cc08935f8356e"},
|
||||
{file = "psycopg_binary-3.1.10-cp39-cp39-win_amd64.whl", hash = "sha256:b30887e631fd67affaed98f6cd2135b44f2d1a6d9bca353a69c3889c78bd7aa8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -459,13 +546,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2023.3"
|
||||
version = "2023.3.post1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"},
|
||||
{file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"},
|
||||
{file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
|
||||
{file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -557,108 +644,108 @@ rpds-py = ">=0.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.10.0"
|
||||
version = "0.10.2"
|
||||
description = "Python bindings to Rust's persistent data structures (rpds)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186"},
|
||||
{file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640"},
|
||||
{file = "rpds_py-0.10.0-cp310-none-win32.whl", hash = "sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d"},
|
||||
{file = "rpds_py-0.10.0-cp310-none-win_amd64.whl", hash = "sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7"},
|
||||
{file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055"},
|
||||
{file = "rpds_py-0.10.0-cp311-none-win32.whl", hash = "sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c"},
|
||||
{file = "rpds_py-0.10.0-cp311-none-win_amd64.whl", hash = "sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882"},
|
||||
{file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1"},
|
||||
{file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453"},
|
||||
{file = "rpds_py-0.10.0-cp38-none-win32.whl", hash = "sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e"},
|
||||
{file = "rpds_py-0.10.0-cp38-none-win_amd64.whl", hash = "sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775"},
|
||||
{file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f"},
|
||||
{file = "rpds_py-0.10.0-cp39-none-win32.whl", hash = "sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5"},
|
||||
{file = "rpds_py-0.10.0-cp39-none-win_amd64.whl", hash = "sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a"},
|
||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a"},
|
||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f"},
|
||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa"},
|
||||
{file = "rpds_py-0.10.0.tar.gz", hash = "sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:9f00d54b18dd837f1431d66b076737deb7c29ce3ebb8412ceaf44d5e1954ac0c"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f4d561f4728f825e3b793a53064b606ca0b6fc264f67d09e54af452aafc5b82"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:013d6c784150d10236a74b4094a79d96a256b814457e388fc5a4ba9efe24c402"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd1142d22fdb183a0fff66d79134bf644401437fed874f81066d314c67ee193c"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a0536ed2b9297c75104e1a3da330828ba1b2639fa53b38d396f98bf7e3c68df"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41bd430b7b63aa802c02964e331ac0b177148fef5f807d2c90d05ce71a52b4d4"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e8474f7233fe1949ce4e03bea698a600c2d5d6b51dab6d6e6336dbe69acf23e"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9d7efaad48b859053b90dedd69bc92f2095084251e732e4c57ac9726bcb1e64"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5612b0b1de8d5114520094bd5fc3d04eb8af6f3e10d48ef05b7c8e77c1fd9545"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d5eaf988951f6ecb6854ca3300b87123599c711183c83da7ce39717a7cbdbce"},
|
||||
{file = "rpds_py-0.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75c8766734ac0053e1d683567e65e85306c4ec62631b0591caeb287ac8f72e08"},
|
||||
{file = "rpds_py-0.10.2-cp310-none-win32.whl", hash = "sha256:8de9b88f0cbac73cfed34220d13c57849e62a7099a714b929142425e926d223a"},
|
||||
{file = "rpds_py-0.10.2-cp310-none-win_amd64.whl", hash = "sha256:2275f1a022e2383da5d2d101fe11ccdcbae799148c4b83260a4b9309fa3e1fc2"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dd91a7d7a9ce7f4983097c91ce211f3e5569cc21caa16f2692298a07e396f82b"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e82b4a70cc67094f3f3fd77579702f48fcf1de7bdc67d79b8f1e24d089a6162c"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e281b71922208e00886e4b7ffbfcf27874486364f177418ab676f102130e7ec9"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3eb1a0d2b6d232d1bcdfc3fcc5f7b004ab3fbd9203011a3172f051d4527c0b6"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02945ae38fd78efc40900f509890de84cfd5ffe2cd2939eeb3a8800dc68b87cb"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccfb77f6dc8abffa6f1c7e3975ed9070a41ce5fcc11154d2bead8c1baa940f09"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af52078719209bef33e38131486fd784832dd8d1dc9b85f00a44f6e7437dd021"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56ba7c1100ed079527f2b995bf5486a2e557e6d5b733c52e8947476338815b69"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:899b03a3be785a7e1ff84b237da71f0efa2f021512f147dd34ffdf7aa82cb678"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22e6de18f00583f06928cc8d0993104ecc62f7c6da6478db2255de89a30e45d1"},
|
||||
{file = "rpds_py-0.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edd74b760a6bb950397e7a7bd2f38e6700f6525062650b1d77c6d851b82f02c2"},
|
||||
{file = "rpds_py-0.10.2-cp311-none-win32.whl", hash = "sha256:18909093944727e068ebfc92e2e6ed1c4fa44135507c1c0555213ce211c53214"},
|
||||
{file = "rpds_py-0.10.2-cp311-none-win_amd64.whl", hash = "sha256:9568764e72d85cf7855ca78b48e07ed1be47bf230e2cea8dabda3c95f660b0ff"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:0fc625059b83695fbb4fc8b7a8b66fa94ff9c7b78c84fb9986cd53ff88a28d80"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c86231c66e4f422e7c13ea6200bb4048b3016c8bfd11b4fd0dabd04d2c8e3501"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56777c57246e048908b550af9b81b0ec9cf804fd47cb7502ccd93238bd6025c2"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4cb372e22e9c879bd9a9cc9b20b7c1fbf30a605ac953da45ecec05d8a6e1c77"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa3b3a43dabc4cc57a7800f526cbe03f71c69121e21b863fdf497b59b462b163"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d222086daa55421d599609b32d0ebe544e57654c4a0a1490c54a7ebaa67561"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:529aab727f54a937085184e7436e1d0e19975cf10115eda12d37a683e4ee5342"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e9b1531d6a898bdf086acb75c41265c7ec4331267d7619148d407efc72bd24"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c2772bb95062e3f9774140205cd65d8997e39620715486cf5f843cf4ad8f744c"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba1b28e44f611f3f2b436bd8290050a61db4b59a8e24be4465f44897936b3824"},
|
||||
{file = "rpds_py-0.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5aba767e64b494483ad60c4873bec78d16205a21f8247c99749bd990d9c846c2"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:e1954f4b239d1a92081647eecfd51cbfd08ea16eb743b8af1cd0113258feea14"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:de4a2fd524993578fe093044f291b4b24aab134390030b3b9b5f87fd41ab7e75"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e69737bd56006a86fd5a78b2b85447580a6138c930a75eb9ef39fe03d90782b1"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f40abbcc0a7d9a8a80870af839d317e6932533f98682aabd977add6c53beeb23"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29ec8507664f94cc08457d98cfc41c3cdbddfa8952438e644177a29b04937876"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcde80aefe7054fad6277762fb7e9d35c72ea479a485ae1bb14629c640987b30"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a65de5c02884760a14a58304fb6303f9ddfc582e630f385daea871e1bdb18686"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e92e5817eb6bfed23aa5e45bfe30647b83602bdd6f9e25d63524d4e6258458b0"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2c8fc6c841ada60a86d29c9ebe2e8757c47eda6553f3596c560e59ca6e9b6fa1"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:8557c807388e6617161fe51b1a4747ea8d1133f2d2ad8e79583439abebe58fbd"},
|
||||
{file = "rpds_py-0.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:00e97d43a36811b78fa9ad9d3329bf34f76a31e891a7031a2ac01450c9b168ab"},
|
||||
{file = "rpds_py-0.10.2-cp38-none-win32.whl", hash = "sha256:1ed3d5385d14be894e12a9033be989e012214a9811e7194849c94032ad69682a"},
|
||||
{file = "rpds_py-0.10.2-cp38-none-win_amd64.whl", hash = "sha256:02b4a2e28eb24dac4ef43dda4f6a6f7766e355179b143f7d0c76a1c5488a307b"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:2a55631b93e47956fbc97d69ba2054a8c6a4016f9a3064ec4e031f5f1030cb90"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ffbf1b38c88d0466de542e91b08225d51782282512f8e2b11715126c41fda48"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213f9ef5c02ec2f883c1075d25a873149daadbaea50d18d622e9db55ec9849c2"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b00150a9a3fd0a8efaa90bc2696c105b04039d50763dd1c95a34c88c5966cb57"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab0f7aabdbce4a202e013083eeab71afdb85efa405dc4a06fea98cde81204675"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cd0c9fb5d40887500b4ed818770c68ab4fa6e0395d286f9704be6751b1b7d98"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8578fc6c8bdd0201327503720fa581000b4bd3934abbf07e2628d1ad3de157d"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d27d08056fcd61ff47a0cd8407eff4d3e816c82cb6b9c6f0ce9a0ad49225f81"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c8f6526df47953b07c45b95c4d1da6b9a0861c0e5da0271db96bb1d807825412"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177c033e467a66a054dd3a9534167234a3d0b2e41445807b13b626e01da25d92"},
|
||||
{file = "rpds_py-0.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c74cbee9e532dc34371127f7686d6953e5153a1f22beab7f953d95ee4a0fe09"},
|
||||
{file = "rpds_py-0.10.2-cp39-none-win32.whl", hash = "sha256:05a1382905026bdd560f806c8c7c16e0f3e3fb359ba8868203ca6e5799884968"},
|
||||
{file = "rpds_py-0.10.2-cp39-none-win_amd64.whl", hash = "sha256:3fd503c27e7b7034128e30847ecdb4bff4ca5e60f29ad022a9f66ae8940d54ac"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4a96147791e49e84207dd1530109aa0e9eeaf1c8b7a59f150047fc0fcdf9bb64"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:203eb1532d51591d32e8dfafd60b5d31347ea7278c8da02b4b550287f6abe28b"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2f416cdfe92f5fbb77177f5f3f7830059d1582db05f2c7119bf80069d1ab69b"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2660000e1a113869c86eb5cc07f3343467490f3cd9d0299f81da9ddae7137b7"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1adb04e4b4e41bf30aaa77eeb169c1b9ba9e5010e2e6ce8d6c17e1446edc9b68"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bca97521ee786087f0c5ef318fef3eef0266a9c3deff88205523cf353af7394"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4969592e3cdeefa4cbb15a26cec102cbd4a1d6e5b695fac9fa026e19741138c8"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df61f818edf7c8626bfa392f825860fb670b5f8336e238eb0ec7e2a5689cdded"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b589d93a60e78fe55d5bc76ee8c2bf945dbdbb7cd16044c53e0307604e448de1"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:73da69e1f612c3e682e34dcb971272d90d6f27b2c99acff444ca455a89978574"},
|
||||
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:89438e8885a186c69fe31f7ef98bb2bf29688c466c3caf9060f404c0be89ae80"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c4ecc4e9a5d73a816cae36ee6b5d8b7a0c72013cae1e101406e832887c3dc2d8"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:907b214da5d2fcff0b6ddb83de1333890ca92abaf4bbf8d9c61dc1b95c87fd6e"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb44644371eaa29a3aba7b69b1862d0d56f073bb7585baa32e4271a71a91ee82"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:80c3cf46511653f94dfe07c7c79ab105c4164d6e1dfcb35b7214fb9af53eaef4"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaba0613c759ebf95988a84f766ca6b7432d55ce399194f95dde588ad1be0878"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0527c97dcd8bb983822ee31d3760187083fd3ba18ac4dd22cf5347c89d5628f4"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cdfd649011ce2d90cb0dd304c5aba1190fac0c266d19a9e2b96b81cfd150a09"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75eea40355a8690459c7291ce6c8ce39c27bd223675c7da6619f510c728feb97"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1b804cfad04f862d6a84af9d1ad941b06f671878f0f7ecad6c92007d423de6"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:bf77f9017fcfa1232f98598a637406e6c33982ccba8a5922339575c3e2b90ea5"},
|
||||
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:46c4c550bf59ce05d6bff2c98053822549aaf9fbaf81103edea325e03350bca1"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:46af4a742b90c7460e94214f923452c2c1d050a9da1d2b8d4c70cbc045e692b7"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2a86d246a160d98d820ee7d02dc18c923c228de095be362e57b9fd8970b2c4a1"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae141c9017f8f473a6ee07a9425da021816a9f8c0683c2e5442f0ccf56b0fc62"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1147bc3d0dd1e549d991110d0a09557ec9f925dbc1ca62871fcdab2ec9d716b"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fce7a8ee8d0f682c953c0188735d823f0fcb62779bf92cd6ba473a8e730e26ad"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c7f9d70f99e1fbcbf57c75328b80e1c0a7f6cad43e75efa90a97221be5efe15"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b309908b6ff5ffbf6394818cb73b5a2a74073acee2c57fe8719046389aeff0d"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ff1f585a0fdc1415bd733b804f33d386064a308672249b14828130dd43e7c31"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0188b580c490bccb031e9b67e9e8c695a3c44ac5e06218b152361eca847317c3"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:abe081453166e206e3a8c6d8ace57214c17b6d9477d7601ac14a365344dbc1f4"},
|
||||
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9118de88c16947eaf5b92f749e65b0501ea69e7c2be7bd6aefc12551622360e1"},
|
||||
{file = "rpds_py-0.10.2.tar.gz", hash = "sha256:289073f68452b96e70990085324be7223944c7409973d13ddfe0eea1c1b5663b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -727,4 +814,4 @@ brotli = ["Brotli"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "39c4da284ce6727245ff8e62ae325111cccd55175f38bc34045cbf2ef153d871"
|
||||
content-hash = "9c8a0b848ed1ebc1da6cafbf9b769f72f8a70e15ed4a226544c956ef69a4f750"
|
||||
|
||||
@@ -20,7 +20,8 @@ django-filter = "23.2"
|
||||
drf-spectacular = "0.26.4"
|
||||
django-two-factor-auth = {extras = ["phonenumberslite"], version = "1.15.3"}
|
||||
whitenoise = "6.5.0"
|
||||
gunicorn = "21.2.0"
|
||||
gunicorn = {extras = ["gthread"], version = "21.2.0"}
|
||||
psycopg = {extras = ["binary"], version = "3.1.10"}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "^6.0.0"
|
||||
|
||||
268
requirements.txt
268
requirements.txt
@@ -46,7 +46,7 @@ djangorestframework==3.14.0 ; python_version >= "3.10" and python_version < "4.0
|
||||
drf-spectacular==0.26.4 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:8f5a8f87353d1bb8dcb3f3909b7109b2dcbe1d91f3e069409cf322963e140bd6 \
|
||||
--hash=sha256:afeccc6533dcdb4e78afbfcc49f3c5e9c369aeb62f965e4d1a43b165449c147a
|
||||
gunicorn==21.2.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
gunicorn[gthread]==21.2.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \
|
||||
--hash=sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033
|
||||
inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
@@ -64,18 +64,76 @@ markdown==3.4.4 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
packaging==23.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
|
||||
--hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
|
||||
phonenumberslite==8.13.19 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:163a18e962e4a29ffd0bd523fa7aa60301bc7aa467bf0383bf3c4dac02097573 \
|
||||
--hash=sha256:5d05d0f31f0838b1773c6093925c7c1ab3e98d98747db7c54a9385f8b6df5f71
|
||||
phonenumberslite==8.13.20 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:09b07b32861f306dee10f17085afb3e058b4c179edf50deb94db4d4dd14235ff \
|
||||
--hash=sha256:f0840de9bfe5b8f10b6883d971dd81f3a53bed8210326a53266f11e8be3428c3
|
||||
psycopg-binary==3.1.10 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0471869e658d0c6b8c3ed53153794739c18d7dad2dd5b8e6ff023a364c20f7df \
|
||||
--hash=sha256:0f062f20256708929a58c41d44f350efced4c00a603323d1413f6dc0b84d95a5 \
|
||||
--hash=sha256:1583ced5948cf88124212c4503dfe5b01ac3e2dd1a2833c083917f4c4aabe8b4 \
|
||||
--hash=sha256:1e46b97073bd4de114f475249d681eaf054e950699c5d7af554d3684db39b82d \
|
||||
--hash=sha256:2098721c486478987be700723b28ec7a48f134eba339de36af0e745f37dfe461 \
|
||||
--hash=sha256:30eb731ed5525d8df892db6532cc8ffd8a163b73bc355127dee9c49334e16eee \
|
||||
--hash=sha256:32caf98cb00881bfcbbbae39a15f2a4e08b79ff983f1c0f13b60a888ef6e8431 \
|
||||
--hash=sha256:36fff836a7823c9d71fa7faa333c74b2b081af216cebdbb0f481dce55ee2d974 \
|
||||
--hash=sha256:3b6c6f90241c4c5a6ca3f0d8827e37ef90fdc4deb9d8cfa5678baa0ea374b391 \
|
||||
--hash=sha256:415961e839bb49cfd75cd961503fb8846c0768f247db1fa7171c1ac61d38711b \
|
||||
--hash=sha256:41a415e78c457b06497fa0084e4ea7245ca1a377b55756dd757034210b64da7e \
|
||||
--hash=sha256:4290060ee0d856caa979ecf675c0e6959325f508272ccf27f64c3801c7bcbde7 \
|
||||
--hash=sha256:4a3a7e99ba10c2e83a48d79431560e0d5ca7865f68f2bac3a462dc2b151e9926 \
|
||||
--hash=sha256:50bf7a59d3a85a82d466fed341d352b44d09d6adc18656101d163a7cfc6509a0 \
|
||||
--hash=sha256:511d38b1e1961d179d47d5103ba9634ecfc7ead431d19a9337ef82f3a2bca807 \
|
||||
--hash=sha256:51fe70708243b83bf16710d8c11b61bd46562e6a24a6300d5434380b35911059 \
|
||||
--hash=sha256:5565a6a86fee8d74f30de89e07f399567cdf59367aeb09624eb690d524339076 \
|
||||
--hash=sha256:57b93c756fee5f7c7bd580c34cd5d244f7d5638f8b2cf25333f97b9b8b2ebfd1 \
|
||||
--hash=sha256:666e7acf2ffdb5e8a58e8b0c1759facdb9688c7e90ee8ca7aed675803b57404d \
|
||||
--hash=sha256:6670d160d054466e8fdedfbc749ef8bf7dfdf69296048954d24645dd4d3d3c01 \
|
||||
--hash=sha256:6a691dc8e2436d9c1e5cf93902d63e9501688fccc957eb22f952d37886257470 \
|
||||
--hash=sha256:747176a6aeb058079f56c5397bd90339581ab7b3cc0d62e7445654e6a484c7e1 \
|
||||
--hash=sha256:74ce92122be34cf0e5f06d79869e1001c8421a68fa7ddf6fe38a717155cf3a64 \
|
||||
--hash=sha256:75608a900984061c8898be68fbddc6f3da5eefdffce6e0624f5371645740d172 \
|
||||
--hash=sha256:7e61f7b412fca7b15dd043a0b22fd528d2ed8276e76b3764c3889e29fa65082b \
|
||||
--hash=sha256:848f4f4707dc73f4b4e844c92f3de795b2ddb728f75132602bda5e6ba55084fc \
|
||||
--hash=sha256:88caa5859740507b3596c6c2e00ceaccee2c6ab5317bc535887801ad3cc7f3e1 \
|
||||
--hash=sha256:8b658f7f8b49fb60a1c52e3f6692f690a85bdf1ad30aafe0f3f1fd74f6958cf8 \
|
||||
--hash=sha256:908fa388a5b75dfd17a937acb24708bd272e21edefca9a495004c6f70ec2636a \
|
||||
--hash=sha256:9cf56bb4b115def3a18157f3b3b7d8322ee94a8dea30028db602c8f9ae34ad1e \
|
||||
--hash=sha256:9fb0d64520b29bd80a6731476ad8e1c20348dfdee00ab098899d23247b641675 \
|
||||
--hash=sha256:a1d61b7724c7215a8ea4495a5c6b704656f4b7bb6165f4cb9989b685886ebc48 \
|
||||
--hash=sha256:a4cbaf12361136afefc5faab21a174a437e71c803b083f410e5140c7605bc66b \
|
||||
--hash=sha256:a4e91e1a8d61c60f592a1dfcebdf55e52a29fe4fdb650c5bd5414c848e77d029 \
|
||||
--hash=sha256:a529c203f6e0f4c67ba27cf8f9739eb3bc880ad70d6ad6c0e56c2230a66b5a09 \
|
||||
--hash=sha256:a7bbe9017edd898d7b3a8747700ed045dda96a907dff87f45e642e28d8584481 \
|
||||
--hash=sha256:abf04bc06c8f6a1ac3dc2106d3b79c8661352e9d8a57ca2934ffa6aae8fe600a \
|
||||
--hash=sha256:b30887e631fd67affaed98f6cd2135b44f2d1a6d9bca353a69c3889c78bd7aa8 \
|
||||
--hash=sha256:b9d88ac72531034ebf7ec09114e732b066a9078f4ce213cf65cc5e42eb538d30 \
|
||||
--hash=sha256:ba7812a593c16d9d661844dc8dd4d81548fd1c2a0ee676f3e3d8638369f4c5e4 \
|
||||
--hash=sha256:bd6e14d1aeb12754a43446c77a5ce819b68875cc25ae6538089ef90d7f6dd6f7 \
|
||||
--hash=sha256:bfc05ed4e74fa8615d7cc2bd57f00f97662f4e865a731dbd43da9a527e289c8c \
|
||||
--hash=sha256:c5b59c8cff887757ddf438ff9489d79c5e6b717112c96f5c68e16f367ff8724e \
|
||||
--hash=sha256:caa771569da01fc0389ca34920c331a284425a68f92d1ba0a80cc08935f8356e \
|
||||
--hash=sha256:d32026cfab7ba7ac687a42c33345026a2fb6fc5608a6144077f767af4386be0b \
|
||||
--hash=sha256:dea30f2704337ca2d0322fccfe1fa30f61ce9185de3937eb986321063114a51f \
|
||||
--hash=sha256:e0f33e33a072e3d5af51ee4d4a439e10dbe623fe87ef295d5d688180d529f13f \
|
||||
--hash=sha256:f2bea0940d69c3e24a72530730952687912893b34c53aa39e79045e7b446174d \
|
||||
--hash=sha256:f48665947c55f8d6eb3f0be98de80411508e1ec329f354685329b57fced82c7f \
|
||||
--hash=sha256:f6f7738c59262d8d19154164d99c881ed58ed377fb6f1d685eb0dc43bbcd8022 \
|
||||
--hash=sha256:f7187269d825e84c945be7d93dd5088a4e0b6481a4bdaba3bf7069d4ac13703d \
|
||||
--hash=sha256:fa92661f99351765673835a4d936d79bd24dfbb358b29b084d83be38229a90e4 \
|
||||
--hash=sha256:ff72576061c774bcce5f5440b93e63d4c430032dd056d30f6cb1988e549dd92c \
|
||||
--hash=sha256:ffc8c796194f23b9b07f6d25f927ec4df84a194bbc7a1f9e73316734eef512f9
|
||||
psycopg[binary]==3.1.10 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6 \
|
||||
--hash=sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52
|
||||
pyjwt==2.8.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \
|
||||
--hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320
|
||||
pypng==0.20220715.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c \
|
||||
--hash=sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1
|
||||
pytz==2023.3 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
|
||||
--hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
|
||||
pytz==2023.3.post1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b \
|
||||
--hash=sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7
|
||||
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
|
||||
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
|
||||
@@ -123,104 +181,104 @@ qrcode==7.4.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
referencing==0.30.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf \
|
||||
--hash=sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0
|
||||
rpds-py==0.10.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a \
|
||||
--hash=sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e \
|
||||
--hash=sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e \
|
||||
--hash=sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225 \
|
||||
--hash=sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d \
|
||||
--hash=sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775 \
|
||||
--hash=sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c \
|
||||
--hash=sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59 \
|
||||
--hash=sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5 \
|
||||
--hash=sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173 \
|
||||
--hash=sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74 \
|
||||
--hash=sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640 \
|
||||
--hash=sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef \
|
||||
--hash=sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474 \
|
||||
--hash=sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370 \
|
||||
--hash=sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b \
|
||||
--hash=sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5 \
|
||||
--hash=sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa \
|
||||
--hash=sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a \
|
||||
--hash=sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3 \
|
||||
--hash=sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5 \
|
||||
--hash=sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48 \
|
||||
--hash=sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17 \
|
||||
--hash=sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397 \
|
||||
--hash=sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6 \
|
||||
--hash=sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd \
|
||||
--hash=sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3 \
|
||||
--hash=sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca \
|
||||
--hash=sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a \
|
||||
--hash=sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e \
|
||||
--hash=sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c \
|
||||
--hash=sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24 \
|
||||
--hash=sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86 \
|
||||
--hash=sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f \
|
||||
--hash=sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e \
|
||||
--hash=sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f \
|
||||
--hash=sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83 \
|
||||
--hash=sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882 \
|
||||
--hash=sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e \
|
||||
--hash=sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00 \
|
||||
--hash=sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212 \
|
||||
--hash=sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1 \
|
||||
--hash=sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309 \
|
||||
--hash=sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c \
|
||||
--hash=sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf \
|
||||
--hash=sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517 \
|
||||
--hash=sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696 \
|
||||
--hash=sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa \
|
||||
--hash=sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582 \
|
||||
--hash=sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3 \
|
||||
--hash=sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291 \
|
||||
--hash=sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a \
|
||||
--hash=sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b \
|
||||
--hash=sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727 \
|
||||
--hash=sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b \
|
||||
--hash=sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878 \
|
||||
--hash=sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d \
|
||||
--hash=sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279 \
|
||||
--hash=sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07 \
|
||||
--hash=sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012 \
|
||||
--hash=sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b \
|
||||
--hash=sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453 \
|
||||
--hash=sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12 \
|
||||
--hash=sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09 \
|
||||
--hash=sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795 \
|
||||
--hash=sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04 \
|
||||
--hash=sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c \
|
||||
--hash=sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8 \
|
||||
--hash=sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1 \
|
||||
--hash=sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060 \
|
||||
--hash=sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb \
|
||||
--hash=sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2 \
|
||||
--hash=sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6 \
|
||||
--hash=sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31 \
|
||||
--hash=sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da \
|
||||
--hash=sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af \
|
||||
--hash=sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4 \
|
||||
--hash=sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685 \
|
||||
--hash=sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186 \
|
||||
--hash=sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103 \
|
||||
--hash=sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f \
|
||||
--hash=sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b \
|
||||
--hash=sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d \
|
||||
--hash=sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a \
|
||||
--hash=sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f \
|
||||
--hash=sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0 \
|
||||
--hash=sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe \
|
||||
--hash=sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d \
|
||||
--hash=sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2 \
|
||||
--hash=sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055 \
|
||||
--hash=sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47 \
|
||||
--hash=sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514 \
|
||||
--hash=sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7 \
|
||||
--hash=sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41 \
|
||||
--hash=sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b \
|
||||
--hash=sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910 \
|
||||
--hash=sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc
|
||||
rpds-py==0.10.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:00e97d43a36811b78fa9ad9d3329bf34f76a31e891a7031a2ac01450c9b168ab \
|
||||
--hash=sha256:013d6c784150d10236a74b4094a79d96a256b814457e388fc5a4ba9efe24c402 \
|
||||
--hash=sha256:0188b580c490bccb031e9b67e9e8c695a3c44ac5e06218b152361eca847317c3 \
|
||||
--hash=sha256:02945ae38fd78efc40900f509890de84cfd5ffe2cd2939eeb3a8800dc68b87cb \
|
||||
--hash=sha256:02b4a2e28eb24dac4ef43dda4f6a6f7766e355179b143f7d0c76a1c5488a307b \
|
||||
--hash=sha256:0527c97dcd8bb983822ee31d3760187083fd3ba18ac4dd22cf5347c89d5628f4 \
|
||||
--hash=sha256:05a1382905026bdd560f806c8c7c16e0f3e3fb359ba8868203ca6e5799884968 \
|
||||
--hash=sha256:0b309908b6ff5ffbf6394818cb73b5a2a74073acee2c57fe8719046389aeff0d \
|
||||
--hash=sha256:0fc625059b83695fbb4fc8b7a8b66fa94ff9c7b78c84fb9986cd53ff88a28d80 \
|
||||
--hash=sha256:177c033e467a66a054dd3a9534167234a3d0b2e41445807b13b626e01da25d92 \
|
||||
--hash=sha256:18909093944727e068ebfc92e2e6ed1c4fa44135507c1c0555213ce211c53214 \
|
||||
--hash=sha256:1adb04e4b4e41bf30aaa77eeb169c1b9ba9e5010e2e6ce8d6c17e1446edc9b68 \
|
||||
--hash=sha256:1ed3d5385d14be894e12a9033be989e012214a9811e7194849c94032ad69682a \
|
||||
--hash=sha256:203eb1532d51591d32e8dfafd60b5d31347ea7278c8da02b4b550287f6abe28b \
|
||||
--hash=sha256:213f9ef5c02ec2f883c1075d25a873149daadbaea50d18d622e9db55ec9849c2 \
|
||||
--hash=sha256:2275f1a022e2383da5d2d101fe11ccdcbae799148c4b83260a4b9309fa3e1fc2 \
|
||||
--hash=sha256:22e6de18f00583f06928cc8d0993104ecc62f7c6da6478db2255de89a30e45d1 \
|
||||
--hash=sha256:289073f68452b96e70990085324be7223944c7409973d13ddfe0eea1c1b5663b \
|
||||
--hash=sha256:29ec8507664f94cc08457d98cfc41c3cdbddfa8952438e644177a29b04937876 \
|
||||
--hash=sha256:2a55631b93e47956fbc97d69ba2054a8c6a4016f9a3064ec4e031f5f1030cb90 \
|
||||
--hash=sha256:2a86d246a160d98d820ee7d02dc18c923c228de095be362e57b9fd8970b2c4a1 \
|
||||
--hash=sha256:2bca97521ee786087f0c5ef318fef3eef0266a9c3deff88205523cf353af7394 \
|
||||
--hash=sha256:2c8fc6c841ada60a86d29c9ebe2e8757c47eda6553f3596c560e59ca6e9b6fa1 \
|
||||
--hash=sha256:2cd0c9fb5d40887500b4ed818770c68ab4fa6e0395d286f9704be6751b1b7d98 \
|
||||
--hash=sha256:2d27d08056fcd61ff47a0cd8407eff4d3e816c82cb6b9c6f0ce9a0ad49225f81 \
|
||||
--hash=sha256:2ffbf1b38c88d0466de542e91b08225d51782282512f8e2b11715126c41fda48 \
|
||||
--hash=sha256:3fd503c27e7b7034128e30847ecdb4bff4ca5e60f29ad022a9f66ae8940d54ac \
|
||||
--hash=sha256:3ff1f585a0fdc1415bd733b804f33d386064a308672249b14828130dd43e7c31 \
|
||||
--hash=sha256:41bd430b7b63aa802c02964e331ac0b177148fef5f807d2c90d05ce71a52b4d4 \
|
||||
--hash=sha256:43e9b1531d6a898bdf086acb75c41265c7ec4331267d7619148d407efc72bd24 \
|
||||
--hash=sha256:46af4a742b90c7460e94214f923452c2c1d050a9da1d2b8d4c70cbc045e692b7 \
|
||||
--hash=sha256:46c4c550bf59ce05d6bff2c98053822549aaf9fbaf81103edea325e03350bca1 \
|
||||
--hash=sha256:4969592e3cdeefa4cbb15a26cec102cbd4a1d6e5b695fac9fa026e19741138c8 \
|
||||
--hash=sha256:4a0536ed2b9297c75104e1a3da330828ba1b2639fa53b38d396f98bf7e3c68df \
|
||||
--hash=sha256:4a96147791e49e84207dd1530109aa0e9eeaf1c8b7a59f150047fc0fcdf9bb64 \
|
||||
--hash=sha256:4c7f9d70f99e1fbcbf57c75328b80e1c0a7f6cad43e75efa90a97221be5efe15 \
|
||||
--hash=sha256:4e8474f7233fe1949ce4e03bea698a600c2d5d6b51dab6d6e6336dbe69acf23e \
|
||||
--hash=sha256:4f1b804cfad04f862d6a84af9d1ad941b06f671878f0f7ecad6c92007d423de6 \
|
||||
--hash=sha256:529aab727f54a937085184e7436e1d0e19975cf10115eda12d37a683e4ee5342 \
|
||||
--hash=sha256:5612b0b1de8d5114520094bd5fc3d04eb8af6f3e10d48ef05b7c8e77c1fd9545 \
|
||||
--hash=sha256:56777c57246e048908b550af9b81b0ec9cf804fd47cb7502ccd93238bd6025c2 \
|
||||
--hash=sha256:56ba7c1100ed079527f2b995bf5486a2e557e6d5b733c52e8947476338815b69 \
|
||||
--hash=sha256:59d222086daa55421d599609b32d0ebe544e57654c4a0a1490c54a7ebaa67561 \
|
||||
--hash=sha256:5aba767e64b494483ad60c4873bec78d16205a21f8247c99749bd990d9c846c2 \
|
||||
--hash=sha256:5d5eaf988951f6ecb6854ca3300b87123599c711183c83da7ce39717a7cbdbce \
|
||||
--hash=sha256:73da69e1f612c3e682e34dcb971272d90d6f27b2c99acff444ca455a89978574 \
|
||||
--hash=sha256:75c8766734ac0053e1d683567e65e85306c4ec62631b0591caeb287ac8f72e08 \
|
||||
--hash=sha256:75eea40355a8690459c7291ce6c8ce39c27bd223675c7da6619f510c728feb97 \
|
||||
--hash=sha256:80c3cf46511653f94dfe07c7c79ab105c4164d6e1dfcb35b7214fb9af53eaef4 \
|
||||
--hash=sha256:8557c807388e6617161fe51b1a4747ea8d1133f2d2ad8e79583439abebe58fbd \
|
||||
--hash=sha256:89438e8885a186c69fe31f7ef98bb2bf29688c466c3caf9060f404c0be89ae80 \
|
||||
--hash=sha256:899b03a3be785a7e1ff84b237da71f0efa2f021512f147dd34ffdf7aa82cb678 \
|
||||
--hash=sha256:8de9b88f0cbac73cfed34220d13c57849e62a7099a714b929142425e926d223a \
|
||||
--hash=sha256:8f4d561f4728f825e3b793a53064b606ca0b6fc264f67d09e54af452aafc5b82 \
|
||||
--hash=sha256:907b214da5d2fcff0b6ddb83de1333890ca92abaf4bbf8d9c61dc1b95c87fd6e \
|
||||
--hash=sha256:9118de88c16947eaf5b92f749e65b0501ea69e7c2be7bd6aefc12551622360e1 \
|
||||
--hash=sha256:9568764e72d85cf7855ca78b48e07ed1be47bf230e2cea8dabda3c95f660b0ff \
|
||||
--hash=sha256:9c74cbee9e532dc34371127f7686d6953e5153a1f22beab7f953d95ee4a0fe09 \
|
||||
--hash=sha256:9cdfd649011ce2d90cb0dd304c5aba1190fac0c266d19a9e2b96b81cfd150a09 \
|
||||
--hash=sha256:9f00d54b18dd837f1431d66b076737deb7c29ce3ebb8412ceaf44d5e1954ac0c \
|
||||
--hash=sha256:a2f416cdfe92f5fbb77177f5f3f7830059d1582db05f2c7119bf80069d1ab69b \
|
||||
--hash=sha256:a4cb372e22e9c879bd9a9cc9b20b7c1fbf30a605ac953da45ecec05d8a6e1c77 \
|
||||
--hash=sha256:a65de5c02884760a14a58304fb6303f9ddfc582e630f385daea871e1bdb18686 \
|
||||
--hash=sha256:aa3b3a43dabc4cc57a7800f526cbe03f71c69121e21b863fdf497b59b462b163 \
|
||||
--hash=sha256:ab0f7aabdbce4a202e013083eeab71afdb85efa405dc4a06fea98cde81204675 \
|
||||
--hash=sha256:abe081453166e206e3a8c6d8ace57214c17b6d9477d7601ac14a365344dbc1f4 \
|
||||
--hash=sha256:ae141c9017f8f473a6ee07a9425da021816a9f8c0683c2e5442f0ccf56b0fc62 \
|
||||
--hash=sha256:af52078719209bef33e38131486fd784832dd8d1dc9b85f00a44f6e7437dd021 \
|
||||
--hash=sha256:b00150a9a3fd0a8efaa90bc2696c105b04039d50763dd1c95a34c88c5966cb57 \
|
||||
--hash=sha256:b2660000e1a113869c86eb5cc07f3343467490f3cd9d0299f81da9ddae7137b7 \
|
||||
--hash=sha256:b3eb1a0d2b6d232d1bcdfc3fcc5f7b004ab3fbd9203011a3172f051d4527c0b6 \
|
||||
--hash=sha256:b589d93a60e78fe55d5bc76ee8c2bf945dbdbb7cd16044c53e0307604e448de1 \
|
||||
--hash=sha256:b8578fc6c8bdd0201327503720fa581000b4bd3934abbf07e2628d1ad3de157d \
|
||||
--hash=sha256:ba1b28e44f611f3f2b436bd8290050a61db4b59a8e24be4465f44897936b3824 \
|
||||
--hash=sha256:bb44644371eaa29a3aba7b69b1862d0d56f073bb7585baa32e4271a71a91ee82 \
|
||||
--hash=sha256:bcde80aefe7054fad6277762fb7e9d35c72ea479a485ae1bb14629c640987b30 \
|
||||
--hash=sha256:bd1142d22fdb183a0fff66d79134bf644401437fed874f81066d314c67ee193c \
|
||||
--hash=sha256:bf77f9017fcfa1232f98598a637406e6c33982ccba8a5922339575c3e2b90ea5 \
|
||||
--hash=sha256:c2772bb95062e3f9774140205cd65d8997e39620715486cf5f843cf4ad8f744c \
|
||||
--hash=sha256:c4ecc4e9a5d73a816cae36ee6b5d8b7a0c72013cae1e101406e832887c3dc2d8 \
|
||||
--hash=sha256:c86231c66e4f422e7c13ea6200bb4048b3016c8bfd11b4fd0dabd04d2c8e3501 \
|
||||
--hash=sha256:c8f6526df47953b07c45b95c4d1da6b9a0861c0e5da0271db96bb1d807825412 \
|
||||
--hash=sha256:ccfb77f6dc8abffa6f1c7e3975ed9070a41ce5fcc11154d2bead8c1baa940f09 \
|
||||
--hash=sha256:d9d7efaad48b859053b90dedd69bc92f2095084251e732e4c57ac9726bcb1e64 \
|
||||
--hash=sha256:dd91a7d7a9ce7f4983097c91ce211f3e5569cc21caa16f2692298a07e396f82b \
|
||||
--hash=sha256:de4a2fd524993578fe093044f291b4b24aab134390030b3b9b5f87fd41ab7e75 \
|
||||
--hash=sha256:df61f818edf7c8626bfa392f825860fb670b5f8336e238eb0ec7e2a5689cdded \
|
||||
--hash=sha256:e1147bc3d0dd1e549d991110d0a09557ec9f925dbc1ca62871fcdab2ec9d716b \
|
||||
--hash=sha256:e1954f4b239d1a92081647eecfd51cbfd08ea16eb743b8af1cd0113258feea14 \
|
||||
--hash=sha256:e281b71922208e00886e4b7ffbfcf27874486364f177418ab676f102130e7ec9 \
|
||||
--hash=sha256:e69737bd56006a86fd5a78b2b85447580a6138c930a75eb9ef39fe03d90782b1 \
|
||||
--hash=sha256:e82b4a70cc67094f3f3fd77579702f48fcf1de7bdc67d79b8f1e24d089a6162c \
|
||||
--hash=sha256:e92e5817eb6bfed23aa5e45bfe30647b83602bdd6f9e25d63524d4e6258458b0 \
|
||||
--hash=sha256:eaba0613c759ebf95988a84f766ca6b7432d55ce399194f95dde588ad1be0878 \
|
||||
--hash=sha256:edd74b760a6bb950397e7a7bd2f38e6700f6525062650b1d77c6d851b82f02c2 \
|
||||
--hash=sha256:f40abbcc0a7d9a8a80870af839d317e6932533f98682aabd977add6c53beeb23 \
|
||||
--hash=sha256:fce7a8ee8d0f682c953c0188735d823f0fcb62779bf92cd6ba473a8e730e26ad
|
||||
sqlparse==0.4.4 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \
|
||||
--hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c
|
||||
|
||||
104
scripts/entrypoint.sh
Executable file
104
scripts/entrypoint.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
|
||||
# set global variables:
|
||||
_DB_URL="${DB_URL}"
|
||||
|
||||
|
||||
# parses the database connection url and sets global variables:
|
||||
#
|
||||
# _DBMS: {psql, sqlite} default dbms used
|
||||
# _DB_HOST {domain,ip_address} for connections
|
||||
# _DB_PORT {5432,3306,etc} for connections
|
||||
# _DB_USER {root,admin,etc} db username
|
||||
# _DB_PASSWORD {super_secret} db user password
|
||||
parse_db_url() {
|
||||
local db_url=$1
|
||||
echo "-------- Check used DBMS --------"
|
||||
if [ -n "${db_url}" ]; then
|
||||
# ! PARSING:
|
||||
# -- -- --
|
||||
# extract the protocol
|
||||
local proto="$(echo "${db_url}" | grep '://' | sed -e's,^\(.*://\).*,\1,g')"
|
||||
local dbms=${proto%:*}
|
||||
# if dpms is "sqlite" we do not need parse url args
|
||||
if [ "${dbms}" = "psql" ]; then
|
||||
# remove the protocol (clean url)
|
||||
local url=$(echo "${db_url}" | sed -e s,"${proto}",,g)
|
||||
# extract the user and password (if any)
|
||||
local userpass="$(echo "${url}" | grep @ | cut -d@ -f1)"
|
||||
local password=$(echo "${userpass}" | grep : | cut -d: -f2)
|
||||
# extract the host and port connection
|
||||
local hostport=$(echo "${url}" | sed -e s,$userpass@,,g | cut -d/ -f1)
|
||||
local port=$(echo "${hostport}" | grep : | cut -d: -f2)
|
||||
# ! SET VARIABLES:
|
||||
# -- -- -- -- --
|
||||
# _DB_HOST or/and _DB_PORT for test db connection
|
||||
if [ -n "${port}" ]; then
|
||||
_DB_HOST=$(echo "${hostport}" | grep : | cut -d: -f1)
|
||||
_DB_PORT="${port}"
|
||||
else
|
||||
_DB_HOST="${hostport}"
|
||||
fi
|
||||
# _DB_USER or/and _DB_PASSWORD for future features
|
||||
if [ -n "${password}" ]; then
|
||||
_DB_USER=$(echo "${userpass}" | grep : | cut -d: -f1)
|
||||
_DB_PASSWORD="${password}"
|
||||
else
|
||||
_DB_USER="${userpass}"
|
||||
fi
|
||||
fi
|
||||
# _DBMS (sqlite or psql)
|
||||
_DBMS="${dbms}"
|
||||
echo "${_DBMS} is set as the default DBMS"
|
||||
else
|
||||
# _DBMS (sqlite)
|
||||
_DBMS="sqlite"
|
||||
echo "${_DBMS} is set as the default DBMS"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# waits until the database becomes available for connection
|
||||
wait_db() {
|
||||
local dbms=$1
|
||||
local db_host=$2
|
||||
local db_port=$3
|
||||
if [ "${dbms}" = "psql" ];then
|
||||
echo "------- Waiting for database start -------"
|
||||
while ! nc -z $db_host $db_port; do
|
||||
sleep 0.1
|
||||
done
|
||||
echo "PostgreSQL started"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# app config performs database migration, collects static files
|
||||
app_config() {
|
||||
echo "-------- Apply migration --------"
|
||||
python manage.py migrate --no-input
|
||||
echo "----- Collect static files ------"
|
||||
python manage.py collectstatic --no-input
|
||||
}
|
||||
|
||||
|
||||
# run app wsgi web server
|
||||
app_run() {
|
||||
echo "--------- Run gunicorn ----------"
|
||||
gunicorn logs_collector.wsgi:application
|
||||
}
|
||||
|
||||
|
||||
# app entrypoint
|
||||
main() {
|
||||
parse_db_url $_DB_URL
|
||||
wait_db $_DBMS $_DB_HOST $_DB_PORT
|
||||
app_config
|
||||
app_run
|
||||
}
|
||||
|
||||
# RUN
|
||||
main
|
||||
|
||||
exec "$@"
|
||||
159
scripts/uploader.sh
Executable file
159
scripts/uploader.sh
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
# INIT GLOBAL VARIABLES:
|
||||
_VERSION="0.1.0"
|
||||
_PACKGMGR="apt yum"
|
||||
_SCRIPT_NAME="$0"
|
||||
_CMD="curl"
|
||||
_FILE=""
|
||||
_TOKEN=""
|
||||
_URL=""
|
||||
|
||||
|
||||
# Colorize output
|
||||
# Usage - $(colorize CYAN "Hello, friend!")
|
||||
colorize() {
|
||||
local RED="\033[0;31m"
|
||||
local GREEN="\033[0;32m" # <-- [0 means not bold
|
||||
local YELLOW="\033[1;33m" # <-- [1 means bold
|
||||
local BLUE="\033[0;34m"
|
||||
local MAGNETA="\033[0;35"
|
||||
local CYAN="\033[1;36m"
|
||||
# ... Add more colors if you like
|
||||
|
||||
local NC="\033[0m" # No Color
|
||||
|
||||
# printf "${(P)1}${2} ${NC}\n" # <-- zsh
|
||||
# printf "${!1}${2} ${NC}\n" # <-- bash
|
||||
echo -e "${!1}${2}${NC}" # <-- all-purpose
|
||||
}
|
||||
|
||||
|
||||
# checks whether the utility is installed
|
||||
# takes the util name as input $_CMD
|
||||
check_util_exists() {
|
||||
local request_util=$1
|
||||
if ! command -v $request_util >/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Print help message how used it script
|
||||
help() {
|
||||
local script=$(colorize GREEN "$_SCRIPT_NAME")
|
||||
local required=$(colorize RED "required")
|
||||
printf "Usage: $script [options [parameters]]\n"
|
||||
printf "\n"
|
||||
printf "Options:\n"
|
||||
printf "\n"
|
||||
printf " -f | --file full path to upload file $required\n"
|
||||
printf " -t | --token access token $required\n"
|
||||
printf " -u | --url target url $required\n"
|
||||
printf " -v | --version print version\n"
|
||||
printf " -h | --help print help\n"
|
||||
}
|
||||
|
||||
|
||||
# parse user arguments
|
||||
argparser() {
|
||||
# count user-passed arguments:
|
||||
local count_arguments=$#
|
||||
# run help if empty and exit:
|
||||
if [[ count_arguments -eq 0 ]]; then
|
||||
help
|
||||
exit 2
|
||||
fi
|
||||
# parse args:
|
||||
while [ ! -z "$1" ]; do
|
||||
case "$1" in
|
||||
--file|-f)
|
||||
shift
|
||||
_FILE="$1"
|
||||
;;
|
||||
--token|-t)
|
||||
shift
|
||||
_TOKEN="$1"
|
||||
;;
|
||||
--url|-u)
|
||||
shift
|
||||
_URL="$1"
|
||||
;;
|
||||
--help|-h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
--version|-v)
|
||||
printf "$_VERSION\n"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
help
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# check curl is exists:
|
||||
curl_is_exists() {
|
||||
if ! check_util_exists $_CMD; then
|
||||
local error_cmd=$(colorize RED "$_CMD")
|
||||
printf "$(colorize RED "ERROR"): upload util doesn't exist, "
|
||||
printf "please install $error_cmd before run $_SCRIPT_NAME\n"
|
||||
# Print how install curl (support only apt/yum):
|
||||
for pkgmgr in $_PACKGMGR; do
|
||||
if check_util_exists $pkgmgr; then
|
||||
printf "$(colorize GREEN "RUN"): $pkgmgr install $error_cmd"
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
validate_args() {
|
||||
if [[ -z $_URL ]]; then
|
||||
printf "$(colorize RED "ERROR"): -u | --url argument is required\n"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z $_FILE ]]; then
|
||||
printf "$(colorize RED "ERROR"): -f | --file argument is required\n"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z $_TOKEN ]]; then
|
||||
printf "$(colorize RED "ERROR"): -t | --token argument is required\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Upload file used curl
|
||||
# get $_URL $_FILE $_TOKEN
|
||||
upload() {
|
||||
local url=$1
|
||||
local file=$2
|
||||
local token=$3
|
||||
# run:
|
||||
curl --progress-bar -X 'POST' \
|
||||
"${url}" \
|
||||
-H 'accept: application/json' \
|
||||
-H "Upload-Token: ${token} " \
|
||||
-H 'Content-Type: multipart/form-data' \
|
||||
-F "file=@${file}" | cat # cat required to show progress bar
|
||||
}
|
||||
|
||||
|
||||
main () {
|
||||
argparser $@
|
||||
curl_is_exists
|
||||
validate_args
|
||||
upload $_URL $_FILE $_TOKEN
|
||||
}
|
||||
|
||||
|
||||
# RUN IT:
|
||||
main $@
|
||||
Reference in New Issue
Block a user