Add: BE, FE
This commit is contained in:
parent
c2eaccbc23
commit
4e83776907
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
*.pth
|
||||||
|
*.zip
|
||||||
|
weights/
|
||||||
|
minio_data/
|
||||||
|
test_images/
|
||||||
|
*.pem
|
||||||
|
server.txt
|
||||||
|
.env
|
||||||
|
.vscode/
|
||||||
|
backup/
|
||||||
|
*.sqlite3
|
||||||
|
*.log
|
||||||
|
__pycache__
|
||||||
|
migrations/
|
23
cope2n-api/Dockerfile
Executable file
23
cope2n-api/Dockerfile
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
FROM python:3.10.10-buster AS builder
|
||||||
|
ARG UID=1000
|
||||||
|
ARG GID=1000
|
||||||
|
ARG USERNAME=container-user
|
||||||
|
RUN groupadd --gid ${GID} ${USERNAME} \
|
||||||
|
&& useradd --uid ${UID} --gid ${GID} -m ${USERNAME} \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y sudo \
|
||||||
|
&& echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME} \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/${USERNAME}
|
||||||
|
RUN yes | apt install postgresql gcc musl-dev
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN apt install bash gettext
|
||||||
|
RUN pip install uvicorn gunicorn Celery
|
||||||
|
|
||||||
|
USER ${UID}
|
||||||
|
ADD --chown=${UID}:${GID} fwd /app
|
||||||
|
COPY --chown=${UID}:${GID} requirements.txt /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pip install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
|
COPY --chown=${UID}:${GID} . /app
|
35
cope2n-api/Dockerfile-dev
Executable file
35
cope2n-api/Dockerfile-dev
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
FROM python:3.10.10-buster AS builder
|
||||||
|
|
||||||
|
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
|
||||||
|
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN yes | apt install postgresql gcc musl-dev
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN apt install bash gettext
|
||||||
|
RUN pip install uvicorn gunicorn Celery gevent
|
||||||
|
|
||||||
|
COPY requirements.txt /requirements/requirements.txt
|
||||||
|
RUN pip install -r /requirements/requirements.txt --no-cache-dir
|
||||||
|
|
||||||
|
# NodeJS
|
||||||
|
RUN apt-get update && apt-get install -y -q --no-install-recommends \
|
||||||
|
apt-transport-https \
|
||||||
|
build-essential \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
libssl-dev \
|
||||||
|
wget \
|
||||||
|
&& apt install -y xdg-utils --fix-missing \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
ENV NVM_DIR /usr/local
|
||||||
|
# RUN mkdir NVM_DIR
|
||||||
|
ENV NODE_VERSION 18.18.0
|
||||||
|
RUN curl https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \
|
||||||
|
&& . $NVM_DIR/nvm.sh \
|
||||||
|
&& nvm install $NODE_VERSION \
|
||||||
|
&& nvm alias default $NODE_VERSION \
|
||||||
|
&& nvm use default
|
||||||
|
ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
|
||||||
|
ENV PATH $NVM_DIR/v$NODE_VERSION/bin:$PATH
|
23
cope2n-api/README.md
Executable file
23
cope2n-api/README.md
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
# Project AI Backend for Frontend
|
||||||
|
# 1. Run DB and RabbitMQ (skip ì you already install)
|
||||||
|
`docker compose -f docker-persistent up --build`
|
||||||
|
# 2. Migrate Database Schema ( If needed )
|
||||||
|
1.1 Make migration file `python manage.py makemigrations`
|
||||||
|
|
||||||
|
1.2 Apply to database `python manage.py migrate`
|
||||||
|
|
||||||
|
# 3. Run Project
|
||||||
|
|
||||||
|
## 2.1 Run with Docker
|
||||||
|
### 2.1.1 Add file `.env` at same folder level with docker-compose.yml.
|
||||||
|
Sample at `env_sample/example_{OS}_env` (Window / Linux)
|
||||||
|
### 2.1.2 Build & Run Image By Command
|
||||||
|
`docker compose up --build`
|
||||||
|
|
||||||
|
## 2.2 Local Development Run
|
||||||
|
### 2.2.1 Add file `.env` at same folder level with docker-compose.yml.
|
||||||
|
Sample at `env_sample/example_local_env`
|
||||||
|
### 2.2.2 Run API
|
||||||
|
`python manage.py runserver 0.0.0.0:8000`
|
||||||
|
### 2.2.3 Run Worker
|
||||||
|
`celery -A fwd_api.proj.worker worker -l INFO --without-gossip --without-mingle --without-heartbeat -Ofair --pool=solo`
|
1
cope2n-api/TODO.md
Executable file
1
cope2n-api/TODO.md
Executable file
@ -0,0 +1 @@
|
|||||||
|
- Default env for all env variables
|
10
cope2n-api/add_user.py
Executable file
10
cope2n-api/add_user.py
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
from fwd_api.models.UserProfile import UserProfile
|
||||||
|
from fwd_api.models.UserProfile import Subscription
|
||||||
|
from fwd_api.models.UserProfile import PricingPlan
|
||||||
|
|
||||||
|
def add_user(user_name, _id):
|
||||||
|
pricing_plan = PricingPlan(id=_id, code=10, token_limitations=1000000000, duration=1000000000)
|
||||||
|
user = UserProfile(id=_id, full_name=user_name)
|
||||||
|
subscription = Subscription(id=_id, current_token=0, limit_token=1000000000, pricing_plan=pricing_plan, )
|
||||||
|
|
||||||
|
|
98
cope2n-api/dev.docker-compose.yml.dev
Executable file
98
cope2n-api/dev.docker-compose.yml.dev
Executable file
@ -0,0 +1,98 @@
|
|||||||
|
version: '2.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
ctel:
|
||||||
|
container_name: api
|
||||||
|
build: .
|
||||||
|
entrypoint: ["python3"]
|
||||||
|
command: ["-m", "gunicorn", "fwd.asgi:application", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0"]
|
||||||
|
ports:
|
||||||
|
- 8001:8000
|
||||||
|
environment:
|
||||||
|
- MEDIA_ROOT=/app/media/ # For save file
|
||||||
|
restart: always
|
||||||
|
cb_driver_ls_consumer:
|
||||||
|
container_name: driver_cb_consumer
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: ["/app/fwd_api/queue/call_back_consumer/driver_ls_queue.py"]
|
||||||
|
ports:
|
||||||
|
- 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
cb_id_card_consumer:
|
||||||
|
container_name: id_cb_consumer
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: ["/app/fwd_api/queue/call_back_consumer/id_queue.py"]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
cb_invoice_consumer:
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: [ "/app/fwd_api/queue/call_back_consumer/invoice_queue.py" ]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
cb_ocr_boxing_consumer:
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: [ "/app/fwd_api/queue/call_back_consumer/ocr_boxing_queue.py" ]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
# For mock only
|
||||||
|
|
||||||
|
driver_ls_consumer:
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: [ "/app/fwd_api/queue/mock_process_consumer/driver_ls.py" ]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
id_card_consumer:
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: [ "/app/fwd_api/queue/mock_process_consumer/id.py" ]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
invoice_consumer:
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: [ "/app/fwd_api/queue/mock_process_consumer/invoice.py" ]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
||||||
|
ocr_boxing_consumer:
|
||||||
|
build: .
|
||||||
|
entrypoint: [ "python3" ]
|
||||||
|
command: [ "/app/fwd_api/queue/mock_process_consumer/ocr_boxing.py" ]
|
||||||
|
# ports:
|
||||||
|
# - 5672:5672
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
restart: "no"
|
65
cope2n-api/docker-compose.yml
Executable file
65
cope2n-api/docker-compose.yml
Executable file
@ -0,0 +1,65 @@
|
|||||||
|
version: '3.0'
|
||||||
|
|
||||||
|
services:
|
||||||
|
ctel:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- "UID=${UID:-1000}"
|
||||||
|
- "GID=${GID:-1000}"
|
||||||
|
command: sh -c "python manage.py collectstatic --no-input &&
|
||||||
|
python manage.py makemigrations &&
|
||||||
|
python manage.py compilemessages &&
|
||||||
|
gunicorn fwd.asgi:application -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000" # pre-makemigrations on prod
|
||||||
|
ports:
|
||||||
|
- "8003:8000"
|
||||||
|
environment:
|
||||||
|
- MEDIA_ROOT=${MEDIA_ROOT}
|
||||||
|
- DB_ENGINE=${DB_ENGINE}
|
||||||
|
- DB_SCHEMA=${DB_SCHEMA}
|
||||||
|
- DB_USER=${DB_USER}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- DB_HOST=${DB_HOST}
|
||||||
|
- DB_PORT=${DB_PUBLIC_PORT}
|
||||||
|
- DEBUG=${DEBUG}
|
||||||
|
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
|
||||||
|
- BASE_URL=${BASE_URL}
|
||||||
|
- CTEL_KEY=${CTEL_KEY}
|
||||||
|
- ALLOWED_HOSTS=${ALLOWED_HOSTS}
|
||||||
|
- BROKER_URL=${BROKER_URL}
|
||||||
|
- BASE_UI_URL=${BASE_UI_URL}
|
||||||
|
- AUTH_TOKEN_LIFE_TIME=${AUTH_TOKEN_LIFE_TIME}
|
||||||
|
- IMAGE_TOKEN_LIFE_TIME=${IMAGE_TOKEN_LIFE_TIME}
|
||||||
|
- INTERNAL_SDS_KEY=${INTERNAL_SDS_KEY}
|
||||||
|
- FI_USER_NAME=${FI_USER_NAME}
|
||||||
|
- FI_PASSWORD=${FI_PASSWORD}
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ${HOST_MEDIA_FOLDER}
|
||||||
|
target: ${MEDIA_ROOT}
|
||||||
|
celery:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- "UID=${UID:-1000}"
|
||||||
|
- "GID=${GID:-1000}"
|
||||||
|
command: sh -c "celery -A fwd_api.celery_worker.worker worker -l INFO"
|
||||||
|
environment:
|
||||||
|
- MEDIA_ROOT=${MEDIA_ROOT}
|
||||||
|
- PYTHONPATH=${PYTHONPATH}:/app # For import module
|
||||||
|
- PYTHONUNBUFFERED=1 # For show print log
|
||||||
|
- DB_SCHEMA=${DB_SCHEMA}
|
||||||
|
- DB_USER=${DB_USER}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- DB_HOST=${DB_HOST}
|
||||||
|
- DB_PORT=${DB_INTERNAL_PORT}
|
||||||
|
- BROKER_URL=${BROKER_URL}
|
||||||
|
- DB_ENGINE=${DB_ENGINE}
|
||||||
|
- DEBUG=${DEBUG}
|
||||||
|
network_mode: host
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ${HOST_MEDIA_FOLDER}
|
||||||
|
target: ${MEDIA_ROOT}
|
42
cope2n-api/docker-persistent.yml
Executable file
42
cope2n-api/docker-persistent.yml
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
version: '2.4'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ctel:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
mem_reservation: 500m
|
||||||
|
mem_limit: 1g
|
||||||
|
container_name: db
|
||||||
|
image: postgres:14.7-alpine
|
||||||
|
ports:
|
||||||
|
- "${DB_PUBLIC_PORT}:${DB_INTERNAL_PORT}"
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- POSTGRES_DB=${DB_SCHEMA}
|
||||||
|
networks:
|
||||||
|
- ctel
|
||||||
|
rabbitmq:
|
||||||
|
mem_reservation: 600m
|
||||||
|
mem_limit: 4g
|
||||||
|
container_name: rabbitmq
|
||||||
|
restart: always
|
||||||
|
image: rabbitmq:3.10-alpine
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
- "15692:15692"
|
||||||
|
volumes:
|
||||||
|
- rabbitmq_data:/var/lib/rabbitmq
|
||||||
|
environment:
|
||||||
|
- RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
|
||||||
|
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
|
||||||
|
networks:
|
||||||
|
- ctel
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
rabbitmq_data:
|
19
cope2n-api/env_sample/example_linux_env
Executable file
19
cope2n-api/env_sample/example_linux_env
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
MEDIA_ROOT=/app/media
|
||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
DB_SCHEMA=d
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=1
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PUBLIC_PORT=5555
|
||||||
|
DB_INTERNAL_PORT=5432
|
||||||
|
DEBUG=FALSE
|
||||||
|
CORS_ALLOWED_ORIGINS=*
|
||||||
|
BASE_URL=http://107.120.70.69:8001
|
||||||
|
CTEL_KEY=99999999999999999999999999999999
|
||||||
|
ALLOWED_HOSTS='*'
|
||||||
|
BROKER_URL=amqp://test:test@172.17.0.1:5672//
|
||||||
|
BASE_UI_URL=http://107.120.70.136:8080/
|
||||||
|
HOST_MEDIA_FOLDER=/home/my/folder/example
|
||||||
|
GID=example
|
||||||
|
UID=example
|
||||||
|
SECRET_KEY=999999999999999999999999999999999999999999999999999999999999999999
|
16
cope2n-api/env_sample/example_local_env
Executable file
16
cope2n-api/env_sample/example_local_env
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
SCHEMA_NAME=v2
|
||||||
|
DB_USER=sample
|
||||||
|
DB_PASSWORD=sample
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=5432
|
||||||
|
DEBUG=TRUE
|
||||||
|
CORS_ALLOWED_ORIGINS=*
|
||||||
|
MEDIA_ROOT=C:\Users\MyUser\MyDataFolder\
|
||||||
|
TEMPLATE_BASE_URL=http://my-template.xyz
|
||||||
|
BASE_URL=http://base-url.xyz
|
||||||
|
CTEL_KEY=99999999999999999999999999999999
|
||||||
|
ALLOWED_HOSTS=*,100.100.100.100
|
||||||
|
BROKER_URL=amqp://test:test@107.120.70.226:5672//
|
||||||
|
SECRET_KEY=999999999999999999999999999999999999999999999999999999999999999999
|
||||||
|
DB_INTERNAL_KEY=99999999999999999999
|
21
cope2n-api/env_sample/example_window_env
Executable file
21
cope2n-api/env_sample/example_window_env
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
MEDIA_ROOT=/app/media
|
||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
DB_SCHEMA=d
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=1
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PUBLIC_PORT=5555
|
||||||
|
DB_INTERNAL_PORT=5432
|
||||||
|
DEBUG=FALSE
|
||||||
|
CORS_ALLOWED_ORIGINS=*
|
||||||
|
BASE_URL=http://107.120.70.69:8001
|
||||||
|
CTEL_KEY=99999999999999999999999999999999
|
||||||
|
ALLOWED_HOSTS='*'
|
||||||
|
BROKER_URL=amqp://test:test@host.docker.internal:5672//
|
||||||
|
BASE_UI_URL=http://107.120.70.136:8080/
|
||||||
|
HOST_MEDIA_FOLDER=C:\Users\MyUserName\Documents\Data
|
||||||
|
GID=example
|
||||||
|
UID=example
|
||||||
|
SECRET_KEY=999999999999999999999999999999999999999999999999999999999999999999
|
||||||
|
DB_INTERNAL_KEY=7LYk-iaWTFPqsZHIE5GHuv41S0c_Vlb0ZVc-BnsEZqQ=
|
||||||
|
DB_INTERNAL_KEY=99999999999999999999
|
0
cope2n-api/fwd/__init__.py
Executable file
0
cope2n-api/fwd/__init__.py
Executable file
16
cope2n-api/fwd/asgi.py
Executable file
16
cope2n-api/fwd/asgi.py
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for fwd project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fwd.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
207
cope2n-api/fwd/settings.py
Executable file
207
cope2n-api/fwd/settings.py
Executable file
@ -0,0 +1,207 @@
|
|||||||
|
"""
|
||||||
|
Django settings for fwd project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.1.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import environ
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
env = environ.Env(
|
||||||
|
DEBUG=(bool, True)
|
||||||
|
)
|
||||||
|
DEBUG = env("DEBUG")
|
||||||
|
if DEBUG:
|
||||||
|
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
||||||
|
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=['*'] + ['107.120.{}.{}'.format(i, j) for i in range(256) for j in range(256)])
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = env.str("SECRET_KEY", '999999999999999999999999999999999999999999999999999999999999999999')
|
||||||
|
CTEL_KEY = env.str("CTEL_KEY", 'fTjWnZr4u7x!A%D*G-KaPdRgUkXp2s5v')
|
||||||
|
INTERNAL_SDS_KEY = env.str('SDS_SECRET_KEY', '2a5c21b593e0ec84c5ad68e175f75a2b2f2c47c387d9adfc9c8d42e16ec848f8e75de10dbcb3abdaf375420e3023fd7c05446a8a9521100038a750d312ab0005')
|
||||||
|
DB_ENCRYPT_KEY = env.str('DB_INTERNAL_KEY', '7LYk-iaWTFPqsZHIE5GHuv41S0c_Vlb0ZVc-BnsEZqQ=')
|
||||||
|
BASE_URL = env.str("BASE_URL", "")
|
||||||
|
BASE_UI_URL = env.str("BASE_UI_URL", "http://107.120.70.136:8080/")
|
||||||
|
AUTH_TOKEN_LIFE_TIME = env.int("AUTH_TOKEN_LIFE_TIME", 0)
|
||||||
|
IMAGE_TOKEN_LIFE_TIME = env.int("IMAGE_TOKEN_LIFE_TIME", 0)
|
||||||
|
FI_USER_NAME = env.str("FI_USER_NAME", "Manulife")
|
||||||
|
FI_PASSWORD = env.str("FI_PASSWORD", 'admin')# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
# Application definition
|
||||||
|
S3_ENDPOINT = env.str("S3_ENDPOINT", "")
|
||||||
|
S3_ACCESS_KEY = env.str("S3_ACCESS_KEY", "TannedCung")
|
||||||
|
S3_SECRET_KEY = env.str("S3_SECRET_KEY", "TannedCung")
|
||||||
|
S3_BUCKET_NAME = env.str("S3_BUCKET_NAME", "ocr-data")
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
'fwd_api.apps.FwdApiConfig',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'rest_framework',
|
||||||
|
'drf_spectacular',
|
||||||
|
'drf_spectacular_sidecar', # required for Django collectstatic discovery
|
||||||
|
'corsheaders',
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCALE_PATHS = [
|
||||||
|
os.path.join(BASE_DIR, 'locale')
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "fwd.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [BASE_DIR / 'templates']
|
||||||
|
,
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "fwd.wsgi.application"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': env.str("DB_ENGINE", "django.db.backends.sqlite3"),
|
||||||
|
'NAME': env.str("DB_SCHEMA", BASE_DIR / "db.sqlite3"),
|
||||||
|
'USER': env.str("DB_USER", None),
|
||||||
|
'PASSWORD': env.str("DB_PASSWORD", None),
|
||||||
|
'HOST': env.str("DB_HOST", None),
|
||||||
|
'PORT': env.str("DB_PORT", None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "Asia/Ho_Chi_Minh"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
STORAGES = {
|
||||||
|
# ...
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
# YOUR SETTINGS
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
|
# 'DEFAULT_SCHEMA_CLASS': 'fwd_api.schema.AutoSchema',
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
|
"fwd_api.filter.AuthFilter.AuthFilter",
|
||||||
|
),
|
||||||
|
'EXCEPTION_HANDLER': 'fwd_api.exception.exceptions_handler.custom_exception_handler',
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
'rest_framework_xml.renderers.XMLRenderer',
|
||||||
|
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
'TITLE': 'SDS C2open',
|
||||||
|
'DESCRIPTION': 'AI powered by SamsungSDS VietNam',
|
||||||
|
'VERSION': '1.0.0',
|
||||||
|
'SERVE_INCLUDE_SCHEMA': True,
|
||||||
|
# OTHER SETTINGS
|
||||||
|
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
|
||||||
|
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||||
|
'REDOC_DIST': 'SIDECAR',
|
||||||
|
# OTHER SETTINGS
|
||||||
|
"PREPROCESSING_HOOKS": ["fwd_api.api_specs.hooks.remove_apis_from_list"],
|
||||||
|
# Custom Spectacular Settings
|
||||||
|
"EXCLUDE_PATH": [reverse_lazy("schema")],
|
||||||
|
"EXCLUDE_RELATIVE_PATH": ["/rsa", '/gen-token', '/app/'],
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE_UPLOAD_HANDLERS = [
|
||||||
|
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
|
||||||
|
]
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
MEDIA_ROOT = env.str("MEDIA_ROOT", default=r"/var/www/example.com/media/")
|
||||||
|
BROKER_URL = env.str("BROKER_URL", default="amqp://test:test@107.120.70.226:5672//")
|
||||||
|
CELERY_TIMEZONE = "Australia/Tasmania"
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
CELERY_TASK_TIME_LIMIT = 30 * 60
|
||||||
|
|
||||||
|
MAX_UPLOAD_SIZE_OF_A_FILE = 100 * 1024 * 1024 # 100 MB
|
||||||
|
MAX_UPLOAD_FILE_SIZE_OF_A_REQUEST = 100 * 1024 * 1024 # 100 MB
|
||||||
|
MAX_UPLOAD_FILES_IN_A_REQUEST = 5
|
||||||
|
SIZE_TO_COMPRESS = 2 * 1024 * 1024
|
||||||
|
MAX_NUMBER_OF_TEMPLATE = 3
|
||||||
|
MAX_PAGES_OF_PDF_FILE = 50
|
31
cope2n-api/fwd/urls.py
Executable file
31
cope2n-api/fwd/urls.py
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
"""C2OPEN URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/4.1/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("api/", include("fwd_api.api_router")),
|
||||||
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
|
# Optional UI:
|
||||||
|
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
|
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
16
cope2n-api/fwd/wsgi.py
Executable file
16
cope2n-api/fwd/wsgi.py
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for fwd project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fwd.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
cope2n-api/fwd_api/__init__.py
Executable file
0
cope2n-api/fwd_api/__init__.py
Executable file
3
cope2n-api/fwd_api/admin.py
Executable file
3
cope2n-api/fwd_api/admin.py
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
0
cope2n-api/fwd_api/annotation/__init__.py
Executable file
0
cope2n-api/fwd_api/annotation/__init__.py
Executable file
38
cope2n-api/fwd_api/annotation/api.py
Executable file
38
cope2n-api/fwd_api/annotation/api.py
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
import traceback
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def throw_on_failure(e):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except Exception as root_e:
|
||||||
|
traceback.print_exc()
|
||||||
|
from fwd_api.exception.exceptions import GeneralException
|
||||||
|
if issubclass(type(root_e), GeneralException):
|
||||||
|
raise root_e
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def limit_file_size(e):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except Exception as root_e:
|
||||||
|
traceback.print_exc()
|
||||||
|
from fwd_api.exception.exceptions import GeneralException
|
||||||
|
if issubclass(type(root_e), GeneralException):
|
||||||
|
raise root_e
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
return decorator
|
0
cope2n-api/fwd_api/api/__init__.py
Executable file
0
cope2n-api/fwd_api/api/__init__.py
Executable file
182
cope2n-api/fwd_api/api/ctel_template_view.py
Executable file
182
cope2n-api/fwd_api/api/ctel_template_view.py
Executable file
@ -0,0 +1,182 @@
|
|||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from ..annotation.api import throw_on_failure
|
||||||
|
from ..exception.exceptions import ServiceUnavailableException, \
|
||||||
|
LimitReachedException
|
||||||
|
from ..models import OcrTemplateBox
|
||||||
|
from ..models.OcrTemplate import OcrTemplate
|
||||||
|
from ..request.CreateTemplateRequest import CreateTemplateRequest
|
||||||
|
from ..constant.common import EntityStatus, TEMPLATE_ID
|
||||||
|
from ..exception.exceptions import InvalidException
|
||||||
|
from ..request.UpdateTemplateRequest import UpdateTemplateRequest
|
||||||
|
from ..response.TemplateResponse import TemplateResponse
|
||||||
|
from ..utils import FileUtils, ProcessUtil
|
||||||
|
from ..utils.ProcessUtil import UserData
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
|
||||||
|
|
||||||
|
class CtelTemplateViewSet(viewsets.ViewSet):
|
||||||
|
lookup_field = "username"
|
||||||
|
base_url = settings.BASE_URL
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['templates'], methods=['GET'])
|
||||||
|
@extend_schema(request={
|
||||||
|
'multipart/form-data': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'files': {
|
||||||
|
'type': 'string',
|
||||||
|
'format': 'binary',
|
||||||
|
},
|
||||||
|
'template_id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '<b>Required if updateTemplate</b>'
|
||||||
|
},
|
||||||
|
'template_name': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'data_box_names': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'data_boxs': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'anchor_boxs': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': {'template_name', 'data_box_names', 'data_boxs', 'anchor_boxs', 'file'}
|
||||||
|
}
|
||||||
|
}, responses=None, tags=['templates'], methods=['POST'])
|
||||||
|
@action(detail=False, methods=["POST", "GET"])
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
def templates(self, request):
|
||||||
|
user_data = ProcessUtil.get_user(request)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
return self.upsert_template(request, user_data)
|
||||||
|
else:
|
||||||
|
return self.get_template(request, user_data)
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['templates'])
|
||||||
|
@action(detail=False, methods=["DELETE"], url_path=r"templates/(?P<template_id>\d+)")
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
@transaction.atomic
|
||||||
|
def delete_template(self, request, template_id=None):
|
||||||
|
user_data: UserData = ProcessUtil.get_user(request)
|
||||||
|
|
||||||
|
if not template_id:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
template = OcrTemplate.objects.filter(id=template_id, subscription=user_data.current_sub)
|
||||||
|
if len(template) != 1:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
template = template[0]
|
||||||
|
old_path = template.file_path
|
||||||
|
deleted, _rows_count = template.delete()
|
||||||
|
if not deleted:
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
# Delete file
|
||||||
|
FileUtils.delete_file_with_path(old_path)
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'template_id': template_id
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_template(self, request, u_data: UserData):
|
||||||
|
tem_id = request.query_params.get('template_id', None)
|
||||||
|
|
||||||
|
if tem_id:
|
||||||
|
tem_id: int = int(tem_id)
|
||||||
|
template = OcrTemplate.objects.filter(id=tem_id, subscription=u_data.current_sub)
|
||||||
|
if len(template) != 1:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
rs = TemplateResponse(data=template)
|
||||||
|
rs.is_valid()
|
||||||
|
return Response(status=status.HTTP_200_OK, data=rs.validated_data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
template = OcrTemplate.objects.filter(subscription=u_data.current_sub)
|
||||||
|
rs = TemplateResponse(template, many=True)
|
||||||
|
return Response(status=status.HTTP_200_OK, data=rs.data)
|
||||||
|
|
||||||
|
def upsert_template(self, request, user_data: UserData):
|
||||||
|
if 'template_id' in request.data:
|
||||||
|
return self.update_template(request, user_data)
|
||||||
|
else:
|
||||||
|
return self.insert_template(request, user_data)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def insert_template(self, request, user_data: UserData):
|
||||||
|
file_list = request.data.getlist('file')
|
||||||
|
FileUtils.validate_list_file(file_list)
|
||||||
|
sub = user_data.current_sub
|
||||||
|
|
||||||
|
# Check request
|
||||||
|
serializer = CreateTemplateRequest(data=request.data)
|
||||||
|
serializer.is_valid()
|
||||||
|
if not serializer.is_valid():
|
||||||
|
print(serializer.errors)
|
||||||
|
raise InvalidException(excArgs=list(serializer.errors.keys()))
|
||||||
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
# Check total
|
||||||
|
templates = OcrTemplate.objects.filter(subscription=sub)
|
||||||
|
if len(templates) >= settings.MAX_NUMBER_OF_TEMPLATE:
|
||||||
|
raise LimitReachedException(excArgs=('Number of template', str(settings.MAX_NUMBER_OF_TEMPLATE), ''))
|
||||||
|
|
||||||
|
|
||||||
|
# Create
|
||||||
|
template: OcrTemplate = OcrTemplate(status=EntityStatus.ACTIVE.value, name=data['template_name'], subscription=sub)
|
||||||
|
template.save()
|
||||||
|
|
||||||
|
file_obj = file_list[0]
|
||||||
|
file_name = str(round(time.time() * 1000)) + '_tem_' + file_obj.name
|
||||||
|
file_path = FileUtils.save_template_file(file_name, template, file_obj, 100)
|
||||||
|
template.file_path = file_path
|
||||||
|
template.file_name = file_name
|
||||||
|
template.save()
|
||||||
|
ProcessUtil.save_template_boxs(data, template)
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
"id": template.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update_template(self, request, user_data: UserData):
|
||||||
|
# Validate
|
||||||
|
data = request.data
|
||||||
|
file_list = data.getlist('file')
|
||||||
|
file_obj = file_list[0]
|
||||||
|
FileUtils.validate_list_file(file_list)
|
||||||
|
serializer = UpdateTemplateRequest(data=data)
|
||||||
|
serializer.is_valid()
|
||||||
|
if not serializer.is_valid():
|
||||||
|
print(serializer.errors)
|
||||||
|
raise InvalidException(excArgs=list(serializer.errors.keys()))
|
||||||
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
template = OcrTemplate.objects.filter(id=data['template_id'], subscription=user_data.current_sub)
|
||||||
|
if len(template) != 1:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
template = template[0]
|
||||||
|
old_path = template.file_path
|
||||||
|
template.name = data['template_name']
|
||||||
|
|
||||||
|
file_path = FileUtils.save_template_file(template.file_name, template, file_obj, 100)
|
||||||
|
template.file_path = file_path
|
||||||
|
template.save()
|
||||||
|
# Delete Old
|
||||||
|
OcrTemplateBox.objects.filter(template=template).delete()
|
||||||
|
# FileUtils.delete_file_with_path(old_path)
|
||||||
|
|
||||||
|
ProcessUtil.save_template_boxs(data, template)
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'template_id': template.id,
|
||||||
|
})
|
312
cope2n-api/fwd_api/api/ctel_user_view.py
Executable file
312
cope2n-api/fwd_api/api/ctel_user_view.py
Executable file
@ -0,0 +1,312 @@
|
|||||||
|
import base64
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import authentication_classes, permission_classes
|
||||||
|
|
||||||
|
from ..annotation.api import throw_on_failure
|
||||||
|
from ..constant.common import USER_MESSAGE, EntityStatus, PLAN_MESSAGE, PlanCode
|
||||||
|
from ..exception.exceptions import InvalidException, NotFoundException, LockedEntityException, TrialOneException, \
|
||||||
|
LimitReachedException, NotAuthenticatedException
|
||||||
|
from ..models import UserProfile, PricingPlan, Subscription
|
||||||
|
from ..request.UpsertUserRequest import UpsertUserRequest
|
||||||
|
from ..response.SubscriptionResponse import SubscriptionResponse
|
||||||
|
from ..utils import ProcessUtil, DateUtil
|
||||||
|
from ..utils.CryptoUtils import sds_authenticator, admin_sds_authenticator, SdsAuthentication
|
||||||
|
import datetime
|
||||||
|
from ..request.LoginRequest import LoginRequest
|
||||||
|
from ..request.HealcheckSerializer import HealthCheckSerializer
|
||||||
|
|
||||||
|
from ..utils.DateUtil import default_zone
|
||||||
|
from fwd import settings
|
||||||
|
|
||||||
|
class CtelUserViewSet(viewsets.ViewSet):
|
||||||
|
lookup_field = "username"
|
||||||
|
|
||||||
|
|
||||||
|
# @extend_schema(request=LoginRequest, responses=None, tags=['users'], examples=[
|
||||||
|
# OpenApiExample(
|
||||||
|
# 'ex1',
|
||||||
|
# summary='Sample Login',
|
||||||
|
# description='Sample Login',
|
||||||
|
# value={
|
||||||
|
# 'username': 'admin',
|
||||||
|
# 'password': 'admin'
|
||||||
|
# }
|
||||||
|
# ),
|
||||||
|
# ])
|
||||||
|
|
||||||
|
@extend_schema(request={
|
||||||
|
'multipart/form-data': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'username': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
'required': {'username', 'password'}
|
||||||
|
}
|
||||||
|
}, responses=None, tags=['ocr'])
|
||||||
|
@action(detail=False, url_path="login", methods=["POST"])
|
||||||
|
def login(self, request):
|
||||||
|
serializer = LoginRequest(data=request.data)
|
||||||
|
print(serializer.is_valid(raise_exception=True))
|
||||||
|
|
||||||
|
data = serializer.validated_data
|
||||||
|
if data['username'] != settings.FI_USER_NAME or data['password'] != settings.FI_PASSWORD:
|
||||||
|
raise NotAuthenticatedException()
|
||||||
|
|
||||||
|
users = UserProfile.objects.filter(sync_id=settings.FI_USER_NAME)
|
||||||
|
|
||||||
|
if len(users) > 1:
|
||||||
|
raise InvalidException(excArgs=USER_MESSAGE)
|
||||||
|
if len(users) == 0:
|
||||||
|
user = UserProfile(sync_id=settings.FI_USER_NAME, status=EntityStatus.ACTIVE.value)
|
||||||
|
user.save()
|
||||||
|
else:
|
||||||
|
user = users[0]
|
||||||
|
subs = Subscription.objects.filter(user=user)
|
||||||
|
if len(subs) > 1:
|
||||||
|
raise TrialOneException(excArgs=PLAN_MESSAGE)
|
||||||
|
if len(subs) == 0:
|
||||||
|
p_code = "FI_PLAN"
|
||||||
|
plans = PricingPlan.objects.filter(code=p_code)
|
||||||
|
if len(plans) > 1:
|
||||||
|
raise TrialOneException(excArgs=PLAN_MESSAGE)
|
||||||
|
if len(plans) == 0:
|
||||||
|
plan = PricingPlan(code=p_code, duration=365, token_limitations=999999)
|
||||||
|
plan.save()
|
||||||
|
else:
|
||||||
|
plan: PricingPlan = plans[0]
|
||||||
|
|
||||||
|
start_plan_at = DateUtil.get_date_time_now()
|
||||||
|
c_time = start_plan_at + datetime.timedelta(days=plan.duration)
|
||||||
|
c_time.replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
|
||||||
|
sub: Subscription = Subscription(limit_token=plan.token_limitations, pricing_plan=plan, expired_at=c_time,
|
||||||
|
user=user, start_at=start_plan_at, status=EntityStatus.ACTIVE.value)
|
||||||
|
sub.save()
|
||||||
|
else:
|
||||||
|
sub = subs[0]
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'user_id': 'SBT',
|
||||||
|
'user_name': settings.FI_USER_NAME,
|
||||||
|
'token': sds_authenticator.generate_token(user_id=settings.FI_USER_NAME, internal_id=user.id, status=EntityStatus.ACTIVE.value, sub_id=sub.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['users'])
|
||||||
|
@action(detail=False, url_path="users/info", methods=["GET"])
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
def get_be_user(self, request):
|
||||||
|
user_data = ProcessUtil.get_user(request)
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'userId': user_data.user.sync_id,
|
||||||
|
'message': 'User is valid'
|
||||||
|
})
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['users'], methods=['GET'], parameters=[
|
||||||
|
OpenApiParameter("user", OpenApiTypes.STR, OpenApiParameter.HEADER, description="CMC/SDS encrypted token"),
|
||||||
|
OpenApiParameter("subscription_id", OpenApiTypes.STR, OpenApiParameter.QUERY, description="Subscription id"),
|
||||||
|
|
||||||
|
])
|
||||||
|
@extend_schema(request=UpsertUserRequest, responses=None, tags=['users'], methods=['POST'], parameters=[
|
||||||
|
OpenApiParameter("user", OpenApiTypes.STR, OpenApiParameter.HEADER, description="CMC/SDS encrypted token"),
|
||||||
|
], examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
'Example',
|
||||||
|
summary='Request Sample',
|
||||||
|
description='Status : 0 ( active ) / 1 (inactive). Default is 0. <br>'
|
||||||
|
'Datetime Format: <b>dd/mm/YYYY HH:MM:SS</b> <br>'
|
||||||
|
'Plan code ( TRIAL / BASIC / ADVANCED ) ( TRIAL apply only one-time per user account )',
|
||||||
|
value={
|
||||||
|
'plan_code': 'A03',
|
||||||
|
'plan_start_at': '01/04/2023 00:00:00',
|
||||||
|
'status': 1,
|
||||||
|
'email': 'abc@mamil.vn',
|
||||||
|
'name': 'Pham Van A'
|
||||||
|
},
|
||||||
|
request_only=True,
|
||||||
|
response_only=False
|
||||||
|
),
|
||||||
|
])
|
||||||
|
@action(detail=False, url_path="users", methods=["POST", "GET"])
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
def user_view(self, request):
|
||||||
|
if request.method == "POST":
|
||||||
|
return self.upsert_user(request)
|
||||||
|
else:
|
||||||
|
return self.get_user(request)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def upsert_user(self, request):
|
||||||
|
if not hasattr(request, 'user_data'):
|
||||||
|
raise NotFoundException(excArgs=USER_MESSAGE)
|
||||||
|
user_data = request.user_data
|
||||||
|
user_updated: bool = False
|
||||||
|
sub_id = -1
|
||||||
|
# Check request
|
||||||
|
serializer = UpsertUserRequest(data=request.data)
|
||||||
|
serializer.is_valid()
|
||||||
|
if not serializer.is_valid():
|
||||||
|
print(serializer.errors)
|
||||||
|
raise InvalidException(excArgs=list(serializer.errors.keys()))
|
||||||
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
users = UserProfile.objects.filter(sync_id=user_data['userId'])
|
||||||
|
|
||||||
|
if len(users) > 1:
|
||||||
|
raise InvalidException(excArgs=USER_MESSAGE)
|
||||||
|
if len(users) == 0:
|
||||||
|
user = UserProfile(sync_id=user_data['userId'])
|
||||||
|
user_updated = True
|
||||||
|
else:
|
||||||
|
user = users[0]
|
||||||
|
|
||||||
|
if 'name' in data:
|
||||||
|
user.full_name = data['name']
|
||||||
|
user_updated = True
|
||||||
|
|
||||||
|
if 'email' in data:
|
||||||
|
user.email = data['email']
|
||||||
|
user_updated = True
|
||||||
|
|
||||||
|
if 'status' in data:
|
||||||
|
user.status = data['status']
|
||||||
|
user_updated = True
|
||||||
|
|
||||||
|
if user_updated:
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
if 'plan_code' in data and "plan_start_at" in data:
|
||||||
|
plan_code = data['plan_code']
|
||||||
|
# create sub
|
||||||
|
plans = PricingPlan.objects.filter(code=plan_code)
|
||||||
|
if len(plans) != 1:
|
||||||
|
raise InvalidException(excArgs=PLAN_MESSAGE)
|
||||||
|
plan: PricingPlan = plans[0]
|
||||||
|
|
||||||
|
if plan_code == PlanCode.TRIAL.value:
|
||||||
|
subs = Subscription.objects.filter(user=user, pricing_plan=plan)
|
||||||
|
if len(subs) > 1:
|
||||||
|
raise TrialOneException(excArgs=PLAN_MESSAGE)
|
||||||
|
|
||||||
|
start_plan_at = DateUtil.to_date(data['plan_start_at'], DateUtil.FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
c_time = start_plan_at + datetime.timedelta(days=plan.duration)
|
||||||
|
c_time.replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
|
||||||
|
sub: Subscription = Subscription(limit_token=plan.token_limitations, pricing_plan=plan, expired_at=c_time,
|
||||||
|
user=user, start_at=start_plan_at, status=EntityStatus.ACTIVE.value)
|
||||||
|
sub.save()
|
||||||
|
sub_id = sub.id
|
||||||
|
subs = Subscription.objects.filter(user=user)
|
||||||
|
sub_res = SubscriptionResponse(data=subs, many=True)
|
||||||
|
sub_res.is_valid()
|
||||||
|
|
||||||
|
response_dict = {
|
||||||
|
'userId': user_data['userId'],
|
||||||
|
'status': user.status,
|
||||||
|
'subscriptionId': sub_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data=response_dict)
|
||||||
|
|
||||||
|
def get_user(self, request):
|
||||||
|
if not hasattr(request, 'user_data'):
|
||||||
|
raise NotFoundException(excArgs='user')
|
||||||
|
user_data = request.user_data
|
||||||
|
sub_id = request.query_params.get('subscription_id', None)
|
||||||
|
users = UserProfile.objects.filter(sync_id=user_data['userId'])
|
||||||
|
if len(users) != 1:
|
||||||
|
raise InvalidException(excArgs="userId")
|
||||||
|
user: UserProfile = users.first()
|
||||||
|
if user.status != EntityStatus.ACTIVE.value:
|
||||||
|
raise LockedEntityException(excArgs="user")
|
||||||
|
|
||||||
|
if sub_id is None or sub_id == -1:
|
||||||
|
subs = Subscription.objects.filter(user=user)
|
||||||
|
sub_res = SubscriptionResponse(data=subs, many=True)
|
||||||
|
sub_res.is_valid()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'userId': user.sync_id,
|
||||||
|
'status': user.status,
|
||||||
|
'subscriptions': sub_res.data
|
||||||
|
})
|
||||||
|
subs = Subscription.objects.filter(user=user, id=sub_id)
|
||||||
|
sub_res = SubscriptionResponse(data=subs, many=True)
|
||||||
|
sub_res.is_valid()
|
||||||
|
sub = sub_res.data[0]
|
||||||
|
gen_x: SdsAuthentication = admin_sds_authenticator if sub['plan_code'] == 'SDS_EXTRA_PREMIUM_PACK' else sds_authenticator # Gen long token for admin
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'userId': user.sync_id,
|
||||||
|
'status': user.status,
|
||||||
|
'subscriptions': sub,
|
||||||
|
'url': gen_x.generate_url(user.sync_id, user.id, user.status, sub['id']),
|
||||||
|
'token': gen_x.generate_token(user.sync_id, user.id, user.status, sub['id']),
|
||||||
|
})
|
||||||
|
|
||||||
|
# @extend_schema(request={
|
||||||
|
# 'application/json': {
|
||||||
|
# 'type': 'object',
|
||||||
|
# 'properties': {
|
||||||
|
# 'userId': {
|
||||||
|
# 'type': 'string',
|
||||||
|
# 'example': "C2H5OH"
|
||||||
|
# },
|
||||||
|
# 'expiredAt': {
|
||||||
|
# 'type': 'string',
|
||||||
|
# 'example': "21/12/2023 00:00:00"
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# 'required': {'collection_id', 'source', 'files'}
|
||||||
|
# }
|
||||||
|
|
||||||
|
# }, responses=None, tags=['users'],
|
||||||
|
# description="API mô phỏng token CMC truyền sang SDS. Dùng để truyền vào header user")
|
||||||
|
@action(detail=False, url_path="users/gen-token", methods=["POST"])
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
def gen_cmc_token(self, request):
|
||||||
|
data = request.data
|
||||||
|
|
||||||
|
if "expiredAt" not in request.data:
|
||||||
|
raise InvalidException(excArgs='expiredAt')
|
||||||
|
uid = ProcessUtil.get_random_string(10) if "userId" not in data or data['userId'] is None else data['userId']
|
||||||
|
m_text = {
|
||||||
|
'userId': uid,
|
||||||
|
"expiredAt": data['expiredAt']
|
||||||
|
}
|
||||||
|
import os
|
||||||
|
from ..utils.CryptoUtils import ctel_cryptor
|
||||||
|
import json
|
||||||
|
iv = os.urandom(16).hex()
|
||||||
|
e_text = ctel_cryptor.encrypt_ctel(json.dumps(m_text), iv)
|
||||||
|
e_data = {
|
||||||
|
'content': e_text,
|
||||||
|
'iv': iv
|
||||||
|
}
|
||||||
|
return Response(status=status.HTTP_200_OK, data={
|
||||||
|
'token': base64.b64encode(json.dumps(e_data).encode())
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, url_path="healthcheck", methods=["GET"], authentication_classes=[], permission_classes=[])
|
||||||
|
def health_check(self, request):
|
||||||
|
# Perform any checks to determine the health status of the application
|
||||||
|
# TODO: check database connectivity, S3 service availability, etc.
|
||||||
|
serializer = HealthCheckSerializer(data={
|
||||||
|
'status': "OK",
|
||||||
|
'message': 'Application is running smoothly.'
|
||||||
|
})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
return JsonResponse(status=status.HTTP_200_OK, data=serializer.data)
|
397
cope2n-api/fwd_api/api/ctel_view.py
Executable file
397
cope2n-api/fwd_api/api/ctel_view.py
Executable file
@ -0,0 +1,397 @@
|
|||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from wsgiref.util import FileWrapper
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import TemporaryUploadedFile
|
||||||
|
from django.db import transaction
|
||||||
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
import io
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from ..annotation.api import throw_on_failure
|
||||||
|
from ..constant.common import allowed_p_type, ProcessType, REQUEST_ID, FOLDER_TYPE, \
|
||||||
|
FolderFileType, TEMPLATE_ID, EntityStatus, standard_ocr_list, pdf_extensions, image_extensions
|
||||||
|
from ..exception.exceptions import RequiredFieldException, InvalidException, NotFoundException, \
|
||||||
|
PermissionDeniedException, LimitReachedException, LockedEntityException
|
||||||
|
from ..models import SubscriptionRequest, UserProfile, SubscriptionRequestFile, OcrTemplate, Subscription
|
||||||
|
from ..response.ReportSerializer import ReportSerializer
|
||||||
|
from ..utils import FileUtils, ProcessUtil
|
||||||
|
|
||||||
|
class CtelViewSet(viewsets.ViewSet):
|
||||||
|
lookup_field = "username"
|
||||||
|
size_to_compress = settings.SIZE_TO_COMPRESS
|
||||||
|
|
||||||
|
@extend_schema(request={
|
||||||
|
'multipart/form-data': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'file': {
|
||||||
|
'type': 'string',
|
||||||
|
'format': 'binary'
|
||||||
|
},
|
||||||
|
'processType': {
|
||||||
|
'type': 'string'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': {'file', 'processType'}
|
||||||
|
}
|
||||||
|
}, responses=None, tags=['ocr'])
|
||||||
|
@action(detail=False, url_path="image/process", methods=["POST"])
|
||||||
|
@transaction.atomic
|
||||||
|
def process(self, request):
|
||||||
|
s_time = time.time()
|
||||||
|
# print(30*"=")
|
||||||
|
# print(f"[DEBUG]: request: {request}")
|
||||||
|
# print(30*"=")
|
||||||
|
user_info = ProcessUtil.get_user(request)
|
||||||
|
user = user_info.user
|
||||||
|
sub = user_info.current_sub
|
||||||
|
|
||||||
|
validated_data = ProcessUtil.validate_ocr_request_and_get(request, sub)
|
||||||
|
|
||||||
|
provider_code = 'SAP'
|
||||||
|
rq_id = provider_code + uuid.uuid4().hex
|
||||||
|
|
||||||
|
file_obj: TemporaryUploadedFile = validated_data['file']
|
||||||
|
file_extension = file_obj.name.split(".")[-1]
|
||||||
|
p_type = validated_data['type']
|
||||||
|
file_name = f"temp_{rq_id}.{file_extension}"
|
||||||
|
|
||||||
|
total_page = 1
|
||||||
|
|
||||||
|
new_request: SubscriptionRequest = SubscriptionRequest(pages=total_page,
|
||||||
|
process_type=p_type, status=1, request_id=rq_id,
|
||||||
|
provider_code=provider_code,
|
||||||
|
subscription=sub)
|
||||||
|
new_request.save()
|
||||||
|
from ..celery_worker.client_connector import c_connector
|
||||||
|
file_obj.seek(0)
|
||||||
|
file_path = FileUtils.resize_and_save_file(file_name, new_request, file_obj, 100)
|
||||||
|
if settings.S3_ENDPOINT!="":
|
||||||
|
FileUtils.save_to_S3(file_name, new_request, file_obj.read())
|
||||||
|
# print(f"[DEBUG]: file_path: {file_path}")
|
||||||
|
|
||||||
|
if file_extension in pdf_extensions:
|
||||||
|
c_connector.do_pdf((rq_id, sub.id, p_type, user.id, file_name, file_path))
|
||||||
|
# b_url = ProcessUtil.process_pdf_file(file_name, file_obj, new_request, user)
|
||||||
|
elif file_extension in image_extensions:
|
||||||
|
b_url = ProcessUtil.process_image_file(file_name, file_obj, new_request, user)
|
||||||
|
j_time = time.time()
|
||||||
|
print(f"[INFO]: Duration of Pre-processing: {j_time - s_time}s")
|
||||||
|
print(f"[INFO]: b_url: {b_url}")
|
||||||
|
if p_type in standard_ocr_list:
|
||||||
|
ProcessUtil.send_to_queue2(rq_id, sub.id, b_url, user.id, p_type)
|
||||||
|
if p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
ProcessUtil.send_template_queue(rq_id, b_url, validated_data['template'], user.id)
|
||||||
|
else:
|
||||||
|
return JsonResponse(status=status.HTTP_406_NOT_ACCEPTABLE, data={"request_id": rq_id, "message": f"File {file_extension} is now allowed"})
|
||||||
|
|
||||||
|
return JsonResponse(status=status.HTTP_200_OK, data={"request_id": rq_id})
|
||||||
|
|
||||||
|
@extend_schema(request={
|
||||||
|
'multipart/form-data': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'imei_files': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'format': 'binary'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'invoice_file': {
|
||||||
|
'type': 'string',
|
||||||
|
'format': 'binary'
|
||||||
|
},
|
||||||
|
'redemption_ID': {
|
||||||
|
'type': 'string'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': {'imei_files', 'invoice_file'}
|
||||||
|
}
|
||||||
|
}, responses=None, tags=['ocr'])
|
||||||
|
@action(detail=False, url_path="images/process", methods=["POST"])
|
||||||
|
@transaction.atomic
|
||||||
|
def processes(self, request):
|
||||||
|
s_time = time.time()
|
||||||
|
# print(30*"=")
|
||||||
|
# print(f"[DEBUG]: request: {request}")
|
||||||
|
# print(30*"=")
|
||||||
|
user_info = ProcessUtil.get_user(request)
|
||||||
|
user = user_info.user
|
||||||
|
sub = user_info.current_sub
|
||||||
|
|
||||||
|
validated_data = ProcessUtil.sbt_validate_ocr_request_and_get(request, sub)
|
||||||
|
|
||||||
|
provider_code = 'SAP'
|
||||||
|
rq_id = provider_code + uuid.uuid4().hex
|
||||||
|
|
||||||
|
imei_file_objs: List[TemporaryUploadedFile] = validated_data['imei_file']
|
||||||
|
invoice_file_objs: List[TemporaryUploadedFile] = validated_data['invoice_file']
|
||||||
|
|
||||||
|
files = {
|
||||||
|
"imei": imei_file_objs,
|
||||||
|
"invoice": invoice_file_objs
|
||||||
|
}
|
||||||
|
total_page = len(files.keys())
|
||||||
|
# file_paths = []
|
||||||
|
list_urls = []
|
||||||
|
p_type = validated_data['type']
|
||||||
|
new_request: SubscriptionRequest = SubscriptionRequest(pages=total_page,
|
||||||
|
process_type=p_type, status=1, request_id=rq_id,
|
||||||
|
provider_code=provider_code,
|
||||||
|
subscription=sub)
|
||||||
|
new_request.save()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for doc_type, doc_files in files.items():
|
||||||
|
for i, doc_file in enumerate(doc_files):
|
||||||
|
_ext = doc_file.name.split(".")[-1]
|
||||||
|
if _ext not in image_extensions:
|
||||||
|
return JsonResponse(status=status.HTTP_406_NOT_ACCEPTABLE, data={"request_id": rq_id, "message": f"File {_ext} is now allowed"})
|
||||||
|
_name = f"temp_{doc_type}_{rq_id}_{i}.{_ext}"
|
||||||
|
doc_file.seek(0)
|
||||||
|
# file_path = FileUtils.resize_and_save_file(_name, new_request, doc_file, 100)
|
||||||
|
# input_file = io.BytesIO(open(doc_file, 'rb').read())
|
||||||
|
input_file = doc_file.read()
|
||||||
|
if settings.S3_ENDPOINT!="":
|
||||||
|
FileUtils.save_to_S3(_name, new_request, input_file)
|
||||||
|
else:
|
||||||
|
file_path = FileUtils.resize_and_save_file(_name, new_request, doc_file, 100)
|
||||||
|
list_urls.append(ProcessUtil.process_image_file(_name, doc_file, new_request, user)[0])
|
||||||
|
list_urls[count]["page_number"] = count
|
||||||
|
list_urls[count]["doc_type"] = doc_type
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if p_type in standard_ocr_list:
|
||||||
|
ProcessUtil.send_to_queue2(rq_id, sub.id, list_urls, user.id, p_type)
|
||||||
|
elif p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
ProcessUtil.send_template_queue(rq_id, list_urls, validated_data['template'], user.id)
|
||||||
|
j_time = time.time()
|
||||||
|
print(f"[INFO]: Duration of Pre-processing: {j_time - s_time}s")
|
||||||
|
print(f"[INFO]: list_urls: {list_urls}")
|
||||||
|
return JsonResponse(status=status.HTTP_200_OK, data={"request_id": rq_id})
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['data'])
|
||||||
|
@extend_schema(request=None, responses=None, tags=['templates'], methods=['GET'])
|
||||||
|
@action(detail=False, url_path=r"media/(?P<folder_type>\w+)/(?P<uq_id>\w+)", methods=["GET"])
|
||||||
|
def get_file_v2(self, request, uq_id=None, folder_type=None):
|
||||||
|
user_data = request.user_data
|
||||||
|
content_type = "image/png"
|
||||||
|
|
||||||
|
file_name: str = request.query_params.get('file_name', None)
|
||||||
|
if folder_type is None:
|
||||||
|
raise RequiredFieldException(excArgs=FOLDER_TYPE)
|
||||||
|
if uq_id is None:
|
||||||
|
raise RequiredFieldException(excArgs=REQUEST_ID)
|
||||||
|
|
||||||
|
if folder_type == 'templates':
|
||||||
|
temps: list = OcrTemplate.objects.filter(id=uq_id)
|
||||||
|
if len(temps) != 1:
|
||||||
|
raise NotFoundException(excArgs='file')
|
||||||
|
temp: OcrTemplate = temps[0]
|
||||||
|
user = temp.subscription.user
|
||||||
|
content_type = 'application/pdf' if temp.file_name.split(".")[-1] in pdf_extensions else content_type
|
||||||
|
if user.id != user_data['internal_id'] or user.status != EntityStatus.ACTIVE.value:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
print(temp.file_path)
|
||||||
|
return HttpResponse(FileWrapper(FileUtils.get_file(temp.file_path)), status=status.HTTP_200_OK,
|
||||||
|
headers={'Content-Disposition': 'filename={fn}'.format(fn=temp.file_name)},
|
||||||
|
content_type=content_type)
|
||||||
|
elif folder_type == 'requests':
|
||||||
|
if file_name is None:
|
||||||
|
raise RequiredFieldException(excArgs='file_name')
|
||||||
|
try:
|
||||||
|
rqs = SubscriptionRequest.objects.filter(request_id=uq_id)
|
||||||
|
if len(rqs) != 1:
|
||||||
|
raise NotFoundException(excArgs='file')
|
||||||
|
rq = rqs[0]
|
||||||
|
user = rq.subscription.user
|
||||||
|
|
||||||
|
content_type = 'application/pdf' if file_name.split(".")[-1] in pdf_extensions else content_type
|
||||||
|
|
||||||
|
if user.id != user_data['internal_id'] or user.status != EntityStatus.ACTIVE.value:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
file_data = SubscriptionRequestFile.objects.filter(request=rq, file_name=file_name)[0]
|
||||||
|
except IndexError:
|
||||||
|
raise NotFoundException(excArgs='file')
|
||||||
|
return HttpResponse(FileWrapper(FileUtils.get_file(file_data.file_path)), status=status.HTTP_200_OK,
|
||||||
|
headers={'Content-Disposition': 'filename={fn}'.format(fn=file_data.file_name)},
|
||||||
|
content_type=content_type)
|
||||||
|
else:
|
||||||
|
raise InvalidException(excArgs='type')
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['data'])
|
||||||
|
@action(detail=False, url_path=r"v2/media/request/(?P<media_id>\w+)", methods=["GET"])
|
||||||
|
def get_file_v3(self, request, media_id=None):
|
||||||
|
|
||||||
|
user_info = ProcessUtil.get_user(request)
|
||||||
|
sub = user_info.current_sub
|
||||||
|
content_type = "image/png"
|
||||||
|
|
||||||
|
if media_id is None:
|
||||||
|
raise RequiredFieldException(excArgs=REQUEST_ID)
|
||||||
|
|
||||||
|
try:
|
||||||
|
media_list = SubscriptionRequestFile.objects.filter(code=media_id)
|
||||||
|
if len(media_list) != 1:
|
||||||
|
raise LockedEntityException(excArgs='media')
|
||||||
|
media_data: SubscriptionRequestFile = media_list[0]
|
||||||
|
if media_data.request.subscription.id != sub.id:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
file_name = media_data.file_name
|
||||||
|
|
||||||
|
content_type = 'application/pdf' if file_name.split(".")[-1] in pdf_extensions else content_type
|
||||||
|
|
||||||
|
except IndexError:
|
||||||
|
raise NotFoundException(excArgs='file')
|
||||||
|
return HttpResponse(FileWrapper(FileUtils.get_file(media_data.file_path)), status=status.HTTP_200_OK,
|
||||||
|
headers={'Content-Disposition': 'filename={fn}'.format(fn=file_name)},
|
||||||
|
content_type=content_type)
|
||||||
|
|
||||||
|
from rest_framework.renderers import JSONRenderer
|
||||||
|
from rest_framework_xml.renderers import XMLRenderer
|
||||||
|
|
||||||
|
@extend_schema(request=None, responses=None, tags=['data'])
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
@action(detail=False, url_path=r"result/(?P<request_id>\w+)", methods=["GET"], renderer_classes=[JSONRenderer, XMLRenderer])
|
||||||
|
def get_result(self, request, request_id=None):
|
||||||
|
|
||||||
|
user_info = ProcessUtil.get_user(request)
|
||||||
|
if request_id is None:
|
||||||
|
raise RequiredFieldException(excArgs='requestId')
|
||||||
|
|
||||||
|
report_filter = SubscriptionRequest.objects.filter(request_id=request_id)
|
||||||
|
if len(report_filter) != 1:
|
||||||
|
raise InvalidException(excArgs='requestId')
|
||||||
|
|
||||||
|
if user_info.current_sub.id != report_filter[0].subscription.id:
|
||||||
|
raise InvalidException(excArgs="user")
|
||||||
|
if int(report_filter[0].process_type) == ProcessType.FI_INVOICE.value:
|
||||||
|
data = report_filter[0].predict_result
|
||||||
|
xml_as_string = ""
|
||||||
|
|
||||||
|
if data and 'content' in data and 'combine_results' in data['content'] and 'xml' in data['content']['combine_results']:
|
||||||
|
xml_as_string = data['content']['combine_results']['xml']
|
||||||
|
xml_as_string = xml_as_string.replace("\n", "").replace("\\", "")
|
||||||
|
|
||||||
|
# return Response(status=status.HTTP_200_OK, data=xml_as_string, content_type="application/xml; charset=utf-8")
|
||||||
|
# return HttpResponse(xml_as_string,content_type="text/xml")
|
||||||
|
|
||||||
|
return HttpResponse(xml_as_string,content_type="text/xml")
|
||||||
|
|
||||||
|
serializer: ReportSerializer = ReportSerializer(data=report_filter, many=True)
|
||||||
|
serializer.is_valid()
|
||||||
|
# print(f"[DEBUG]: result: {serializer.data[0]}")
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data=serializer.data[0])
|
||||||
|
|
||||||
|
@throw_on_failure(InvalidException(excArgs='data'))
|
||||||
|
@action(detail=False, url_path=r"rsa/(?P<request_id>\w+)", methods=["GET"])
|
||||||
|
def get_result2(self, request, request_id=None):
|
||||||
|
|
||||||
|
user_info = ProcessUtil.get_user(request)
|
||||||
|
if request_id is None:
|
||||||
|
raise RequiredFieldException(excArgs='requestId')
|
||||||
|
|
||||||
|
report_filter = SubscriptionRequest.objects.filter(request_id=request_id)
|
||||||
|
if len(report_filter) != 1:
|
||||||
|
raise InvalidException(excArgs='requestId')
|
||||||
|
|
||||||
|
if user_info.current_sub.id != report_filter[0].subscription.id:
|
||||||
|
raise InvalidException(excArgs="user")
|
||||||
|
if int(report_filter[0].process_type) == ProcessType.FI_INVOICE.value:
|
||||||
|
data = report_filter[0].predict_result
|
||||||
|
xml_as_string = ""
|
||||||
|
|
||||||
|
if data and 'content' in data and 'combine_results' in data['content'] and 'xml' in data['content']['combine_results']:
|
||||||
|
xml_as_string = data['content']['combine_results']['xml']
|
||||||
|
xml_as_string = xml_as_string.replace("\n", "").replace("\\", "")
|
||||||
|
|
||||||
|
# return Response(status=status.HTTP_200_OK, data=xml_as_string, content_type="application/xml; charset=utf-8")
|
||||||
|
return HttpResponse(xml_as_string,content_type="text/xml")
|
||||||
|
|
||||||
|
|
||||||
|
serializer: ReportSerializer = ReportSerializer(data=report_filter, many=True)
|
||||||
|
serializer.is_valid()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK, data=serializer.data[0])
|
||||||
|
|
||||||
|
@action(detail=False, url_path="image/process/app", methods=["POST"])
|
||||||
|
@transaction.atomic
|
||||||
|
def process_app(self, request):
|
||||||
|
app_id = "THIS_IS_OUR_APP_TEST_ACCOUNT_9123"
|
||||||
|
users = UserProfile.objects.filter(sync_id=app_id)
|
||||||
|
if len(users) > 1:
|
||||||
|
raise InvalidException(excArgs='user')
|
||||||
|
if len(users) == 0:
|
||||||
|
user = UserProfile(sync_id=app_id, limit_total_pages=1000, status=EntityStatus.ACTIVE.value)
|
||||||
|
user.save()
|
||||||
|
else:
|
||||||
|
user = users[0]
|
||||||
|
|
||||||
|
subs = Subscription.objects.filter(user=user)
|
||||||
|
if len(subs) > 1:
|
||||||
|
raise InvalidException(excArgs='sub')
|
||||||
|
if len(subs) == 0:
|
||||||
|
sub = Subscription(user=user, limit_token=10000, current_token=0, status=EntityStatus.ACTIVE.value)
|
||||||
|
sub.save()
|
||||||
|
else:
|
||||||
|
sub = subs[0]
|
||||||
|
cur = sub.current_token
|
||||||
|
lim = sub.limit_token
|
||||||
|
|
||||||
|
list_file = request.data.getlist('file')
|
||||||
|
s_time = time.time()
|
||||||
|
|
||||||
|
if "processType" not in request.data or int(request.data['processType']) not in allowed_p_type:
|
||||||
|
raise InvalidException(excArgs='processType')
|
||||||
|
p_type: int = int(request.data['processType'])
|
||||||
|
|
||||||
|
if cur + ProcessUtil.token_value(p_type) >= lim:
|
||||||
|
raise LimitReachedException(excArgs=('Number of request', str(sub.limit_token), 'times'))
|
||||||
|
|
||||||
|
FileUtils.validate_list_file(list_file)
|
||||||
|
|
||||||
|
if ("templateId" not in request.data) and p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
|
||||||
|
provider_code = 'Ctel'
|
||||||
|
|
||||||
|
rq_id = provider_code + str(p_type) + get_random_string(5) + str(round(time.time() * 1000))
|
||||||
|
|
||||||
|
file_obj: TemporaryUploadedFile = list_file[0]
|
||||||
|
|
||||||
|
file_name = "temp_file_" + rq_id + get_random_string(2) + ".jpg"
|
||||||
|
|
||||||
|
total_page = 1
|
||||||
|
|
||||||
|
new_request: SubscriptionRequest = SubscriptionRequest(pages=total_page,
|
||||||
|
process_type=p_type, status=1, request_id=rq_id,
|
||||||
|
provider_code=provider_code, subscription=sub)
|
||||||
|
new_request.save()
|
||||||
|
|
||||||
|
if p_type == ProcessType.ID_CARD.value or p_type == ProcessType.INVOICE.value or p_type == ProcessType.OCR_WITH_BOX.value or p_type == ProcessType.DRIVER_LICENSE.value:
|
||||||
|
if file_obj.size > self.size_to_compress:
|
||||||
|
quality = 90
|
||||||
|
else:
|
||||||
|
quality = 100
|
||||||
|
file_path = FileUtils.resize_and_save_file(file_name, new_request, file_obj, quality)
|
||||||
|
new_request_file: SubscriptionRequestFile = SubscriptionRequestFile(file_path=file_path,
|
||||||
|
request=new_request,
|
||||||
|
file_name=file_name)
|
||||||
|
new_request_file.save()
|
||||||
|
|
||||||
|
b_url = FileUtils.build_url(FolderFileType.REQUESTS.value, new_request.request_id, user.id, file_name)
|
||||||
|
j_time = time.time()
|
||||||
|
print("Json {}".format(j_time - s_time))
|
||||||
|
ProcessUtil.send_to_queue2(rq_id, sub.id, b_url, user.id, p_type)
|
||||||
|
return JsonResponse(status=status.HTTP_200_OK, data={"request_id": rq_id})
|
||||||
|
|
||||||
|
return JsonResponse(status=status.HTTP_502_BAD_GATEWAY, data={"message": "unknown_error"})
|
19
cope2n-api/fwd_api/api_router.py
Executable file
19
cope2n-api/fwd_api/api_router.py
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from rest_framework.routers import DefaultRouter, SimpleRouter
|
||||||
|
|
||||||
|
from fwd_api.api.ctel_view import CtelViewSet
|
||||||
|
from fwd_api.api.ctel_user_view import CtelUserViewSet
|
||||||
|
|
||||||
|
from fwd_api.api.ctel_template_view import CtelTemplateViewSet
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
router = DefaultRouter()
|
||||||
|
else:
|
||||||
|
router = SimpleRouter()
|
||||||
|
|
||||||
|
router.register("ctel", CtelViewSet, basename="CtelAPI")
|
||||||
|
router.register("ctel", CtelUserViewSet, basename="CtelUserAPI")
|
||||||
|
router.register("ctel", CtelTemplateViewSet, basename="CtelTemplateAPI")
|
||||||
|
|
||||||
|
app_name = "api"
|
||||||
|
urlpatterns = router.urls
|
0
cope2n-api/fwd_api/api_specs/__init__.py
Executable file
0
cope2n-api/fwd_api/api_specs/__init__.py
Executable file
15
cope2n-api/fwd_api/api_specs/auth_extension.py
Executable file
15
cope2n-api/fwd_api/api_specs/auth_extension.py
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||||
|
|
||||||
|
from fwd_api.filter.AuthFilter import AuthFilter
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleJWTTokenUserScheme(OpenApiAuthenticationExtension):
|
||||||
|
name = "Authorization"
|
||||||
|
target_class = AuthFilter
|
||||||
|
|
||||||
|
def get_security_definition(self, auto_schema):
|
||||||
|
return {
|
||||||
|
'type': 'apiKey',
|
||||||
|
'in': 'header',
|
||||||
|
'name': 'Authorization',
|
||||||
|
}
|
11
cope2n-api/fwd_api/api_specs/hooks.py
Executable file
11
cope2n-api/fwd_api/api_specs/hooks.py
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
EXCLUDE_PATH = settings.SPECTACULAR_SETTINGS["EXCLUDE_PATH"]
|
||||||
|
EXCLUDE_RELATIVE_PATH = settings.SPECTACULAR_SETTINGS["EXCLUDE_RELATIVE_PATH"]
|
||||||
|
|
||||||
|
def remove_apis_from_list(endpoints):
|
||||||
|
il = []
|
||||||
|
for (path, path_regex, method, callback) in endpoints:
|
||||||
|
if path not in EXCLUDE_PATH and all(r_path not in path for r_path in EXCLUDE_RELATIVE_PATH):
|
||||||
|
il.append((path, path_regex, method, callback))
|
||||||
|
return il
|
9
cope2n-api/fwd_api/apps.py
Executable file
9
cope2n-api/fwd_api/apps.py
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FwdApiConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "fwd_api"
|
||||||
|
def ready(self):
|
||||||
|
# import above schema.py
|
||||||
|
import fwd_api.api_specs.auth_extension # noqa: E402
|
0
cope2n-api/fwd_api/celery_worker/__init__.py
Executable file
0
cope2n-api/fwd_api/celery_worker/__init__.py
Executable file
94
cope2n-api/fwd_api/celery_worker/client_connector.py
Executable file
94
cope2n-api/fwd_api/celery_worker/client_connector.py
Executable file
@ -0,0 +1,94 @@
|
|||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from fwd_api.exception.exceptions import GeneralException
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryConnector:
|
||||||
|
task_routes = {
|
||||||
|
# save result
|
||||||
|
'process_id_result': {'queue': 'id_card_rs'},
|
||||||
|
'process_driver_license_result': {'queue': "driver_license_rs"},
|
||||||
|
'process_invoice_result': {'queue': "invoice_rs"},
|
||||||
|
'process_ocr_with_box_result': {'queue': "ocr_with_box_rs"},
|
||||||
|
'process_template_matching_result': {'queue': "process_template_matching_rs"},
|
||||||
|
'process_sap_invoice': {'queue': "invoice_sap"},
|
||||||
|
'process_manulife_invoice_result': {'queue': 'invoice_manulife_rs'},
|
||||||
|
'process_sbt_invoice_result': {'queue': 'invoice_sbt_rs'},
|
||||||
|
|
||||||
|
# process task
|
||||||
|
'process_id': {'queue': 'id_card'},
|
||||||
|
'process_driver_license': {'queue': "driver_license"},
|
||||||
|
'process_invoice': {'queue': "invoice"},
|
||||||
|
'process_ocr_with_box': {'queue': "ocr_with_box"},
|
||||||
|
'process_template_matching': {'queue': 'template_matching'},
|
||||||
|
"process_alignment": {"queue": "alignment"},
|
||||||
|
'process_sap_invoice_result': {'queue': 'invoice_sap_rs'},
|
||||||
|
'process_fi_invoice_result': {'queue': 'invoice_fi_rs'},
|
||||||
|
'process_fi_invoice': {'queue': "invoice_fi"},
|
||||||
|
'process_manulife_invoice': {'queue': "invoice_manulife"},
|
||||||
|
'process_sbt_invoice': {'queue': "invoice_sbt"},
|
||||||
|
'do_pdf': {'queue': "do_pdf"},
|
||||||
|
'upload_file_to_s3': {'queue': "upload_file_to_s3"},
|
||||||
|
'upload_obj_to_s3': {'queue': "upload_obj_to_s3"},
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
app = Celery(
|
||||||
|
'postman',
|
||||||
|
broker=settings.BROKER_URL,
|
||||||
|
)
|
||||||
|
def do_pdf(self, args):
|
||||||
|
return self.send_task('do_pdf', args)
|
||||||
|
def upload_file_to_s3(self, args):
|
||||||
|
return self.send_task('upload_file_to_s3', args)
|
||||||
|
def upload_obj_to_s3(self, args):
|
||||||
|
return self.send_task('upload_obj_to_s3', args)
|
||||||
|
def process_fi(self, args):
|
||||||
|
return self.send_task('process_fi_invoice', args)
|
||||||
|
def process_fi_result(self, args):
|
||||||
|
return self.send_task('process_fi_invoice_result', args)
|
||||||
|
|
||||||
|
def process_id_result(self, args):
|
||||||
|
return self.send_task('process_id_result', args)
|
||||||
|
|
||||||
|
def process_driver_license_result(self, args):
|
||||||
|
return self.send_task('process_driver_license_result', args)
|
||||||
|
|
||||||
|
def process_invoice_result(self, args):
|
||||||
|
return self.send_task('process_invoice_result', args)
|
||||||
|
|
||||||
|
def process_ocr_with_box_result(self, args):
|
||||||
|
return self.send_task('process_ocr_with_box_result', args)
|
||||||
|
|
||||||
|
def process_id(self, args):
|
||||||
|
return self.send_task('process_id', args)
|
||||||
|
|
||||||
|
def process_driver_license(self, args):
|
||||||
|
return self.send_task('process_driver_license', args)
|
||||||
|
|
||||||
|
def process_invoice(self, args):
|
||||||
|
return self.send_task('process_invoice', args)
|
||||||
|
|
||||||
|
def process_ocr_with_box(self, args):
|
||||||
|
return self.send_task('process_ocr_with_box', args)
|
||||||
|
|
||||||
|
def process_template_matching(self, args):
|
||||||
|
return self.send_task('process_template_matching', args)
|
||||||
|
|
||||||
|
def process_invoice_sap(self, args):
|
||||||
|
return self.send_task('process_sap_invoice', args)
|
||||||
|
|
||||||
|
def process_invoice_manulife(self, args):
|
||||||
|
return self.send_task('process_manulife_invoice', args)
|
||||||
|
|
||||||
|
def process_invoice_sbt(self, args):
|
||||||
|
return self.send_task('process_sbt_invoice', args)
|
||||||
|
|
||||||
|
def send_task(self, name=None, args=None):
|
||||||
|
if name not in self.task_routes or 'queue' not in self.task_routes[name]:
|
||||||
|
raise GeneralException("System")
|
||||||
|
return self.app.send_task(name, args, queue=self.task_routes[name]['queue'])
|
||||||
|
|
||||||
|
|
||||||
|
c_connector = CeleryConnector()
|
92
cope2n-api/fwd_api/celery_worker/internal_task.py
Executable file
92
cope2n-api/fwd_api/celery_worker/internal_task.py
Executable file
@ -0,0 +1,92 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
import time
|
||||||
|
import fitz
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
from fwd_api.celery_worker.worker import app
|
||||||
|
from ..constant.common import allowed_p_type, ProcessType, REQUEST_ID, FOLDER_TYPE, \
|
||||||
|
FolderFileType, TEMPLATE_ID, EntityStatus, standard_ocr_list, pdf_extensions
|
||||||
|
from ..utils import FileUtils, ProcessUtil, S3_process
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
from fwd import settings
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
s3_client = S3_process.MinioS3Client(
|
||||||
|
endpoint=settings.S3_ENDPOINT,
|
||||||
|
access_key=settings.S3_ACCESS_KEY,
|
||||||
|
secret_key=settings.S3_SECRET_KEY,
|
||||||
|
bucket_name=settings.S3_BUCKET_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_pdf_file(file_name: str, file_path: str, request, user) -> list:
|
||||||
|
from fwd_api.models import SubscriptionRequest, SubscriptionRequestFile
|
||||||
|
from fwd_api.constant.common import ProcessType
|
||||||
|
doc: fitz.Document = fitz.open(stream=FileUtils.get_file(file_path).read(), filetype="pdf")
|
||||||
|
|
||||||
|
# Origin file
|
||||||
|
new_request_file: SubscriptionRequestFile = SubscriptionRequestFile(file_path=file_path,
|
||||||
|
request=request,
|
||||||
|
file_name=file_name,
|
||||||
|
code=f'FIL{uuid.uuid4().hex}')
|
||||||
|
new_request_file.save()
|
||||||
|
# Sub-file
|
||||||
|
return ProcessUtil.pdf_to_images_urls(doc, request, user)
|
||||||
|
|
||||||
|
|
||||||
|
def process_image_file(file_name: str, file_path, request, user) -> list:
|
||||||
|
from fwd_api.models import SubscriptionRequest, SubscriptionRequestFile
|
||||||
|
|
||||||
|
new_request_file: SubscriptionRequestFile = SubscriptionRequestFile(file_path=file_path,
|
||||||
|
request=request,
|
||||||
|
file_name=file_name,
|
||||||
|
code=f'FIL{uuid.uuid4().hex}')
|
||||||
|
new_request_file.save()
|
||||||
|
return [{
|
||||||
|
'file_url': FileUtils.build_url(FolderFileType.REQUESTS.value, request.request_id, user.id, file_name),
|
||||||
|
'page_number': 0,
|
||||||
|
'request_file_id': new_request_file.code
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(name='do_pdf')
|
||||||
|
def process_pdf(rq_id, sub_id, p_type, user_id, file_name, file_path):
|
||||||
|
from fwd_api.models import SubscriptionRequest, SubscriptionRequestFile,UserProfile
|
||||||
|
|
||||||
|
new_request = SubscriptionRequest.objects.filter(request_id=rq_id)[0]
|
||||||
|
user = UserProfile.objects.filter(id=user_id).first()
|
||||||
|
file_extension = file_name.split(".")[-1]
|
||||||
|
# logger.info(f"[DEBUG]: file_path: {file_path}")
|
||||||
|
if file_extension in pdf_extensions:
|
||||||
|
b_url = process_pdf_file(file_name, file_path, new_request, user)
|
||||||
|
else:
|
||||||
|
b_url = process_image_file(file_name, file_path, new_request, user)
|
||||||
|
|
||||||
|
j_time = time.time()
|
||||||
|
# logger.info(f"[INFO]: Duration of Pre-processing: {j_time - 0}s")
|
||||||
|
# logger.info(f"[INFO]: b_url: {b_url}")
|
||||||
|
if p_type in standard_ocr_list:
|
||||||
|
ProcessUtil.send_to_queue2(rq_id, sub_id, b_url, user_id, p_type)
|
||||||
|
if p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
ProcessUtil.send_template_queue(rq_id, b_url, '', user_id)
|
||||||
|
|
||||||
|
@app.task(name='upload_file_to_s3')
|
||||||
|
def upload_file_to_s3(local_file_path, s3_key):
|
||||||
|
if s3_client.s3_client is not None:
|
||||||
|
res = s3_client.upload_file(local_file_path, s3_key)
|
||||||
|
if res != None and res["ResponseMetadata"]["HTTPStatusCode"] == 200:
|
||||||
|
os.remove(local_file_path)
|
||||||
|
else:
|
||||||
|
print(f"[INFO] S3 is not available, skipping,...")
|
||||||
|
|
||||||
|
@app.task(name='upload_obj_to_s3')
|
||||||
|
def upload_obj_to_s3(byte_obj, s3_key):
|
||||||
|
if s3_client.s3_client is not None:
|
||||||
|
obj = base64.b64decode(byte_obj)
|
||||||
|
res = s3_client.update_object(s3_key, obj)
|
||||||
|
else:
|
||||||
|
print(f"[INFO] S3 is not available, skipping,...")
|
326
cope2n-api/fwd_api/celery_worker/process_result_tasks.py
Executable file
326
cope2n-api/fwd_api/celery_worker/process_result_tasks.py
Executable file
@ -0,0 +1,326 @@
|
|||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from fwd_api.celery_worker.worker import app
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
|
|
||||||
|
def print_id(rq_id):
|
||||||
|
print(" [x] Received {rq}".format(rq=rq_id))
|
||||||
|
|
||||||
|
|
||||||
|
def to_status(result):
|
||||||
|
print('X')
|
||||||
|
if 'status' in result and result['status'] not in [200, 201, 202]:
|
||||||
|
return 4
|
||||||
|
return 3
|
||||||
|
|
||||||
|
|
||||||
|
def update_user(rq: SubscriptionRequest):
|
||||||
|
sub = rq.subscription
|
||||||
|
predict_status = rq.status
|
||||||
|
if predict_status == 3:
|
||||||
|
from fwd_api.utils import ProcessUtil
|
||||||
|
sub.current_token += ProcessUtil.token_value(int(rq.process_type))
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
@app.task(name='process_sap_invoice_result')
|
||||||
|
def process_invoice_sap_result(rq_id, result):
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
print_id(rq_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rq: SubscriptionRequest = \
|
||||||
|
SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.INVOICE.value)[0]
|
||||||
|
status = to_status(result)
|
||||||
|
|
||||||
|
rq.predict_result = result
|
||||||
|
rq.status = status
|
||||||
|
rq.save()
|
||||||
|
|
||||||
|
update_user(rq)
|
||||||
|
except IndexError as e:
|
||||||
|
print(e)
|
||||||
|
print("NotFound request by requestId, %d", rq_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("Fail Invoice %d", rq_id)
|
||||||
|
return "FailInvoice"
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(name='process_fi_invoice_result')
|
||||||
|
def process_invoice_fi_result(rq_id, result):
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
print_id(rq_id)
|
||||||
|
print(result)
|
||||||
|
try:
|
||||||
|
rq: SubscriptionRequest = \
|
||||||
|
SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.FI_INVOICE.value)[0]
|
||||||
|
status = to_status(result)
|
||||||
|
|
||||||
|
rq.predict_result = result
|
||||||
|
rq.status = status
|
||||||
|
rq.save()
|
||||||
|
|
||||||
|
update_user(rq)
|
||||||
|
except IndexError as e:
|
||||||
|
print(e)
|
||||||
|
print("NotFound request by requestId, %d", rq_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("Fail Invoice %d", rq_id)
|
||||||
|
return "FailInvoice"
|
||||||
|
|
||||||
|
@app.task(name='process_manulife_invoice_result')
|
||||||
|
def process_invoice_manulife_result(rq_id, result):
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
print_id(f"[DEBUG]: Received manulife request with id {rq_id}")
|
||||||
|
try:
|
||||||
|
rq: SubscriptionRequest = \
|
||||||
|
SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.MANULIFE_INVOICE.value)[0]
|
||||||
|
status = to_status(result)
|
||||||
|
|
||||||
|
rq.predict_result = result
|
||||||
|
rq.status = status
|
||||||
|
rq.save()
|
||||||
|
|
||||||
|
update_user(rq)
|
||||||
|
except IndexError as e:
|
||||||
|
print(e)
|
||||||
|
print("NotFound request by requestId, %d", rq_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("Fail Invoice %d", rq_id)
|
||||||
|
return "FailInvoice"
|
||||||
|
|
||||||
|
@app.task(name='process_sbt_invoice_result')
|
||||||
|
def process_invoice_sbt_result(rq_id, result):
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
print_id(f"[DEBUG]: Received SBT request with id {rq_id}")
|
||||||
|
print_id(f"[DEBUG]: result: {result}")
|
||||||
|
try:
|
||||||
|
rq: SubscriptionRequest = \
|
||||||
|
SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.SBT_INVOICE.value)[0]
|
||||||
|
status = to_status(result)
|
||||||
|
|
||||||
|
rq.predict_result = result
|
||||||
|
rq.status = status
|
||||||
|
rq.save()
|
||||||
|
update_user(rq)
|
||||||
|
except IndexError as e:
|
||||||
|
print(e)
|
||||||
|
print("NotFound request by requestId, %d", rq_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("Fail Invoice %d", rq_id)
|
||||||
|
return "FailInvoice"
|
||||||
|
|
||||||
|
|
||||||
|
# @app.task(name='process_id_result', queue='id_card_rs')
|
||||||
|
# def process_id_result(rq_id, result):
|
||||||
|
# from fwd_api.models import SubscriptionRequest
|
||||||
|
# from fwd_api.constant.common import ProcessType
|
||||||
|
# from fwd_api.models import SubscriptionRequestFile
|
||||||
|
# from fwd_api.constant.common import FileCategory
|
||||||
|
|
||||||
|
# print_id(rq_id)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# s_time = time.time()
|
||||||
|
# print("Start")
|
||||||
|
# j_time = time.time()
|
||||||
|
# print("Json {}".format(j_time - s_time))
|
||||||
|
|
||||||
|
# rq: SubscriptionRequest = \
|
||||||
|
# SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.ID_CARD.value)[0]
|
||||||
|
|
||||||
|
# if 'content' in result and 'pages' in result['content']:
|
||||||
|
# pages = result['content']['pages']
|
||||||
|
# if isinstance(pages, list):
|
||||||
|
# new_pages = []
|
||||||
|
# for idx, page in enumerate(pages):
|
||||||
|
# if 'path_image_croped' in page:
|
||||||
|
# img_name = f'crop_{idx}_{get_random_string(3)}.jpg'
|
||||||
|
# path = page['path_image_croped']
|
||||||
|
# rq_file: SubscriptionRequestFile = SubscriptionRequestFile(file_name=img_name, request=rq,
|
||||||
|
# file_category=FileCategory.CROP.value,
|
||||||
|
# file_path=path,
|
||||||
|
# code=f'IDC{uuid.uuid4().hex}')
|
||||||
|
# rq_file.save()
|
||||||
|
# page['path_image_croped'] = rq_file.code
|
||||||
|
|
||||||
|
# l_time = time.time()
|
||||||
|
# print("Save {}".format(l_time - j_time))
|
||||||
|
|
||||||
|
# status = to_status(result)
|
||||||
|
|
||||||
|
# rq.predict_result = result
|
||||||
|
|
||||||
|
# rq.status = status
|
||||||
|
# rq.save()
|
||||||
|
|
||||||
|
# update_user(rq)
|
||||||
|
# e_time = time.time()
|
||||||
|
# print("End {}".format(e_time - l_time))
|
||||||
|
|
||||||
|
# except IndexError as e:
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print(e)
|
||||||
|
# except Exception as e:
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print(e)
|
||||||
|
# print("Fail ID %d", rq_id)
|
||||||
|
# return "Fail"
|
||||||
|
|
||||||
|
# return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
# @app.task(name='process_driver_license_result')
|
||||||
|
# def process_driver_license_result(rq_id, result):
|
||||||
|
# from fwd_api.models import SubscriptionRequest
|
||||||
|
# from fwd_api.models import SubscriptionRequestFile
|
||||||
|
# from fwd_api.constant.common import FileCategory
|
||||||
|
# from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
# print_id(rq_id)
|
||||||
|
# try:
|
||||||
|
# rq: SubscriptionRequest = \
|
||||||
|
# SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.DRIVER_LICENSE.value)[0]
|
||||||
|
|
||||||
|
# if 'content' in result and 'pages' in result['content']:
|
||||||
|
# pages = result['content']['pages']
|
||||||
|
# if isinstance(pages, list):
|
||||||
|
# new_pages = []
|
||||||
|
# for idx, page in enumerate(pages):
|
||||||
|
# if 'path_image_croped' in page:
|
||||||
|
# img_name = f'crop_{idx}_{get_random_string(3)}.jpg'
|
||||||
|
# path = page['path_image_croped']
|
||||||
|
# rq_file: SubscriptionRequestFile = SubscriptionRequestFile(file_name=img_name, request=rq,
|
||||||
|
# file_category=FileCategory.CROP.value,
|
||||||
|
# file_path=path,
|
||||||
|
# code=f'DLC{uuid.uuid4().hex}')
|
||||||
|
# rq_file.save()
|
||||||
|
# page['path_image_croped'] = rq_file.code
|
||||||
|
# status = to_status(result)
|
||||||
|
|
||||||
|
# rq.predict_result = result
|
||||||
|
# rq.status = status
|
||||||
|
# rq.save()
|
||||||
|
|
||||||
|
# update_user(rq)
|
||||||
|
|
||||||
|
# except IndexError as e:
|
||||||
|
# print(e)
|
||||||
|
# except Exception as e:
|
||||||
|
# print(e)
|
||||||
|
# print("Fail DL %d", rq_id)
|
||||||
|
# return "Fail"
|
||||||
|
|
||||||
|
# return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
# @app.task(name='process_invoice_result')
|
||||||
|
# def process_invoice_result(rq_id, result):
|
||||||
|
# from fwd_api.models import SubscriptionRequest
|
||||||
|
# from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
# print_id(rq_id)
|
||||||
|
# try:
|
||||||
|
# rq: SubscriptionRequest = \
|
||||||
|
# SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.INVOICE.value)
|
||||||
|
# print(rq)
|
||||||
|
# rq: SubscriptionRequest = \
|
||||||
|
# SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.INVOICE.value)[0]
|
||||||
|
# status = to_status(result)
|
||||||
|
|
||||||
|
# rq.predict_result = result
|
||||||
|
# rq.status = status
|
||||||
|
# rq.save()
|
||||||
|
|
||||||
|
# update_user(rq)
|
||||||
|
# except IndexError as e:
|
||||||
|
# print(e)
|
||||||
|
# print("NotFound request by requestId, %d", rq_id)
|
||||||
|
# except Exception as e:
|
||||||
|
# print(e)
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print("Fail Invoice %d", rq_id)
|
||||||
|
# return "FailInvoice"
|
||||||
|
|
||||||
|
# return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
# @app.task(name='process_ocr_with_box_result')
|
||||||
|
# def process_ocr_with_box_result(rq_id, result):
|
||||||
|
# from fwd_api.models import SubscriptionRequest
|
||||||
|
# from fwd_api.constant.common import ProcessType
|
||||||
|
|
||||||
|
# print_id(rq_id)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# rq: SubscriptionRequest = \
|
||||||
|
# SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.OCR_WITH_BOX.value)[0]
|
||||||
|
# status = to_status(result)
|
||||||
|
|
||||||
|
# rq.predict_result = result
|
||||||
|
# rq.status = status
|
||||||
|
# rq.save()
|
||||||
|
|
||||||
|
# update_user(rq)
|
||||||
|
# except IndexError as e:
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print(e)
|
||||||
|
# except Exception as e:
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print(e)
|
||||||
|
# print("Fail OCR %d", rq_id)
|
||||||
|
# return "FailOCR"
|
||||||
|
|
||||||
|
# return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
# @app.task(name='process_template_matching_result')
|
||||||
|
# def template_matching_result(rq_id, result, align_img):
|
||||||
|
# from fwd_api.models import SubscriptionRequest
|
||||||
|
# from fwd_api.constant.common import ProcessType
|
||||||
|
# from fwd_api.constant.common import FileCategory
|
||||||
|
# from fwd_api.models import SubscriptionRequestFile
|
||||||
|
|
||||||
|
# print_id(rq_id)
|
||||||
|
# try:
|
||||||
|
# rq: SubscriptionRequest = \
|
||||||
|
# SubscriptionRequest.objects.filter(request_id=rq_id, process_type=ProcessType.TEMPLATE_MATCHING.value)[0]
|
||||||
|
|
||||||
|
# if align_img:
|
||||||
|
# from fwd_api.constant.common import IMAGE_NAME
|
||||||
|
# rq_file: SubscriptionRequestFile = SubscriptionRequestFile(file_name=IMAGE_NAME, request=rq,
|
||||||
|
# file_category=FileCategory.CROP.value,
|
||||||
|
# file_path=align_img)
|
||||||
|
# rq_file.save()
|
||||||
|
# status = to_status(result)
|
||||||
|
|
||||||
|
# rq.predict_result = result
|
||||||
|
# rq.status = status
|
||||||
|
# rq.save()
|
||||||
|
|
||||||
|
# update_user(rq)
|
||||||
|
|
||||||
|
# except IndexError as e:
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print(e)
|
||||||
|
# except Exception as e:
|
||||||
|
# traceback.format_exc()
|
||||||
|
# print(e)
|
||||||
|
# print("Fail Template %d", rq_id)
|
||||||
|
# return "FailTemplate"
|
||||||
|
|
||||||
|
# return "Success"
|
61
cope2n-api/fwd_api/celery_worker/worker.py
Executable file
61
cope2n-api/fwd_api/celery_worker/worker.py
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import django
|
||||||
|
from celery import Celery
|
||||||
|
from kombu import Queue
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fwd.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
app: Celery = Celery(
|
||||||
|
'postman',
|
||||||
|
broker=settings.BROKER_URL,
|
||||||
|
include=['fwd_api.celery_worker.process_result_tasks', 'fwd_api.celery_worker.internal_task'],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.conf.update({
|
||||||
|
'task_queues':
|
||||||
|
[
|
||||||
|
# Queue('id_card_rs'),
|
||||||
|
# Queue('driver_license_rs'),
|
||||||
|
# Queue('invoice_rs'),
|
||||||
|
# Queue('ocr_with_box_rs'),
|
||||||
|
# Queue('template_matching_rs'),
|
||||||
|
Queue('invoice_sap_rs'),
|
||||||
|
Queue('invoice_fi_rs'),
|
||||||
|
Queue('invoice_manulife_rs'),
|
||||||
|
Queue('invoice_sbt_rs'),
|
||||||
|
Queue('do_pdf'),
|
||||||
|
Queue('upload_file_to_s3'),
|
||||||
|
Queue('upload_obj_to_s3'),
|
||||||
|
|
||||||
|
],
|
||||||
|
'task_routes': {
|
||||||
|
# 'process_id_result': {'queue': 'id_card_rs'},
|
||||||
|
# 'process_driver_license_result': {'queue': "driver_license_rs"},
|
||||||
|
# 'process_invoice_result': {'queue': "invoice_rs"},
|
||||||
|
# 'process_ocr_with_box_result': {'queue': "ocr_with_box_rs"},
|
||||||
|
# 'process_template_matching_result': {'queue': 'template_matching_rs'},
|
||||||
|
'process_sap_invoice_result': {'queue': 'invoice_sap_rs'},
|
||||||
|
'process_sap_invoice': {'queue': "invoice_sap"},
|
||||||
|
'process_fi_invoice_result': {'queue': 'invoice_fi_rs'},
|
||||||
|
'process_fi_invoice': {'queue': "invoice_fi"},
|
||||||
|
'process_manulife_invoice_result': {'queue': 'invoice_manulife_rs'},
|
||||||
|
'process_manulife_invoice': {'queue': "invoice_manulife"},
|
||||||
|
'process_sbt_invoice_result': {'queue': 'invoice_sbt_rs'},
|
||||||
|
'process_sbt_invoice': {'queue': "invoice_sbt"},
|
||||||
|
'do_pdf': {'queue': "do_pdf"},
|
||||||
|
'upload_file_to_s3': {'queue': "upload_file_to_s3"},
|
||||||
|
'upload_obj_to_s3': {'queue': "upload_obj_to_s3"},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
argv = [
|
||||||
|
'worker',
|
||||||
|
'--loglevel=INFO',
|
||||||
|
'--pool=solo' # Window opts
|
||||||
|
]
|
||||||
|
app.worker_main(argv)
|
0
cope2n-api/fwd_api/constant/__init__.py
Executable file
0
cope2n-api/fwd_api/constant/__init__.py
Executable file
123
cope2n-api/fwd_api/constant/common.py
Executable file
123
cope2n-api/fwd_api/constant/common.py
Executable file
@ -0,0 +1,123 @@
|
|||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
|
||||||
|
image_extensions = ('jpg', 'jpeg', 'png', 'JPG', 'JPEG', 'PNG')
|
||||||
|
pdf_extensions = ('pdf', 'PDF')
|
||||||
|
allowed_file_extensions = image_extensions + pdf_extensions
|
||||||
|
allowed_p_type = [2, 3, 4, 5, 6]
|
||||||
|
LIST_BOX_MESSAGE = 'list_box'
|
||||||
|
NAME_MESSAGE = 'name'
|
||||||
|
VN_AND_SPACE_REGEX = r"[AĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴA-Z0-9 ]+"
|
||||||
|
IMAGE_NAME = "image_croped.jpg"
|
||||||
|
TEMPLATE_ID = 'template_id'
|
||||||
|
pattern = re.compile(VN_AND_SPACE_REGEX)
|
||||||
|
REQUEST_ID = 'requestId'
|
||||||
|
FOLDER_TYPE = 'folderType'
|
||||||
|
MAX_NUMBER_OF_TEMPLATE_DATA_BOX = 20
|
||||||
|
MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX = 3
|
||||||
|
NUMBER_OF_ITEM_IN_A_BOX = 4 # 4 coordinates
|
||||||
|
ESCAPE_VALUE = 'W5@X8#'
|
||||||
|
USER_MESSAGE = 'user'
|
||||||
|
PLAN_MESSAGE = 'plan'
|
||||||
|
|
||||||
|
|
||||||
|
class FolderFileType(Enum):
|
||||||
|
TEMPLATES = 'templates'
|
||||||
|
REQUESTS = 'requests'
|
||||||
|
|
||||||
|
|
||||||
|
class FileCategory(Enum):
|
||||||
|
CROP = 'Crop'
|
||||||
|
Origin = 'Origin'
|
||||||
|
BREAK = 'Break'
|
||||||
|
|
||||||
|
|
||||||
|
class EntityStatus(Enum):
|
||||||
|
ACTIVE = 1
|
||||||
|
INACTIVE = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TEMPLATE_BOX_TYPE(Enum):
|
||||||
|
ANCHOR = 1
|
||||||
|
DATA = 2
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessType(Enum):
|
||||||
|
TEMPLATE_MATCHING = 2
|
||||||
|
ID_CARD = 3
|
||||||
|
DRIVER_LICENSE = 4
|
||||||
|
INVOICE = 5
|
||||||
|
OCR_WITH_BOX = 6
|
||||||
|
AP_INVOICE = 7
|
||||||
|
FI_INVOICE = 10
|
||||||
|
|
||||||
|
class PlanCode(Enum):
|
||||||
|
TRIAL = 'TRIAL'
|
||||||
|
BASIC = 'BASIC'
|
||||||
|
ADVANCED = 'ADVANCED'
|
||||||
|
|
||||||
|
|
||||||
|
standard_ocr_list = (ProcessType.INVOICE.value, ProcessType.ID_CARD.value, ProcessType.DRIVER_LICENSE.value, ProcessType.OCR_WITH_BOX.value)
|
||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
|
||||||
|
image_extensions = ('jpg', 'jpeg', 'png', 'JPG', 'JPEG', 'PNG')
|
||||||
|
pdf_extensions = ('pdf', 'PDF')
|
||||||
|
# allowed_file_extensions = image_extensions + pdf_extensions
|
||||||
|
allowed_file_extensions = image_extensions
|
||||||
|
allowed_p_type = [12]
|
||||||
|
LIST_BOX_MESSAGE = 'list_box'
|
||||||
|
NAME_MESSAGE = 'name'
|
||||||
|
VN_AND_SPACE_REGEX = r"[AĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴAĂÂÁẮẤÀẰẦẢẲẨÃẴẪẠẶẬĐEÊÉẾÈỀẺỂẼỄẸỆIÍÌỈĨỊOÔƠÓỐỚÒỒỜỎỔỞÕỖỠỌỘỢUƯÚỨÙỪỦỬŨỮỤỰYÝỲỶỸỴA-Z0-9 ]+"
|
||||||
|
IMAGE_NAME = "image_croped.jpg"
|
||||||
|
TEMPLATE_ID = 'template_id'
|
||||||
|
pattern = re.compile(VN_AND_SPACE_REGEX)
|
||||||
|
REQUEST_ID = 'requestId'
|
||||||
|
FOLDER_TYPE = 'folderType'
|
||||||
|
MAX_NUMBER_OF_TEMPLATE_DATA_BOX = 20
|
||||||
|
MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX = 3
|
||||||
|
NUMBER_OF_ITEM_IN_A_BOX = 4 # 4 coordinates
|
||||||
|
ESCAPE_VALUE = 'W5@X8#'
|
||||||
|
USER_MESSAGE = 'user'
|
||||||
|
PLAN_MESSAGE = 'plan'
|
||||||
|
|
||||||
|
|
||||||
|
class FolderFileType(Enum):
|
||||||
|
TEMPLATES = 'templates'
|
||||||
|
REQUESTS = 'requests'
|
||||||
|
|
||||||
|
|
||||||
|
class FileCategory(Enum):
|
||||||
|
CROP = 'Crop'
|
||||||
|
Origin = 'Origin'
|
||||||
|
BREAK = 'Break'
|
||||||
|
|
||||||
|
|
||||||
|
class EntityStatus(Enum):
|
||||||
|
ACTIVE = 1
|
||||||
|
INACTIVE = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TEMPLATE_BOX_TYPE(Enum):
|
||||||
|
ANCHOR = 1
|
||||||
|
DATA = 2
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessType(Enum):
|
||||||
|
TEMPLATE_MATCHING = 2
|
||||||
|
ID_CARD = 3
|
||||||
|
DRIVER_LICENSE = 4
|
||||||
|
INVOICE = 5
|
||||||
|
OCR_WITH_BOX = 6
|
||||||
|
AP_INVOICE = 7
|
||||||
|
FI_INVOICE = 10
|
||||||
|
MANULIFE_INVOICE = 11
|
||||||
|
SBT_INVOICE = 12
|
||||||
|
|
||||||
|
class PlanCode(Enum):
|
||||||
|
TRIAL = 'TRIAL'
|
||||||
|
BASIC = 'BASIC'
|
||||||
|
ADVANCED = 'ADVANCED'
|
||||||
|
|
||||||
|
standard_ocr_list = (ProcessType.INVOICE.value, ProcessType.ID_CARD.value,
|
||||||
|
ProcessType.DRIVER_LICENSE.value, ProcessType.OCR_WITH_BOX.value, ProcessType.FI_INVOICE.value, ProcessType.MANULIFE_INVOICE.value, ProcessType.SBT_INVOICE.value)
|
0
cope2n-api/fwd_api/exception/__init__.py
Executable file
0
cope2n-api/fwd_api/exception/__init__.py
Executable file
112
cope2n-api/fwd_api/exception/exceptions.py
Executable file
112
cope2n-api/fwd_api/exception/exceptions.py
Executable file
@ -0,0 +1,112 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.exceptions import APIException, ValidationError
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralException(APIException):
|
||||||
|
detail_with_arg = "error {}"
|
||||||
|
default_code = 999
|
||||||
|
|
||||||
|
def __init__(self, detail=None, code=None, excArgs=None):
|
||||||
|
|
||||||
|
code = code if code is not None else self.default_code
|
||||||
|
if excArgs is None:
|
||||||
|
super().__init__(detail, code)
|
||||||
|
else:
|
||||||
|
super().__init__(self.detail_with_arg, code)
|
||||||
|
|
||||||
|
self.excArgs = excArgs
|
||||||
|
|
||||||
|
class NotAuthenticatedException(GeneralException):
|
||||||
|
status_code = status.HTTP_401_UNAUTHORIZED
|
||||||
|
default_code = 4010
|
||||||
|
default_detail = 'Authentication failed. Username or password is incorrect'
|
||||||
|
detail_with_arg = 'Authentication failed. Username or password is incorrect'
|
||||||
|
|
||||||
|
class TrialOneException(GeneralException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 4032
|
||||||
|
default_detail = 'You have signed up for a trial plan before. Please contact sales for more information'
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionDeniedException(GeneralException):
|
||||||
|
status_code = status.HTTP_403_FORBIDDEN
|
||||||
|
default_code = 4031
|
||||||
|
default_detail = 'Action refuse. Dont have permission to perform this action'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailableException(GeneralException):
|
||||||
|
status_code = 503
|
||||||
|
default_code = 5030
|
||||||
|
default_detail = 'Service temporarily unavailable, try again later.'
|
||||||
|
|
||||||
|
|
||||||
|
class LockedEntityException(GeneralException):
|
||||||
|
status_code = status.HTTP_423_LOCKED
|
||||||
|
default_code = 4231
|
||||||
|
default_detail = 'Locked Entity'
|
||||||
|
detail_with_arg = '{} has been locked or inactive'
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidException(GeneralException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 4001
|
||||||
|
default_detail = 'Data Invalid'
|
||||||
|
detail_with_arg = '{} invalid'
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredFieldException(GeneralException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 4002
|
||||||
|
default_detail = 'Field required'
|
||||||
|
detail_with_arg = '{} param is required'
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateEntityException(GeneralException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 4041
|
||||||
|
default_detail = 'Data duplicate'
|
||||||
|
detail_with_arg = '{} duplicate'
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundException(GeneralException):
|
||||||
|
status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
default_code = 4042
|
||||||
|
default_detail = 'Data not found'
|
||||||
|
detail_with_arg = '{} not found'
|
||||||
|
|
||||||
|
|
||||||
|
class NumberOfBoxLimitReachedException(InvalidException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 4004
|
||||||
|
default_detail = 'Number of box limit reached : 20 for data boxs and 3 for anchor boxs'
|
||||||
|
detail_with_arg = 'Number of box limit reached : 20 for data boxs and 3 for anchor boxs'
|
||||||
|
|
||||||
|
|
||||||
|
class BadGatewayException(GeneralException):
|
||||||
|
status_code = status.HTTP_502_BAD_GATEWAY
|
||||||
|
default_code = 5020
|
||||||
|
default_detail = 'Bad gateway. Please contact administrator to support'
|
||||||
|
detail_with_arg = 'Bad gateway. Please contact administrator to support'
|
||||||
|
|
||||||
|
|
||||||
|
class FileFormatInvalidException(InvalidException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 4006
|
||||||
|
default_detail = 'File invalid type'
|
||||||
|
detail_with_arg = 'File must have type {}'
|
||||||
|
|
||||||
|
|
||||||
|
class TokenExpiredException(GeneralException):
|
||||||
|
status_code = status.HTTP_401_UNAUTHORIZED
|
||||||
|
default_code = 4106
|
||||||
|
default_detail = 'Token expired or invalid'
|
||||||
|
detail_with_arg = 'Token expired or invalid'
|
||||||
|
|
||||||
|
|
||||||
|
class LimitReachedException(InvalidException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 40001
|
||||||
|
default_detail = 'Data reach limit'
|
||||||
|
detail_with_arg = 'Limit reached. {} limit at {} {}'
|
53
cope2n-api/fwd_api/exception/exceptions_handler.py
Executable file
53
cope2n-api/fwd_api/exception/exceptions_handler.py
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
from functools import reduce
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.exceptions import ErrorDetail
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import exception_handler
|
||||||
|
|
||||||
|
from fwd_api.exception.exceptions import GeneralException
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
|
||||||
|
def custom_exception_handler(exc, context):
|
||||||
|
# Call REST framework's default exception handler first,
|
||||||
|
# to get the standard error response.
|
||||||
|
response = exception_handler(exc, context)
|
||||||
|
|
||||||
|
# Now add the HTTP status code to the response.
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if hasattr(exc, 'excArgs'):
|
||||||
|
excArgs = exc.excArgs
|
||||||
|
if 'detail' not in response.data:
|
||||||
|
raise GeneralException()
|
||||||
|
detail_error: ErrorDetail = response.data['detail']
|
||||||
|
|
||||||
|
if not hasattr(detail_error, 'code'):
|
||||||
|
setattr(detail_error, 'code', '9999')
|
||||||
|
if isinstance(excArgs, str):
|
||||||
|
response.data = {'code': detail_error.code, 'detail': _(detail_error).format(_(excArgs)),
|
||||||
|
'invalid_fields': [excArgs]}
|
||||||
|
return response
|
||||||
|
if isinstance(excArgs, tuple):
|
||||||
|
translated_args = tuple(map(lambda arg: _(arg), excArgs))
|
||||||
|
response.data = {
|
||||||
|
'code': detail_error.code,
|
||||||
|
'detail': _(detail_error).format(*translated_args)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
if isinstance(excArgs, list):
|
||||||
|
excArgs = list(excArgs)
|
||||||
|
response.data = {'code': detail_error.code,
|
||||||
|
'detail': _(detail_error).format(reduce((lambda x, y: _(x) + " , " + _(y)), excArgs)),
|
||||||
|
'invalid_fields': excArgs}
|
||||||
|
return response
|
||||||
|
response.data = {'code': detail_error.code, **response.data}
|
||||||
|
return response
|
||||||
|
if exc and not response:
|
||||||
|
res = Response()
|
||||||
|
res.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
res.data = {'code': '9999', 'detail': "Internal Error"}
|
||||||
|
return res
|
||||||
|
return response
|
87
cope2n-api/fwd_api/filter/AuthFilter.py
Executable file
87
cope2n-api/fwd_api/filter/AuthFilter.py
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
from rest_framework import authentication
|
||||||
|
from rest_framework.exceptions import NotAuthenticated
|
||||||
|
|
||||||
|
from fwd_api.annotation.api import throw_on_failure
|
||||||
|
from fwd_api.utils import DateUtil
|
||||||
|
from fwd_api.utils.CryptoUtils import ctel_cryptor, sds_authenticator, image_authenticator
|
||||||
|
from fwd_api.exception.exceptions import InvalidException, TokenExpiredException, PermissionDeniedException
|
||||||
|
|
||||||
|
|
||||||
|
@throw_on_failure(TokenExpiredException())
|
||||||
|
def authenticate_by_param_token(request):
|
||||||
|
token = request.query_params.get('token')
|
||||||
|
data = image_authenticator.decode_data(token)
|
||||||
|
if 'expired_at' not in data or 'internal_id' not in data:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
|
||||||
|
token_end_date = DateUtil.to_date(data['expired_at'], DateUtil.FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
if token_end_date < DateUtil.get_date_time_now():
|
||||||
|
raise TokenExpiredException()
|
||||||
|
request.user_data = data
|
||||||
|
|
||||||
|
|
||||||
|
# @throw_on_failure(TokenExpiredException())
|
||||||
|
def authenticate_by_authorization_header(request):
|
||||||
|
token = request.headers['Authorization']
|
||||||
|
print(f"[INFO]: recived request at {datetime.datetime.utcnow()}, with path {request.path}")
|
||||||
|
# print(f"[DEBUG]: token: {token}")
|
||||||
|
data = sds_authenticator.decode_data(token)
|
||||||
|
if 'expired_at' not in data or 'status' not in data or 'subscription_id' not in data:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
if data['status'] != 1:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
|
||||||
|
token_end_date = DateUtil.to_date(data['expired_at'], DateUtil.FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
if token_end_date < DateUtil.get_date_time_now():
|
||||||
|
raise TokenExpiredException()
|
||||||
|
request.user_data = data
|
||||||
|
|
||||||
|
|
||||||
|
@throw_on_failure(TokenExpiredException())
|
||||||
|
def authenticate_by_ctel_token(request):
|
||||||
|
raw_data = base64.b64decode(request.headers['user'])
|
||||||
|
json_data = json.loads(raw_data)
|
||||||
|
if 'content' not in json_data or 'iv' not in json_data:
|
||||||
|
raise InvalidException(excArgs='data')
|
||||||
|
en_text = json_data['content']
|
||||||
|
iv = json_data['iv']
|
||||||
|
user_data = json.loads(ctel_cryptor.decrypt_ctel(en_text, iv))
|
||||||
|
if 'userId' not in user_data or 'expiredAt' not in user_data:
|
||||||
|
raise InvalidException(excArgs='data')
|
||||||
|
date_str = user_data['expiredAt']
|
||||||
|
|
||||||
|
expired_at = DateUtil.to_date(date_str, DateUtil.FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
if expired_at < DateUtil.get_date_time_now():
|
||||||
|
raise TokenExpiredException()
|
||||||
|
|
||||||
|
request.user_data = user_data
|
||||||
|
|
||||||
|
|
||||||
|
class AuthFilter(authentication.BaseAuthentication):
|
||||||
|
white_list_path = [
|
||||||
|
'/api/schema/swagger-ui', '/api/schema/swagger-ui/', '/api/schema/',
|
||||||
|
'/api/ctel/users/gen-token/', # Todo : Debug API
|
||||||
|
'/api/ctel/image/process/app/', '/api/ctel/login/', '/api/ctel/login/'
|
||||||
|
]
|
||||||
|
|
||||||
|
@throw_on_failure(TokenExpiredException())
|
||||||
|
def authenticate(self, request):
|
||||||
|
|
||||||
|
if request.path in self.white_list_path:
|
||||||
|
print("App API")
|
||||||
|
return None, None
|
||||||
|
if '/api/ctel/media/' in request.path or '/api/ctel/v2/media/' in request.path:
|
||||||
|
return authenticate_by_param_token(request)
|
||||||
|
|
||||||
|
if 'Authorization' in request.headers:
|
||||||
|
return authenticate_by_authorization_header(request)
|
||||||
|
# Todo Check Ip only CMC
|
||||||
|
# Todo request add expried date
|
||||||
|
|
||||||
|
if 'user' in request.headers and request.path == '/api/ctel/users/':
|
||||||
|
return authenticate_by_ctel_token(request)
|
||||||
|
raise NotAuthenticated()
|
0
cope2n-api/fwd_api/filter/__init__.py
Executable file
0
cope2n-api/fwd_api/filter/__init__.py
Executable file
16
cope2n-api/fwd_api/models/OcrTemplate.py
Executable file
16
cope2n-api/fwd_api/models/OcrTemplate.py
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from fwd_api.models import UserProfile
|
||||||
|
from fwd_api.models.fields.EncryptedCharField import EncryptedCharField
|
||||||
|
from fwd_api.models.Subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
|
class OcrTemplate(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
name: str = models.CharField(max_length=300)
|
||||||
|
status = models.IntegerField()
|
||||||
|
file_path = EncryptedCharField(max_length=500, null=True)
|
||||||
|
file_name = EncryptedCharField(max_length=500, null=True)
|
||||||
|
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
13
cope2n-api/fwd_api/models/OcrTemplateBox.py
Executable file
13
cope2n-api/fwd_api/models/OcrTemplateBox.py
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from fwd_api.models.OcrTemplate import OcrTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class OcrTemplateBox(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
name: str = models.CharField(max_length=300, null=True)
|
||||||
|
template = models.ForeignKey(OcrTemplate, on_delete=models.CASCADE)
|
||||||
|
type: str = models.CharField(max_length=100) # ANCHOR / DATA
|
||||||
|
coordinates: str = models.CharField(max_length=200)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
11
cope2n-api/fwd_api/models/PricingPlan.py
Executable file
11
cope2n-api/fwd_api/models/PricingPlan.py
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class PricingPlan(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
code: str = models.CharField(max_length=300)
|
||||||
|
token_limitations = models.IntegerField(default=0)
|
||||||
|
duration = models.IntegerField(default=0) # Day Count
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
19
cope2n-api/fwd_api/models/Subscription.py
Executable file
19
cope2n-api/fwd_api/models/Subscription.py
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from fwd_api.constant.common import EntityStatus
|
||||||
|
from fwd_api.models import UserProfile
|
||||||
|
from fwd_api.models.PricingPlan import PricingPlan
|
||||||
|
|
||||||
|
|
||||||
|
class Subscription(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
current_token: int = models.IntegerField(default=0)
|
||||||
|
limit_token: int = models.IntegerField(default=0)
|
||||||
|
pricing_plan = models.ForeignKey(PricingPlan, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
|
||||||
|
status = models.IntegerField(default=EntityStatus.INACTIVE.value)
|
||||||
|
start_at = models.DateTimeField(default=timezone.now)
|
||||||
|
expired_at = models.DateTimeField(default=timezone.now)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
19
cope2n-api/fwd_api/models/SubscriptionRequest.py
Executable file
19
cope2n-api/fwd_api/models/SubscriptionRequest.py
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from fwd_api.models import UserProfile
|
||||||
|
from fwd_api.models.Subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionRequest(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
pages: int = models.IntegerField()
|
||||||
|
doc_type: str = models.CharField(max_length=100)
|
||||||
|
request_id = models.CharField(max_length=200) # Change to request_id
|
||||||
|
process_type = models.CharField(max_length=200) # driver/id/invoice
|
||||||
|
provider_code = models.CharField(max_length=200, default="Guest") # Request source FWD/CTel
|
||||||
|
predict_result = models.JSONField(null=True)
|
||||||
|
status = models.IntegerField() # 1: Processing(Pending) 2: PredictCompleted 3: ReturnCompleted
|
||||||
|
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
17
cope2n-api/fwd_api/models/SubscriptionRequestFile.py
Executable file
17
cope2n-api/fwd_api/models/SubscriptionRequestFile.py
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from fwd_api.constant.common import FileCategory
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
from fwd_api.models.fields.EncryptedCharField import EncryptedCharField
|
||||||
|
from django.utils import timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionRequestFile(models.Model):
|
||||||
|
code = models.CharField(max_length=300, default=f'FIL{uuid.uuid4().hex}')
|
||||||
|
file_name = models.CharField(max_length=300, default=None)
|
||||||
|
file_path = EncryptedCharField(max_length=500, default=None)
|
||||||
|
file_category = models.CharField(max_length=200, default=FileCategory.Origin.value)
|
||||||
|
request = models.ForeignKey(SubscriptionRequest, related_name="files", on_delete=models.CASCADE)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
18
cope2n-api/fwd_api/models/UserProfile.py
Executable file
18
cope2n-api/fwd_api/models/UserProfile.py
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from fwd_api.constant.common import EntityStatus
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
full_name: str = models.CharField(max_length=200)
|
||||||
|
sync_id: str = models.CharField(max_length=100)
|
||||||
|
provider_id: str = models.CharField(max_length=100, default='Ctel') # CTel/GCP/Azure :v
|
||||||
|
current_total_pages: int = models.IntegerField(default=0)
|
||||||
|
limit_total_pages: int = models.IntegerField(default=0)
|
||||||
|
status = models.IntegerField(default=EntityStatus.INACTIVE.value) # 0 INACTIVE 1 ACTIVE
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
email = models.CharField(max_length=200, null=True)
|
7
cope2n-api/fwd_api/models/__init__.py
Executable file
7
cope2n-api/fwd_api/models/__init__.py
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
from .UserProfile import UserProfile
|
||||||
|
from .SubscriptionRequest import SubscriptionRequest
|
||||||
|
from .SubscriptionRequestFile import SubscriptionRequestFile
|
||||||
|
from .OcrTemplate import OcrTemplate
|
||||||
|
from .OcrTemplateBox import OcrTemplateBox
|
||||||
|
from .PricingPlan import PricingPlan
|
||||||
|
from .Subscription import Subscription
|
33
cope2n-api/fwd_api/models/fields/EncryptedCharField.py
Executable file
33
cope2n-api/fwd_api/models/fields/EncryptedCharField.py
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from fwd_api.utils.CryptoUtils import sds_db_encryptor
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedCharField(models.CharField):
|
||||||
|
description = "Encrypted sin value"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
# encrypt data with your own function
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
return sds_db_encryptor.encrypt(value)
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
# decrypt data with your own function
|
||||||
|
return sds_db_encryptor.decrypt(value)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
# check if is instance of this type return value
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
# decrypt data with your own function
|
||||||
|
return sds_db_encryptor.decode_data(value)
|
35
cope2n-api/fwd_api/request/CreateTemplateRequest.py
Executable file
35
cope2n-api/fwd_api/request/CreateTemplateRequest.py
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.constant.common import MAX_NUMBER_OF_TEMPLATE_DATA_BOX, MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX, \
|
||||||
|
NUMBER_OF_ITEM_IN_A_BOX, ESCAPE_VALUE
|
||||||
|
from fwd_api.exception.exceptions import InvalidException
|
||||||
|
from fwd_api.utils import ProcessUtil
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTemplateRequest(serializers.Serializer):
|
||||||
|
template_name = serializers.CharField(required=True, max_length=255, allow_blank=False)
|
||||||
|
data_box_names = serializers.CharField(required=True, max_length=9000, allow_blank=False)
|
||||||
|
data_boxs = serializers.CharField(required=True, max_length=1000, allow_blank=False)
|
||||||
|
anchor_boxs = serializers.CharField(required=True, max_length=255, allow_blank=False)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
b_names = attrs['data_box_names'].split(ESCAPE_VALUE)
|
||||||
|
lenn: int = len(b_names)
|
||||||
|
ProcessUtil.validate_duplicate(b_names)
|
||||||
|
for n in b_names:
|
||||||
|
if len(n) > 255:
|
||||||
|
raise InvalidException(excArgs="box_label")
|
||||||
|
data_boxs = ProcessUtil.to_box_list(attrs['data_boxs'])
|
||||||
|
anchor_boxs = ProcessUtil.to_box_list(attrs['anchor_boxs'])
|
||||||
|
ProcessUtil.validate_box(data_boxs, MAX_NUMBER_OF_TEMPLATE_DATA_BOX, NUMBER_OF_ITEM_IN_A_BOX, lenn)
|
||||||
|
ProcessUtil.validate_box(anchor_boxs, MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX, NUMBER_OF_ITEM_IN_A_BOX, MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX)
|
||||||
|
|
||||||
|
nb_data = []
|
||||||
|
for d, name in zip(data_boxs, b_names):
|
||||||
|
nb_data.append({
|
||||||
|
'coordinates': ",".join(d),
|
||||||
|
'name': name
|
||||||
|
})
|
||||||
|
attrs['data_boxs'] = nb_data
|
||||||
|
attrs['anchor_boxs'] = anchor_boxs
|
||||||
|
return attrs
|
19
cope2n-api/fwd_api/request/FwdSerializer.py
Executable file
19
cope2n-api/fwd_api/request/FwdSerializer.py
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
|
||||||
|
|
||||||
|
class ReportSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
files = serializers.FileField(required=False)
|
||||||
|
requestId = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""
|
||||||
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
|
"""
|
||||||
|
return SubscriptionRequest.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
return instance
|
5
cope2n-api/fwd_api/request/HealcheckSerializer.py
Normal file
5
cope2n-api/fwd_api/request/HealcheckSerializer.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class HealthCheckSerializer(serializers.Serializer):
|
||||||
|
status = serializers.CharField(max_length=100)
|
||||||
|
message = serializers.CharField(max_length=200, allow_blank=True)
|
9
cope2n-api/fwd_api/request/LoginRequest.py
Executable file
9
cope2n-api/fwd_api/request/LoginRequest.py
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.models import SubscriptionRequest
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(serializers.Serializer):
|
||||||
|
|
||||||
|
username = serializers.CharField(required=True, max_length=250)
|
||||||
|
password = serializers.CharField(required=True, max_length=250)
|
37
cope2n-api/fwd_api/request/UpdateTemplateRequest.py
Executable file
37
cope2n-api/fwd_api/request/UpdateTemplateRequest.py
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.constant.common import MAX_NUMBER_OF_TEMPLATE_DATA_BOX, NUMBER_OF_ITEM_IN_A_BOX, \
|
||||||
|
MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX, ESCAPE_VALUE
|
||||||
|
from fwd_api.exception.exceptions import InvalidException
|
||||||
|
from fwd_api.utils import ProcessUtil
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateTemplateRequest(serializers.Serializer):
|
||||||
|
template_id = serializers.IntegerField(required=True, max_value=999999)
|
||||||
|
template_name = serializers.CharField(required=True, max_length=255, allow_blank=False)
|
||||||
|
data_box_names = serializers.CharField(required=True, max_length=9000, allow_blank=False)
|
||||||
|
data_boxs = serializers.CharField(required=True, max_length=600)
|
||||||
|
anchor_boxs = serializers.CharField(required=True, max_length=255)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
b_names = attrs['data_box_names'].split(ESCAPE_VALUE)
|
||||||
|
lenn: int = len(b_names)
|
||||||
|
ProcessUtil.validate_duplicate(b_names)
|
||||||
|
for n in b_names:
|
||||||
|
if len(n) > 255:
|
||||||
|
raise InvalidException(excArgs="box_label")
|
||||||
|
data_boxs = ProcessUtil.to_box_list(attrs['data_boxs'])
|
||||||
|
anchor_boxs = ProcessUtil.to_box_list(attrs['anchor_boxs'])
|
||||||
|
ProcessUtil.validate_box(data_boxs, MAX_NUMBER_OF_TEMPLATE_DATA_BOX, NUMBER_OF_ITEM_IN_A_BOX, lenn)
|
||||||
|
ProcessUtil.validate_box(anchor_boxs, MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX, NUMBER_OF_ITEM_IN_A_BOX, MAX_NUMBER_OF_TEMPLATE_ANCHOR_BOX)
|
||||||
|
|
||||||
|
nb_data = []
|
||||||
|
for d, name in zip(data_boxs, b_names):
|
||||||
|
nb_data.append({
|
||||||
|
'coordinates': ",".join(d),
|
||||||
|
'name': name
|
||||||
|
})
|
||||||
|
attrs['data_boxs'] = nb_data
|
||||||
|
attrs['anchor_boxs'] = anchor_boxs
|
||||||
|
return attrs
|
||||||
|
|
13
cope2n-api/fwd_api/request/UpsertUserRequest.py
Executable file
13
cope2n-api/fwd_api/request/UpsertUserRequest.py
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class UpsertUserRequest(serializers.Serializer):
|
||||||
|
plan_code = serializers.CharField(required=False, max_length=100, allow_blank=False)
|
||||||
|
status = serializers.IntegerField(required=False, max_value=1)
|
||||||
|
plan_start_at = serializers.CharField(required=False, max_length=100, allow_blank=False)
|
||||||
|
email = serializers.EmailField(required=False, max_length=200, allow_blank=True)
|
||||||
|
name = serializers.CharField(required=False, max_length=200, allow_blank=True)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
# Todo Validate here
|
||||||
|
return attrs
|
0
cope2n-api/fwd_api/request/__init__.py
Executable file
0
cope2n-api/fwd_api/request/__init__.py
Executable file
25
cope2n-api/fwd_api/response/ReportFileSerializer.py
Executable file
25
cope2n-api/fwd_api/response/ReportFileSerializer.py
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from fwd_api.constant.common import FileCategory, FolderFileType
|
||||||
|
from fwd_api.models import SubscriptionRequestFile, SubscriptionRequest
|
||||||
|
from fwd_api.utils import FileUtils
|
||||||
|
|
||||||
|
|
||||||
|
class ReportFileSerializer(serializers.Serializer):
|
||||||
|
file_name = serializers.CharField(read_only=True)
|
||||||
|
file_url = serializers.SerializerMethodField()
|
||||||
|
file_category = serializers.CharField(max_length=200, default=FileCategory.Origin.value)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""
|
||||||
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
|
"""
|
||||||
|
return SubscriptionRequestFile.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def get_file_url(self, obj: SubscriptionRequestFile):
|
||||||
|
rq: SubscriptionRequest = obj.request
|
||||||
|
return FileUtils.build_url(FolderFileType.REQUESTS.value, rq.request_id, rq.subscription.user.id, obj.file_name)
|
81
cope2n-api/fwd_api/response/ReportSerializer.py
Executable file
81
cope2n-api/fwd_api/response/ReportSerializer.py
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.constant.common import ProcessType, FileCategory
|
||||||
|
from fwd_api.models import SubscriptionRequest, SubscriptionRequestFile
|
||||||
|
from fwd_api.response.ReportFileSerializer import ReportFileSerializer
|
||||||
|
from fwd_api.utils.DateUtil import FORMAT
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from fwd_api.utils import FileUtils
|
||||||
|
|
||||||
|
def i18n_for_label(data) -> list:
|
||||||
|
if 'fields' in data and isinstance(data['fields'], list):
|
||||||
|
return list(map(lambda field: {**field, 'label': _(field['label'])}, data['fields']))
|
||||||
|
return []
|
||||||
|
|
||||||
|
class ReportSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
fwd_request_id = serializers.CharField(required=False)
|
||||||
|
pages = serializers.IntegerField(read_only=True)
|
||||||
|
# doc_type = serializers.CharField(required=False)
|
||||||
|
created_at = serializers.DateTimeField(required=False, format=FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
response_uri = serializers.CharField(required=False)
|
||||||
|
data = serializers.SerializerMethodField()
|
||||||
|
# predict_result = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
status = serializers.CharField()
|
||||||
|
files = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""
|
||||||
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
|
"""
|
||||||
|
return SubscriptionRequest.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def get_files(self, obj: SubscriptionRequest):
|
||||||
|
|
||||||
|
data = SubscriptionRequestFile.objects.filter(request=obj, file_category=FileCategory.Origin.value)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
return ReportFileSerializer(data, many=True).data
|
||||||
|
|
||||||
|
def get_data(self, obj: SubscriptionRequest):
|
||||||
|
data = obj.predict_result
|
||||||
|
sub = obj.subscription
|
||||||
|
user_id = sub.user.id
|
||||||
|
sync_id = sub.user.sync_id
|
||||||
|
sub_id = sub.id
|
||||||
|
if int(obj.process_type) == ProcessType.FI_INVOICE.value:
|
||||||
|
if data and 'content' in data and 'combine_results' in data['content']:
|
||||||
|
return data['content']['combine_results']
|
||||||
|
return ""
|
||||||
|
if data and 'content' in data:
|
||||||
|
model_status = data['status']
|
||||||
|
data = data['content']
|
||||||
|
data.pop('document_type', None)
|
||||||
|
data['status'] = model_status
|
||||||
|
|
||||||
|
if 'pages' in data and isinstance(data['pages'], list):
|
||||||
|
new_data = []
|
||||||
|
for page in data['pages']:
|
||||||
|
new_page_object = {
|
||||||
|
'page_index': page['page_index'],
|
||||||
|
'fields': i18n_for_label(page),
|
||||||
|
'image_url': FileUtils.build_media_url_v2(str(page['request_file_id']), user_id, sub_id, sync_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'path_image_croped' in page:
|
||||||
|
new_page_object['image_url'] = FileUtils.build_media_url_v2(str(page['path_image_croped']), user_id, sub_id, sync_id)
|
||||||
|
new_data.append(new_page_object)
|
||||||
|
data['pages'] = new_data
|
||||||
|
return data
|
||||||
|
|
||||||
|
# def get_predict_result(self, obj: SubscriptionRequest):
|
||||||
|
# from fwd_api.constant.common import ProcessType
|
||||||
|
# typez = int(obj.process_type)
|
||||||
|
# if typez == ProcessType.OCR_WITH_BOX.value or typez == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
# return obj.predict_result
|
||||||
|
# return None
|
30
cope2n-api/fwd_api/response/SubscriptionResponse.py
Executable file
30
cope2n-api/fwd_api/response/SubscriptionResponse.py
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.models import SubscriptionRequest, Subscription
|
||||||
|
|
||||||
|
from fwd_api.utils.DateUtil import FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionResponse(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
current_token = serializers.IntegerField(required=False)
|
||||||
|
limit_token = serializers.IntegerField(required=False)
|
||||||
|
start_at = serializers.DateTimeField(required=False, format=FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
expired_at = serializers.DateTimeField(required=False, format=FORMAT.DD_MM_YYYY_HHMMSS.value)
|
||||||
|
plan_code = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""
|
||||||
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
|
"""
|
||||||
|
return SubscriptionRequest.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def get_plan_code(self, obj: Subscription):
|
||||||
|
return obj.pricing_plan.code
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
43
cope2n-api/fwd_api/response/TemplateResponse.py
Executable file
43
cope2n-api/fwd_api/response/TemplateResponse.py
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from fwd_api.constant.common import TEMPLATE_BOX_TYPE, FolderFileType
|
||||||
|
|
||||||
|
from fwd_api.models.OcrTemplate import OcrTemplate
|
||||||
|
from fwd_api.models.OcrTemplateBox import OcrTemplateBox
|
||||||
|
from fwd_api.utils import FileUtils
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateBoxResponse(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
name = serializers.CharField(required=False)
|
||||||
|
coordinates = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_coordinates(self, obj: OcrTemplateBox):
|
||||||
|
if not obj.coordinates:
|
||||||
|
return []
|
||||||
|
list_box = obj.coordinates.split(",")
|
||||||
|
return list_box
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateResponse(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
name = serializers.CharField(required=False)
|
||||||
|
data_boxs = serializers.SerializerMethodField()
|
||||||
|
anchor_boxs = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
file_url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_file_url(self, obj: OcrTemplate):
|
||||||
|
return FileUtils.build_url(FolderFileType.TEMPLATES.value, str(obj.id), obj.subscription.user.id)
|
||||||
|
|
||||||
|
def get_data_boxs(self, obj: OcrTemplate):
|
||||||
|
data = OcrTemplateBox.objects.filter(template=obj, type=TEMPLATE_BOX_TYPE.DATA.value)
|
||||||
|
if len(data) == 0:
|
||||||
|
return []
|
||||||
|
return TemplateBoxResponse(data, many=True).data
|
||||||
|
|
||||||
|
def get_anchor_boxs(self, obj: OcrTemplate):
|
||||||
|
data = OcrTemplateBox.objects.filter(template=obj, type=TEMPLATE_BOX_TYPE.ANCHOR.value)
|
||||||
|
if len(data) == 0:
|
||||||
|
return []
|
||||||
|
return TemplateBoxResponse(data, many=True).data
|
0
cope2n-api/fwd_api/response/__init__.py
Executable file
0
cope2n-api/fwd_api/response/__init__.py
Executable file
0
cope2n-api/fwd_api/response/models/__init__.py
Executable file
0
cope2n-api/fwd_api/response/models/__init__.py
Executable file
22
cope2n-api/fwd_api/schema.py
Executable file
22
cope2n-api/fwd_api/schema.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
from drf_spectacular.extensions import OpenApiFilterExtension
|
||||||
|
from drf_spectacular.openapi import AutoSchema as SpectacularAutoSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AutoSchema(SpectacularAutoSchema):
|
||||||
|
def _get_filter_parameters(self):
|
||||||
|
if not (self._is_a_general_list_view() or self._is_list_view()):
|
||||||
|
return []
|
||||||
|
if getattr(self.view, 'filter_backends', None) is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
parameters = []
|
||||||
|
for filter_backend in self.view.filter_backends:
|
||||||
|
filter_extension = OpenApiFilterExtension.get_match(filter_backend())
|
||||||
|
if filter_extension:
|
||||||
|
parameters += filter_extension.get_schema_operation_parameters(self)
|
||||||
|
else:
|
||||||
|
parameters += filter_backend().get_schema_operation_parameters(self.view)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
def _is_a_general_list_view(self):
|
||||||
|
return hasattr(self.view, "detail") and self.method.lower() == "get" and not self.view.detail
|
3
cope2n-api/fwd_api/tests.py
Executable file
3
cope2n-api/fwd_api/tests.py
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
118
cope2n-api/fwd_api/utils/CryptoUtils.py
Executable file
118
cope2n-api/fwd_api/utils/CryptoUtils.py
Executable file
@ -0,0 +1,118 @@
|
|||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from fwd_api.annotation.api import throw_on_failure
|
||||||
|
from fwd_api.exception.exceptions import InvalidException
|
||||||
|
from fwd_api.utils.DateUtil import default_zone, get_date_time_now
|
||||||
|
|
||||||
|
class InternalCryptor:
|
||||||
|
|
||||||
|
def __init__(self, key):
|
||||||
|
self.cryptor = Fernet(key)
|
||||||
|
|
||||||
|
@throw_on_failure(e=InvalidException(excArgs="data"))
|
||||||
|
def encrypt(self, raw_text: str) -> str:
|
||||||
|
return self.cryptor.encrypt(raw_text.encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
@throw_on_failure(e=InvalidException(excArgs="data"))
|
||||||
|
def decrypt(self, cipher_text: str) -> str:
|
||||||
|
return self.cryptor.decrypt(cipher_text.encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
@throw_on_failure(e=InvalidException(excArgs="data"))
|
||||||
|
def encrypt_json(self, dict_data: dict) -> str:
|
||||||
|
return self.cryptor.encrypt(json.dumps(dict_data).encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
@throw_on_failure(e=InvalidException(excArgs="data"))
|
||||||
|
def decrypt_json(self, cipher_text: str) -> dict:
|
||||||
|
raw_data = self.cryptor.decrypt(cipher_text.encode('utf-8')).decode('utf-8')
|
||||||
|
data = json.loads(raw_data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
sds_db_encryptor = InternalCryptor(key=settings.DB_ENCRYPT_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
class SdsAuthentication:
|
||||||
|
key = settings.INTERNAL_SDS_KEY
|
||||||
|
base_url_query = settings.BASE_UI_URL + "?cope2n-token={}"
|
||||||
|
|
||||||
|
def __init__(self, life_time, algorithm):
|
||||||
|
self.duration_of_token = life_time
|
||||||
|
self.algorithm = algorithm
|
||||||
|
|
||||||
|
def encode_data(self, data: dict) -> str:
|
||||||
|
return jwt.encode(data, self.key, algorithm=self.algorithm)
|
||||||
|
|
||||||
|
def decode_data(self, data: str) -> str:
|
||||||
|
return jwt.decode(data, self.key, algorithms=self.algorithm)
|
||||||
|
|
||||||
|
def generate_token(self, user_id, internal_id, status, sub_id=-1) -> str:
|
||||||
|
c_time = get_date_time_now() + datetime.timedelta(hours=self.duration_of_token)
|
||||||
|
c_time.replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
from fwd_api.utils import DateUtil
|
||||||
|
from fwd_api.utils.DateUtil import FORMAT
|
||||||
|
payload = {"id": user_id, "expired_at": DateUtil.to_str(c_time, FORMAT.DD_MM_YYYY_HHMMSS.value),
|
||||||
|
'internal_id': internal_id, 'status': status, 'subscription_id': sub_id}
|
||||||
|
return self.encode_data(payload)
|
||||||
|
|
||||||
|
def generate_img_token(self, user_id) -> str:
|
||||||
|
c_time = get_date_time_now() + datetime.timedelta(hours=self.duration_of_token)
|
||||||
|
c_time.replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
from fwd_api.utils import DateUtil
|
||||||
|
from fwd_api.utils.DateUtil import FORMAT
|
||||||
|
payload = {"expired_at": DateUtil.to_str(c_time, FORMAT.DD_MM_YYYY_HHMMSS.value), "internal_id": user_id}
|
||||||
|
return self.encode_data(payload)
|
||||||
|
|
||||||
|
def generate_img_token_v2(self, user_id, sub_id=None, user_sync_id=None) -> str:
|
||||||
|
c_time = get_date_time_now() + datetime.timedelta(hours=self.duration_of_token)
|
||||||
|
c_time.replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
from fwd_api.utils import DateUtil
|
||||||
|
from fwd_api.utils.DateUtil import FORMAT
|
||||||
|
payload = {"expired_at": DateUtil.to_str(c_time, FORMAT.DD_MM_YYYY_HHMMSS.value), "internal_id": user_id, 'subscription_id': sub_id, "id": user_sync_id}
|
||||||
|
return self.encode_data(payload)
|
||||||
|
|
||||||
|
def generate_url(self, user_id, internal_id, status, sub_id) -> str:
|
||||||
|
return self.base_url_query.format(self.generate_token(user_id, internal_id, status, sub_id))
|
||||||
|
|
||||||
|
|
||||||
|
sds_authenticator = SdsAuthentication(life_time=settings.AUTH_TOKEN_LIFE_TIME, algorithm="HS512")
|
||||||
|
admin_sds_authenticator = SdsAuthentication(life_time=settings.AUTH_TOKEN_LIFE_TIME * 10, algorithm="HS512")
|
||||||
|
image_authenticator = SdsAuthentication(life_time=settings.IMAGE_TOKEN_LIFE_TIME, algorithm="HS512")
|
||||||
|
|
||||||
|
|
||||||
|
class CtelCrypto:
|
||||||
|
key = settings.CTEL_KEY
|
||||||
|
cypher = Cipher(algorithms.AES256(key.encode()), modes.CTR(os.urandom(16)))
|
||||||
|
|
||||||
|
def encrypt_ctel(self, text: str, init_vector: str) -> str:
|
||||||
|
# cypher equal to self.cypher if use one encryptor
|
||||||
|
cypher = Cipher(algorithms.AES256(self.key.encode()), modes.CTR(bytes.fromhex(init_vector)))
|
||||||
|
encryptor = cypher.encryptor()
|
||||||
|
en_text = (encryptor.update(text.encode()) + encryptor.finalize())
|
||||||
|
return en_text.hex()
|
||||||
|
|
||||||
|
def decrypt_ctel(self, encrypted_hex_text: str, init_vector: str):
|
||||||
|
# cypher equal to self.cypher if use one encryptor
|
||||||
|
cypher = Cipher(algorithms.AES256(self.key.encode()), modes.CTR(bytes.fromhex(init_vector)))
|
||||||
|
decryptor = cypher.decryptor()
|
||||||
|
decrypted_text = (decryptor.update(bytes.fromhex(encrypted_hex_text)) + decryptor.finalize())
|
||||||
|
return decrypted_text.decode()
|
||||||
|
|
||||||
|
|
||||||
|
ctel_cryptor = CtelCrypto()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Gen fake token
|
||||||
|
ic = SdsAuthentication(life_time=130, algorithm="HS512")
|
||||||
|
print(ic.generate_token('xmmkoyfpjc', 49, 1, 1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
33
cope2n-api/fwd_api/utils/DateUtil.py
Executable file
33
cope2n-api/fwd_api/utils/DateUtil.py
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from fwd_api.exception.exceptions import InvalidException
|
||||||
|
|
||||||
|
default_zone = settings.TIME_ZONE
|
||||||
|
|
||||||
|
|
||||||
|
class FORMAT(Enum):
|
||||||
|
DD_MM_YYYY_HHMMSS = "%d/%m/%Y %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
|
def to_date(date_str: str, format_date: str) -> datetime:
|
||||||
|
try:
|
||||||
|
from_date: datetime = datetime.strptime(date_str, format_date)
|
||||||
|
from_date = from_date.replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
return from_date
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidException(excArgs='dateFormat')
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(date: datetime, format: str) -> str:
|
||||||
|
try:
|
||||||
|
return date.strftime(format)
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidException(excArgs='dateFormat')
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_time_now():
|
||||||
|
# return datetime.utcnow().replace(tzinfo=ZoneInfo(default_zone))
|
||||||
|
return datetime.now(tz=ZoneInfo(default_zone))
|
218
cope2n-api/fwd_api/utils/FileUtils.py
Executable file
218
cope2n-api/fwd_api/utils/FileUtils.py
Executable file
@ -0,0 +1,218 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from PIL import Image, ExifTags
|
||||||
|
from django.core.files.uploadedfile import TemporaryUploadedFile
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from fwd_api.constant.common import allowed_file_extensions
|
||||||
|
from fwd_api.exception.exceptions import GeneralException, RequiredFieldException, InvalidException, \
|
||||||
|
ServiceUnavailableException, FileFormatInvalidException, LimitReachedException
|
||||||
|
from fwd_api.models import SubscriptionRequest, OcrTemplate
|
||||||
|
from fwd_api.utils import ProcessUtil
|
||||||
|
from fwd_api.utils.CryptoUtils import image_authenticator
|
||||||
|
from ..celery_worker.client_connector import c_connector
|
||||||
|
|
||||||
|
def validate_list_file(files, max_file_num=settings.MAX_UPLOAD_FILES_IN_A_REQUEST, min_file_num=1, file_field="files"):
|
||||||
|
total_file_size = 0
|
||||||
|
if len(files) < min_file_num:
|
||||||
|
raise RequiredFieldException(excArgs=file_field)
|
||||||
|
if len(files) > max_file_num:
|
||||||
|
raise LimitReachedException(excArgs=(f'Number of {file_field}', str(max_file_num), ''))
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
print(f'dafile {f} is file type{type(f)}')
|
||||||
|
if not isinstance(f, TemporaryUploadedFile):
|
||||||
|
# print(f'[DEBUG]: {f.name}')
|
||||||
|
raise InvalidException(excArgs="files")
|
||||||
|
extension = f.name.split(".")[-1] in allowed_file_extensions
|
||||||
|
if not extension or "." not in f.name:
|
||||||
|
raise FileFormatInvalidException(excArgs=allowed_file_extensions)
|
||||||
|
if f.size > settings.MAX_UPLOAD_SIZE_OF_A_FILE:
|
||||||
|
raise LimitReachedException(excArgs=('A file', str(settings.MAX_UPLOAD_SIZE_OF_A_FILE / 1024 / 1024), 'MB'))
|
||||||
|
total_file_size += f.size
|
||||||
|
if total_file_size > settings.MAX_UPLOAD_FILE_SIZE_OF_A_REQUEST:
|
||||||
|
raise LimitReachedException(excArgs=('Total size of all files', str(settings.MAX_UPLOAD_SIZE_OF_A_FILE / 1024 / 1024), 'MB'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_file(file_path: str):
|
||||||
|
try:
|
||||||
|
return open(file_path, 'rb')
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise GeneralException("System")
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_folder_path(tem: OcrTemplate):
|
||||||
|
tem_id = str(tem.id)
|
||||||
|
sub_id = str(tem.subscription.id)
|
||||||
|
user_id = str(tem.subscription.user.id)
|
||||||
|
return os.path.join(settings.MEDIA_ROOT, 'users', user_id, "subscriptions", sub_id, "templates", tem_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_folder_path(rq: SubscriptionRequest):
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
request_id = str(rq.request_id)
|
||||||
|
logger.info(f"[DEBUG]: rq.process_type: {rq.process_type}")
|
||||||
|
p_type = ProcessUtil.map_process_type_to_folder_name(int(rq.process_type))
|
||||||
|
sub_id = str(rq.subscription.id)
|
||||||
|
user_id = str(rq.subscription.user.id)
|
||||||
|
|
||||||
|
return os.path.join(settings.MEDIA_ROOT, 'users', user_id, "subscriptions", sub_id, 'requests', p_type, request_id)
|
||||||
|
|
||||||
|
|
||||||
|
def save_byte_file(file_name: str, rq: SubscriptionRequest, file_bytes):
|
||||||
|
folder_path = get_folder_path(rq)
|
||||||
|
is_exist = os.path.exists(folder_path)
|
||||||
|
if not is_exist:
|
||||||
|
# Create a new directory because it does not exist
|
||||||
|
os.makedirs(folder_path)
|
||||||
|
file_path = os.path.join(folder_path, file_name)
|
||||||
|
with open(file_path, 'wb+') as w:
|
||||||
|
w.write(file_bytes)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
def save_file(file_name: str, rq: SubscriptionRequest, file: TemporaryUploadedFile):
|
||||||
|
folder_path = get_folder_path(rq)
|
||||||
|
is_exist = os.path.exists(folder_path)
|
||||||
|
if not is_exist:
|
||||||
|
# Create a new directory because it does not exist
|
||||||
|
os.makedirs(folder_path)
|
||||||
|
file_path = os.path.join(folder_path, file_name)
|
||||||
|
f = open(file_path, 'wb+')
|
||||||
|
|
||||||
|
for chunk in file.chunks():
|
||||||
|
f.write(chunk)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
def delete_file_with_path(file_path: str) -> bool:
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_template_file(file_name: str, rq: OcrTemplate, file: TemporaryUploadedFile, quality):
|
||||||
|
try:
|
||||||
|
folder_path = get_template_folder_path(rq)
|
||||||
|
is_exist = os.path.exists(folder_path)
|
||||||
|
if not is_exist:
|
||||||
|
# Create a new directory because it does not exist
|
||||||
|
os.makedirs(folder_path)
|
||||||
|
return save_file_with_path(file_name, file, quality, folder_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
|
||||||
|
|
||||||
|
def resize_and_save_file(file_name: str, rq: SubscriptionRequest, file: TemporaryUploadedFile, quality):
|
||||||
|
try:
|
||||||
|
folder_path = get_folder_path(rq)
|
||||||
|
# print(f"[DEBUG]: folder_path: {folder_path}")
|
||||||
|
is_exist = os.path.exists(folder_path)
|
||||||
|
if not is_exist:
|
||||||
|
# Create a new directory because it does not exist
|
||||||
|
os.makedirs(folder_path)
|
||||||
|
return save_file_with_path(file_name, file, quality, folder_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR]: {e}")
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
|
||||||
|
def save_to_S3(file_name, rq, obj):
|
||||||
|
try:
|
||||||
|
base64_obj = base64.b64encode(obj).decode('utf-8')
|
||||||
|
file_path = get_folder_path(rq)
|
||||||
|
assert len(file_path.split("/")) >= 2, "file_path must have at least process type and request id"
|
||||||
|
s3_key = os.path.join(file_path.split("/")[-2], file_path.split("/")[-1], file_name)
|
||||||
|
# c_connector.upload_file_to_s3((file_path, s3_key))
|
||||||
|
c_connector.upload_obj_to_s3((base64_obj, s3_key))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR]: {e}")
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
|
||||||
|
def save_file_with_path(file_name: str, file: TemporaryUploadedFile, quality, folder_path):
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(folder_path, file_name)
|
||||||
|
extension = file_name.split(".")[-1]
|
||||||
|
|
||||||
|
if extension in ['pdf', 'PDF']:
|
||||||
|
save_pdf(file_path, file)
|
||||||
|
else:
|
||||||
|
save_img(file_path, file, quality)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
def save_pdf(file_path: str, file: TemporaryUploadedFile):
|
||||||
|
f = open(file_path, 'wb+')
|
||||||
|
for chunk in file.chunks():
|
||||||
|
f.write(chunk)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
def save_img(file_path: str, file: TemporaryUploadedFile, quality):
|
||||||
|
with open(file.temporary_file_path(), "rb") as fs:
|
||||||
|
input_file = io.BytesIO(fs.read())
|
||||||
|
image = Image.open(input_file)
|
||||||
|
|
||||||
|
# read orient from metadata. WindowsPhoto keep the origin
|
||||||
|
for orientation in ExifTags.TAGS.keys():
|
||||||
|
if ExifTags.TAGS[orientation] == 'Orientation':
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
e = image._getexif() # returns None if no EXIF data
|
||||||
|
if e:
|
||||||
|
exif = dict(e.items())
|
||||||
|
if orientation in exif:
|
||||||
|
orientation = exif[orientation]
|
||||||
|
if orientation == 3:
|
||||||
|
image = image.transpose(Image.ROTATE_180)
|
||||||
|
elif orientation == 6:
|
||||||
|
image = image.transpose(Image.ROTATE_270)
|
||||||
|
elif orientation == 8:
|
||||||
|
image = image.transpose(Image.ROTATE_90)
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex)
|
||||||
|
print("Rotation Error")
|
||||||
|
traceback.print_exc()
|
||||||
|
image.convert('RGB').save(file_path, optimize=True, quality=quality)
|
||||||
|
|
||||||
|
def build_media_url(folder: str, uid: str, file_name: str = None) -> str:
|
||||||
|
token = image_authenticator.generate_img_token()
|
||||||
|
if not file_name:
|
||||||
|
return '{base_url}/api/ctel/media/{folder}/{uid}/?token={token}'.format(folder=folder, uid=uid,
|
||||||
|
base_url=settings.BASE_URL,
|
||||||
|
token=token)
|
||||||
|
return '{base_url}/api/ctel/media/{folder}/{uid}/?file_name={file_name}&token={token}'.format(folder=folder,
|
||||||
|
uid=uid,
|
||||||
|
file_name=file_name,
|
||||||
|
base_url=settings.BASE_URL,
|
||||||
|
token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def build_url(folder: str, data_id: str, user_id: int, file_name: str = None) -> str:
|
||||||
|
token = image_authenticator.generate_img_token(user_id)
|
||||||
|
if not file_name:
|
||||||
|
return '{base_url}/api/ctel/media/{folder}/{uid}/?token={token}'.format(folder=folder, uid=data_id,
|
||||||
|
base_url=settings.BASE_URL,
|
||||||
|
token=token)
|
||||||
|
return '{base_url}/api/ctel/media/{folder}/{uid}/?file_name={file_name}&token={token}'.format(folder=folder,
|
||||||
|
uid=data_id,
|
||||||
|
file_name=file_name,
|
||||||
|
base_url=settings.BASE_URL,
|
||||||
|
token=token)
|
||||||
|
def build_media_url_v2(media_id: str, user_id: int, sub_id: int, u_sync_id: str) -> str:
|
||||||
|
token = image_authenticator.generate_img_token_v2(user_id, sub_id, u_sync_id)
|
||||||
|
return f'{settings.BASE_URL}/api/ctel/v2/media/request/{media_id}/?token={token}'
|
4
cope2n-api/fwd_api/utils/NumberUtils.py
Executable file
4
cope2n-api/fwd_api/utils/NumberUtils.py
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
def is_number_repl_isdigit(s):
|
||||||
|
""" Returns True if string is a number. """
|
||||||
|
return s.replace('.', '', 1).isdigit()
|
425
cope2n-api/fwd_api/utils/ProcessUtil.py
Executable file
425
cope2n-api/fwd_api/utils/ProcessUtil.py
Executable file
@ -0,0 +1,425 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import tempfile
|
||||||
|
import fitz
|
||||||
|
import PyPDF2
|
||||||
|
from django.core.files.uploadedfile import TemporaryUploadedFile
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from fwd import settings
|
||||||
|
from fwd_api.constant.common import LIST_BOX_MESSAGE, pattern, NAME_MESSAGE, allowed_p_type, TEMPLATE_ID, \
|
||||||
|
FolderFileType, FileCategory
|
||||||
|
from fwd_api.exception.exceptions import NumberOfBoxLimitReachedException, \
|
||||||
|
ServiceUnavailableException, DuplicateEntityException, LimitReachedException, BadGatewayException
|
||||||
|
from fwd_api.utils import DateUtil, FileUtils
|
||||||
|
from ..constant.common import ProcessType, TEMPLATE_BOX_TYPE, EntityStatus
|
||||||
|
from ..exception.exceptions import InvalidException, NotFoundException, \
|
||||||
|
PermissionDeniedException, RequiredFieldException, InvalidException
|
||||||
|
from ..models import UserProfile, OcrTemplate, OcrTemplateBox, \
|
||||||
|
Subscription, SubscriptionRequestFile, SubscriptionRequest
|
||||||
|
from ..celery_worker.client_connector import c_connector
|
||||||
|
import uuid
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
class UserData:
|
||||||
|
user: UserProfile = None
|
||||||
|
current_sub: Subscription = None
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
user_data = validate_user_request_and_get(request)
|
||||||
|
users = UserProfile.objects.filter(sync_id=user_data['id'])
|
||||||
|
subs = Subscription.objects.filter(id=user_data['subscription_id'])
|
||||||
|
|
||||||
|
subs_num = len(subs)
|
||||||
|
users_num = len(users)
|
||||||
|
|
||||||
|
if subs_num == 0:
|
||||||
|
raise NotFoundException(excArgs='subscription')
|
||||||
|
if users_num == 0:
|
||||||
|
raise NotFoundException(excArgs='user')
|
||||||
|
|
||||||
|
if subs_num > 1:
|
||||||
|
raise DuplicateEntityException(excArgs='subscription')
|
||||||
|
if users_num > 1:
|
||||||
|
raise DuplicateEntityException(excArgs='user')
|
||||||
|
|
||||||
|
user = users[0]
|
||||||
|
sub = subs[0]
|
||||||
|
if user.id != sub.user.id:
|
||||||
|
raise PermissionDeniedException()
|
||||||
|
if sub.status != EntityStatus.ACTIVE.value:
|
||||||
|
raise InvalidException(excArgs='Subscription status')
|
||||||
|
if sub.expired_at < DateUtil.get_date_time_now():
|
||||||
|
raise InvalidException(excArgs='Subscription')
|
||||||
|
if user.status != EntityStatus.ACTIVE.value:
|
||||||
|
raise InvalidException(excArgs='User status')
|
||||||
|
self.user = user
|
||||||
|
self.current_sub = sub
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(request) -> UserData:
|
||||||
|
return UserData(request)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_user_request_and_get(request):
|
||||||
|
if not hasattr(request, 'user_data'):
|
||||||
|
raise NotFoundException(excArgs='user')
|
||||||
|
data = request.user_data
|
||||||
|
if 'internal_id' not in data:
|
||||||
|
raise NotFoundException(excArgs='user')
|
||||||
|
if 'subscription_id' not in data:
|
||||||
|
raise NotFoundException(excArgs='subscription')
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ocr_request_and_get(request, subscription):
|
||||||
|
validated_data = {}
|
||||||
|
if "processType" not in request.data or request.data['processType'] is None \
|
||||||
|
or not request.data['processType'].isnumeric() or int(request.data['processType']) not in allowed_p_type:
|
||||||
|
raise InvalidException(excArgs='processType')
|
||||||
|
p_type: int = int(request.data['processType'])
|
||||||
|
validated_data['type'] = p_type
|
||||||
|
|
||||||
|
if subscription.current_token + token_value(p_type) >= subscription.limit_token:
|
||||||
|
raise LimitReachedException(excArgs=('Number of tokens', str(subscription.limit_token), 'times'))
|
||||||
|
|
||||||
|
if p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
if "templateId" not in request.data:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
temp_id = request.data['templateId']
|
||||||
|
temp = OcrTemplate.objects.filter(id=temp_id, subscription=subscription)
|
||||||
|
if len(temp) != 1:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
validated_data['template'] = temp
|
||||||
|
|
||||||
|
list_file = request.data.getlist('file')
|
||||||
|
FileUtils.validate_list_file(list_file)
|
||||||
|
validated_data['file'] = list_file[0]
|
||||||
|
|
||||||
|
return validated_data
|
||||||
|
|
||||||
|
def sbt_validate_ocr_request_and_get(request, subscription):
|
||||||
|
validated_data = {}
|
||||||
|
# if "processType" not in request.data or request.data['processType'] is None \
|
||||||
|
# or not request.data['processType'].isnumeric() or int(request.data['processType']) not in allowed_p_type:
|
||||||
|
# raise InvalidException(excArgs='processType')
|
||||||
|
# p_type: int = int(request.data['processType'])
|
||||||
|
p_type = 12
|
||||||
|
validated_data['type'] = p_type # hard fix to be of type SBT Invoice
|
||||||
|
|
||||||
|
if subscription.current_token + token_value(p_type) >= subscription.limit_token:
|
||||||
|
raise LimitReachedException(excArgs=('Number of tokens', str(subscription.limit_token), 'times'))
|
||||||
|
|
||||||
|
if p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
if "templateId" not in request.data:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
temp_id = request.data['templateId']
|
||||||
|
temp = OcrTemplate.objects.filter(id=temp_id, subscription=subscription)
|
||||||
|
if len(temp) != 1:
|
||||||
|
raise InvalidException(excArgs=TEMPLATE_ID)
|
||||||
|
validated_data['template'] = temp
|
||||||
|
|
||||||
|
imei_files = request.data.getlist('imei_files')
|
||||||
|
invoice_file = request.data.getlist('invoice_file')
|
||||||
|
redemption_ID = request.data.get('redemption_ID', None)
|
||||||
|
|
||||||
|
FileUtils.validate_list_file(imei_files, file_field="imei_file")
|
||||||
|
FileUtils.validate_list_file(invoice_file, max_file_num=1, min_file_num=0, file_field="invoice_file")
|
||||||
|
# if not isinstance(redemption_ID, str):
|
||||||
|
# # raise RequiredFieldException(excArgs="redemption_ID")
|
||||||
|
# raise InvalidException(excArgs="redemption_ID")
|
||||||
|
|
||||||
|
validated_data['imei_file'] = imei_files
|
||||||
|
validated_data['invoice_file'] = invoice_file
|
||||||
|
validated_data['redemption_ID'] = redemption_ID
|
||||||
|
|
||||||
|
return validated_data
|
||||||
|
|
||||||
|
def count_pages_in_pdf(pdf_file):
|
||||||
|
count = 0
|
||||||
|
fh, temp_filename = tempfile.mkstemp() # make a tmp file
|
||||||
|
f = os.fdopen(fh, 'wb+') # open the tmp file for writing
|
||||||
|
for chunk in pdf_file.chunks():
|
||||||
|
f.write(chunk)
|
||||||
|
read_pdf = PyPDF2.PdfFileReader(f, strict=False)
|
||||||
|
count = read_pdf.numPages
|
||||||
|
f.close()
|
||||||
|
os.remove(temp_filename)
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def count_pages_in_pdf_list(list_file):
|
||||||
|
total_page = 0
|
||||||
|
|
||||||
|
for file_obj in list_file:
|
||||||
|
total_page += count_pages_in_pdf(file_obj)
|
||||||
|
|
||||||
|
return total_page
|
||||||
|
|
||||||
|
|
||||||
|
def map_process_type_to_folder_name(p_type):
|
||||||
|
if p_type == ProcessType.ID_CARD.value:
|
||||||
|
return 'id_card'
|
||||||
|
elif p_type == ProcessType.DRIVER_LICENSE.value:
|
||||||
|
return 'driver_license'
|
||||||
|
elif p_type == ProcessType.INVOICE.value:
|
||||||
|
return 'invoice'
|
||||||
|
elif p_type == ProcessType.OCR_WITH_BOX.value:
|
||||||
|
return 'basic_ocr'
|
||||||
|
elif p_type == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
return 'template_matching'
|
||||||
|
elif p_type == ProcessType.AP_INVOICE.value:
|
||||||
|
return 'ap_invoice'
|
||||||
|
elif p_type == ProcessType.FI_INVOICE.value:
|
||||||
|
return 'fi_invoice'
|
||||||
|
elif p_type == ProcessType.MANULIFE_INVOICE.value:
|
||||||
|
return 'manulife_invoice'
|
||||||
|
elif p_type == ProcessType.SBT_INVOICE.value:
|
||||||
|
return 'sbt_invoice'
|
||||||
|
else:
|
||||||
|
raise InvalidException(excArgs='processType')
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_string(length):
|
||||||
|
# choose from all lowercase letter
|
||||||
|
letters = string.ascii_lowercase
|
||||||
|
result_str = ''.join(random.choice(letters) for _ in range(length))
|
||||||
|
print("Random string of length", length, "is:", result_str)
|
||||||
|
return result_str
|
||||||
|
|
||||||
|
|
||||||
|
def is_int(text) -> bool:
|
||||||
|
try:
|
||||||
|
# converting to integer
|
||||||
|
int(text)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_box(list_box, max_number_of_box, max_number_of_item_in_a_box, number_of_box=None):
|
||||||
|
if len(list_box) > max_number_of_box:
|
||||||
|
raise NumberOfBoxLimitReachedException(excArgs=LIST_BOX_MESSAGE)
|
||||||
|
|
||||||
|
if number_of_box and len(list_box) != number_of_box:
|
||||||
|
raise InvalidException(excArgs=LIST_BOX_MESSAGE)
|
||||||
|
|
||||||
|
for box in list_box:
|
||||||
|
if len(box) != max_number_of_item_in_a_box:
|
||||||
|
raise InvalidException(excArgs="box coordinates")
|
||||||
|
|
||||||
|
|
||||||
|
def to_box_list(str_list):
|
||||||
|
ls = []
|
||||||
|
if not str_list:
|
||||||
|
raise InvalidException(excArgs=LIST_BOX_MESSAGE)
|
||||||
|
box_list = str_list.split(";")
|
||||||
|
for box_str in box_list:
|
||||||
|
if not box_str:
|
||||||
|
raise InvalidException(excArgs=LIST_BOX_MESSAGE)
|
||||||
|
ls.append(box_str.split(","))
|
||||||
|
return ls
|
||||||
|
|
||||||
|
|
||||||
|
def validate_json_response_and_return(res):
|
||||||
|
if res.status_code != status.HTTP_200_OK:
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
|
||||||
|
res_data = res.json()
|
||||||
|
if 'status' in res_data and res_data['status'] != 200:
|
||||||
|
raise ServiceUnavailableException()
|
||||||
|
return res_data
|
||||||
|
|
||||||
|
|
||||||
|
def is_duplicate_in_list(str_list):
|
||||||
|
unique_set: set = set({})
|
||||||
|
for label in str_list:
|
||||||
|
if label not in unique_set:
|
||||||
|
unique_set.add(label)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_duplicate(list_box):
|
||||||
|
if is_duplicate_in_list(list_box):
|
||||||
|
raise DuplicateEntityException(excArgs="box_label")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_vn_and_space(txt: str):
|
||||||
|
if not pattern.fullmatch(txt.upper()):
|
||||||
|
raise InvalidException(excArgs=NAME_MESSAGE)
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save_template_boxs(data, template):
|
||||||
|
saving_list = []
|
||||||
|
for d_box in data['data_boxs']:
|
||||||
|
box = OcrTemplateBox(name=d_box['name'], template=template, coordinates=d_box['coordinates'],
|
||||||
|
type=TEMPLATE_BOX_TYPE.DATA.value)
|
||||||
|
saving_list.append(box)
|
||||||
|
for a_box in data['anchor_boxs']:
|
||||||
|
box = OcrTemplateBox(template=template, coordinates=','.join(a_box), type=TEMPLATE_BOX_TYPE.ANCHOR.value)
|
||||||
|
saving_list.append(box)
|
||||||
|
OcrTemplateBox.objects.bulk_create(saving_list)
|
||||||
|
|
||||||
|
|
||||||
|
def token_value(token_type):
|
||||||
|
if token_type == ProcessType.ID_CARD.value or token_type == ProcessType.DRIVER_LICENSE.value:
|
||||||
|
return 3
|
||||||
|
if token_type == ProcessType.TEMPLATE_MATCHING.value or token_type == ProcessType.INVOICE.value:
|
||||||
|
return 5
|
||||||
|
return 1 # Basic OCR
|
||||||
|
|
||||||
|
|
||||||
|
def send_to_queue2(rq_id, sub_id, file_url, user_id, typez):
|
||||||
|
try:
|
||||||
|
if typez == ProcessType.ID_CARD.value:
|
||||||
|
c_connector.process_id(
|
||||||
|
(rq_id, sub_id, map_process_type_to_folder_name(typez), file_url, user_id))
|
||||||
|
elif typez == ProcessType.INVOICE.value:
|
||||||
|
c_connector.process_invoice_sap((rq_id, file_url))
|
||||||
|
elif typez == ProcessType.FI_INVOICE.value:
|
||||||
|
c_connector.process_fi((rq_id, file_url))
|
||||||
|
elif typez == ProcessType.MANULIFE_INVOICE.value:
|
||||||
|
c_connector.process_invoice_manulife((rq_id, file_url))
|
||||||
|
elif typez == ProcessType.SBT_INVOICE.value:
|
||||||
|
c_connector.process_invoice_sbt((rq_id, file_url))
|
||||||
|
# elif typez == ProcessType.DRIVER_LICENSE.value:
|
||||||
|
# c_connector.process_driver_license(
|
||||||
|
# (rq_id, sub_id, map_process_type_to_folder_name(typez), file_url, user_id))
|
||||||
|
# elif typez == ProcessType.OCR_WITH_BOX.value:
|
||||||
|
# c_connector.process_ocr_with_box((rq_id, file_url))
|
||||||
|
# elif typez == ProcessType.TEMPLATE_MATCHING.value:
|
||||||
|
# c_connector.process_template_matching((rq_id, file_url))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise BadGatewayException()
|
||||||
|
|
||||||
|
|
||||||
|
def build_template_matching_data(template):
|
||||||
|
temp_dict = {
|
||||||
|
|
||||||
|
}
|
||||||
|
list_anchor = OcrTemplateBox.objects.filter(template=template, type=TEMPLATE_BOX_TYPE.ANCHOR.value)
|
||||||
|
la = []
|
||||||
|
for a_box in list_anchor:
|
||||||
|
cos = a_box.coordinates.split(",")
|
||||||
|
la.append(cos)
|
||||||
|
temp_dict['anchors'] = la
|
||||||
|
|
||||||
|
list_data = OcrTemplateBox.objects.filter(template=template, type=TEMPLATE_BOX_TYPE.DATA.value)
|
||||||
|
ld = []
|
||||||
|
for d_box in list_data:
|
||||||
|
cos = d_box.coordinates.split(",")
|
||||||
|
ld.append({
|
||||||
|
"box": cos,
|
||||||
|
"label": d_box.name
|
||||||
|
})
|
||||||
|
temp_dict['fields'] = ld
|
||||||
|
|
||||||
|
temp_dict['image_path'] = template.file_path[11:] # len of /app/media/
|
||||||
|
temp_dict['template_name'] = template.name
|
||||||
|
return temp_dict
|
||||||
|
|
||||||
|
|
||||||
|
def send_template_queue(rq_id, file_url, template: OcrTemplate, uid):
|
||||||
|
try:
|
||||||
|
|
||||||
|
template_data = build_template_matching_data(template)
|
||||||
|
folder_name = map_process_type_to_folder_name(ProcessType.TEMPLATE_MATCHING.value)
|
||||||
|
c_connector.process_template_matching(
|
||||||
|
(rq_id, template.subscription.id, folder_name, file_url, template_data, uid))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise BadGatewayException()
|
||||||
|
|
||||||
|
|
||||||
|
def process_pdf_file(file_name: str, file_obj: TemporaryUploadedFile, request: SubscriptionRequest, user) -> list:
|
||||||
|
doc: fitz.Document = fitz.open(stream=file_obj.file.read())
|
||||||
|
if doc.page_count > settings.MAX_PAGES_OF_PDF_FILE:
|
||||||
|
raise LimitReachedException(excArgs=('Number of pages', str(settings.MAX_PAGES_OF_PDF_FILE), 'pages'))
|
||||||
|
request.pages = doc.page_count
|
||||||
|
request.save()
|
||||||
|
# Origin file
|
||||||
|
file_obj.seek(0)
|
||||||
|
file_path = FileUtils.resize_and_save_file(file_name, request, file_obj, 100)
|
||||||
|
new_request_file: SubscriptionRequestFile = SubscriptionRequestFile(file_path=file_path,
|
||||||
|
request=request,
|
||||||
|
file_name=file_name,
|
||||||
|
code=f'FIL{uuid.uuid4().hex}')
|
||||||
|
new_request_file.save()
|
||||||
|
# Sub-file
|
||||||
|
return pdf_to_images_urls(doc, request, user)
|
||||||
|
|
||||||
|
|
||||||
|
def process_image_file(file_name: str, file_obj: TemporaryUploadedFile, request: SubscriptionRequest, user) -> list:
|
||||||
|
if file_obj.size > settings.SIZE_TO_COMPRESS:
|
||||||
|
quality = 95
|
||||||
|
else:
|
||||||
|
quality = 100
|
||||||
|
file_path = FileUtils.resize_and_save_file(file_name, request, file_obj, quality)
|
||||||
|
new_request_file: SubscriptionRequestFile = SubscriptionRequestFile(file_path=file_path,
|
||||||
|
request=request,
|
||||||
|
file_name=file_name,
|
||||||
|
code=f'FIL{uuid.uuid4().hex}')
|
||||||
|
new_request_file.save()
|
||||||
|
return [{
|
||||||
|
'file_url': FileUtils.build_url(FolderFileType.REQUESTS.value, request.request_id, user.id, file_name),
|
||||||
|
'page_number': 0,
|
||||||
|
'request_file_id': new_request_file.code
|
||||||
|
}]
|
||||||
|
|
||||||
|
def pdf_to_images_urls(doc: fitz.Document, request: SubscriptionRequest, user, dpi: int = 300) -> list:
|
||||||
|
def resize(image, max_w=1920, max_h=1080):
|
||||||
|
logger.info(f"[DEBUG]: image.size: {image.size}, type(image): {type(image)}")
|
||||||
|
cur_w, cur_h = image.width, image.height
|
||||||
|
image_bytes = image.samples
|
||||||
|
image = Image.frombytes("RGB", [cur_w, cur_h], image_bytes)
|
||||||
|
if cur_h > max_w or cur_h > max_h:
|
||||||
|
ratio_w = max_w/cur_w
|
||||||
|
ratio_h = max_h/cur_h
|
||||||
|
ratio = min([ratio_h, ratio_w])
|
||||||
|
new_w = int(ratio*cur_w)
|
||||||
|
new_h = int(ratio*cur_h)
|
||||||
|
image = image.resize((new_w, new_h))
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
zoom = dpi // 72
|
||||||
|
magnify = fitz.Matrix(zoom, zoom)
|
||||||
|
pdf_extracted = []
|
||||||
|
for idx, page in enumerate(doc):
|
||||||
|
saving_path = FileUtils.get_folder_path(request)
|
||||||
|
# saving_path = r'C:\Users\mrdra\PycharmProjects\Ctel\test_data'
|
||||||
|
break_file_name = f'break_{idx}.jpg'
|
||||||
|
saving_path = os.path.join(saving_path, break_file_name)
|
||||||
|
|
||||||
|
page = doc.load_page(idx)
|
||||||
|
pix = page.get_pixmap(dpi=250) # render page to an image
|
||||||
|
# pix = resize(pix)
|
||||||
|
# print(f"[DEBUG]: pix.size: {pix.size}")
|
||||||
|
pix.save(saving_path)
|
||||||
|
print(f"Saving {saving_path}")
|
||||||
|
new_request_file: SubscriptionRequestFile = SubscriptionRequestFile(file_path=saving_path,
|
||||||
|
request=request,
|
||||||
|
file_name=break_file_name,
|
||||||
|
file_category=FileCategory.BREAK.value,
|
||||||
|
code=f'FIL{uuid.uuid4().hex}')
|
||||||
|
new_request_file.save()
|
||||||
|
|
||||||
|
file_url = FileUtils.build_url(FolderFileType.REQUESTS.value, request.request_id, user.id, break_file_name)
|
||||||
|
pdf_extracted.append(
|
||||||
|
{
|
||||||
|
'file_url': file_url,
|
||||||
|
'page_number': idx,
|
||||||
|
'request_file_id': new_request_file.code
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return pdf_extracted
|
66
cope2n-api/fwd_api/utils/S3_process.py
Executable file
66
cope2n-api/fwd_api/utils/S3_process.py
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
import boto3
|
||||||
|
|
||||||
|
class MinioS3Client:
|
||||||
|
def __init__(self, endpoint, access_key, secret_key, bucket_name):
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.access_key = access_key
|
||||||
|
self.secret_key = secret_key
|
||||||
|
self.bucket_name = bucket_name
|
||||||
|
try:
|
||||||
|
self.s3_client = boto3.client(
|
||||||
|
's3',
|
||||||
|
endpoint_url=endpoint,
|
||||||
|
aws_access_key_id=access_key,
|
||||||
|
aws_secret_access_key=secret_key
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARM] Unable to create an s3 client, {e}")
|
||||||
|
self.s3_client = None
|
||||||
|
|
||||||
|
def update_object(self, s3_key, content):
|
||||||
|
try:
|
||||||
|
res = self.s3_client.put_object(
|
||||||
|
Bucket=self.bucket_name,
|
||||||
|
Key=s3_key,
|
||||||
|
Body=content
|
||||||
|
)
|
||||||
|
# print(f"Object '{s3_key}' updated in S3 with res: {res}")
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating object in S3: {str(e)}")
|
||||||
|
|
||||||
|
def upload_file(self, local_file_path, s3_key):
|
||||||
|
try:
|
||||||
|
res = self.s3_client.upload_file(local_file_path, self.bucket_name, s3_key)
|
||||||
|
# print(f"File '{local_file_path}' uploaded to S3 with key '{s3_key}'")
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error uploading file to S3: {str(e)}")
|
||||||
|
|
||||||
|
def download_file(self, s3_key, local_file_path):
|
||||||
|
try:
|
||||||
|
res = self.s3_client.download_file(self.bucket_name, s3_key, local_file_path)
|
||||||
|
# print(f"File '{s3_key}' downloaded from S3 to '{local_file_path}'")
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading file from S3: {str(e)}")
|
||||||
|
if __name__=="__main__":
|
||||||
|
|
||||||
|
FILE = "/app/media/users/2/subscriptions/34/requests/invoice/Ctel33aed8ecc189447a81c23a86c5262403/temp_Ctel33aed8ecc189447a81c23a86c5262403.jpg"
|
||||||
|
s3_client = MinioS3Client(
|
||||||
|
endpoint='http://minio:9884',
|
||||||
|
access_key='TannedCung',
|
||||||
|
secret_key='TannedCung',
|
||||||
|
bucket_name='ocr-data'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read the content of the JPG image file
|
||||||
|
with open(FILE, 'rb') as file:
|
||||||
|
image_content = file.read()
|
||||||
|
|
||||||
|
# Update the object in S3 with the new content
|
||||||
|
# res = s3_client.update_object('invoice/viettinbank/image.jpg', image_content)
|
||||||
|
# print(f"[DEBUG]: res: {res}")
|
||||||
|
|
||||||
|
res = s3_client.upload_file(FILE, 'invoic1e/viettinbank/image.jpg')
|
||||||
|
print(f"[DEBUG]: res: {res}")
|
0
cope2n-api/fwd_api/utils/__init__.py
Executable file
0
cope2n-api/fwd_api/utils/__init__.py
Executable file
BIN
cope2n-api/locale/en_US/LC_MESSAGES/django.mo
Executable file
BIN
cope2n-api/locale/en_US/LC_MESSAGES/django.mo
Executable file
Binary file not shown.
133
cope2n-api/locale/en_US/LC_MESSAGES/django.po
Executable file
133
cope2n-api/locale/en_US/LC_MESSAGES/django.po
Executable file
@ -0,0 +1,133 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2023-04-17 13:55+0700\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: .\fwd_api\api\ctel_user_view.py:232
|
||||||
|
msgid "test"
|
||||||
|
msgstr "This is us"
|
||||||
|
|
||||||
|
# LabelMap Invoice
|
||||||
|
msgid "no_value"
|
||||||
|
msgstr "Invoice number"
|
||||||
|
|
||||||
|
msgid "form_value"
|
||||||
|
msgstr "Form number"
|
||||||
|
|
||||||
|
msgid "serial_value"
|
||||||
|
msgstr "Serial"
|
||||||
|
|
||||||
|
msgid "date"
|
||||||
|
msgstr "Date"
|
||||||
|
|
||||||
|
msgid "seller_company_name_value"
|
||||||
|
msgstr "Seller company name"
|
||||||
|
|
||||||
|
msgid "seller_tax_code_value"
|
||||||
|
msgstr "Seller tax code"
|
||||||
|
|
||||||
|
msgid "seller_address_value"
|
||||||
|
msgstr "Seller address"
|
||||||
|
|
||||||
|
|
||||||
|
msgid "seller_mobile_value"
|
||||||
|
msgstr "Seller mobile"
|
||||||
|
|
||||||
|
msgid "buyer_company_name_value"
|
||||||
|
msgstr "Buyer company"
|
||||||
|
|
||||||
|
msgid "buyer_name_value"
|
||||||
|
msgstr "Buyer name"
|
||||||
|
|
||||||
|
msgid "buyer_tax_code_value"
|
||||||
|
msgstr "Buyer tax code"
|
||||||
|
|
||||||
|
msgid "buyer_address_value"
|
||||||
|
msgstr "Buyer address"
|
||||||
|
|
||||||
|
msgid "buyer_mobile_value"
|
||||||
|
msgstr "Buyer mobile"
|
||||||
|
|
||||||
|
msgid "VAT_amount_value"
|
||||||
|
msgstr "VAT amount"
|
||||||
|
|
||||||
|
msgid "total_value"
|
||||||
|
msgstr "Total value"
|
||||||
|
|
||||||
|
msgid "total_in_words_value"
|
||||||
|
msgstr "Total in words"
|
||||||
|
|
||||||
|
# LabelMap InvoiceAP
|
||||||
|
|
||||||
|
msgid "Date_value"
|
||||||
|
msgstr "Date"
|
||||||
|
|
||||||
|
msgid "Store_name_value"
|
||||||
|
msgstr "Seller company name"
|
||||||
|
|
||||||
|
msgid "Total_value"
|
||||||
|
msgstr "Total value"
|
||||||
|
|
||||||
|
msgid "Receipt Number"
|
||||||
|
msgstr "Receipt Number"
|
||||||
|
|
||||||
|
|
||||||
|
# LabelMap IdCard & Driver
|
||||||
|
msgid "Number"
|
||||||
|
msgstr "Number"
|
||||||
|
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
msgid "Birthday"
|
||||||
|
msgstr "Date of birth"
|
||||||
|
|
||||||
|
msgid "Home Town"
|
||||||
|
msgstr "Place of origin"
|
||||||
|
|
||||||
|
msgid "Address"
|
||||||
|
msgstr "Address"
|
||||||
|
|
||||||
|
msgid "Sex"
|
||||||
|
msgstr "Sex"
|
||||||
|
|
||||||
|
msgid "Nationality"
|
||||||
|
msgstr "Nationality"
|
||||||
|
|
||||||
|
msgid "Expiry Date"
|
||||||
|
msgstr "Date of Expiry"
|
||||||
|
|
||||||
|
msgid "Nation"
|
||||||
|
msgstr "Nation"
|
||||||
|
|
||||||
|
msgid "Religion"
|
||||||
|
msgstr "Religion"
|
||||||
|
|
||||||
|
msgid "Date Range"
|
||||||
|
msgstr "Issued At"
|
||||||
|
|
||||||
|
msgid "Issued By"
|
||||||
|
msgstr "Issued By"
|
||||||
|
|
||||||
|
msgid "others"
|
||||||
|
msgstr "Others"
|
||||||
|
|
||||||
|
msgid "Rank"
|
||||||
|
msgstr "Rank"
|
||||||
|
|
||||||
|
msgid "table"
|
||||||
|
msgstr "Table"
|
BIN
cope2n-api/locale/vi_VN/LC_MESSAGES/django.mo
Executable file
BIN
cope2n-api/locale/vi_VN/LC_MESSAGES/django.mo
Executable file
Binary file not shown.
207
cope2n-api/locale/vi_VN/LC_MESSAGES/django.po
Executable file
207
cope2n-api/locale/vi_VN/LC_MESSAGES/django.po
Executable file
@ -0,0 +1,207 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2023-04-17 11:06+0700\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: .\fwd_api\api\ctel_user_view.py:232
|
||||||
|
msgid "test"
|
||||||
|
msgstr "Vietnam"
|
||||||
|
|
||||||
|
# error base message exception.py
|
||||||
|
|
||||||
|
msgid "You have signed up for a trial plan before. Please contact sales for more information"
|
||||||
|
msgstr "Bạn đã đăng ký dùng Gói trải nghiệm trước đó. Vui lòng liên hệ CSKH để có thêm thông tin"
|
||||||
|
|
||||||
|
msgid "Action refuse. Dont have permission to perform this action"
|
||||||
|
msgstr "Thao tác bị từ chôi. Bạn không có quyền thực hiện hành động này"
|
||||||
|
|
||||||
|
msgid "Service temporarily unavailable, try again later."
|
||||||
|
msgstr "Dịch vụ hiện tại không khả dụng, vui lòng thử lại sau"
|
||||||
|
|
||||||
|
msgid "Locked Entity"
|
||||||
|
msgstr "Tài nguyên đã bị khoá"
|
||||||
|
|
||||||
|
msgid "{} has been locked or inactive"
|
||||||
|
msgstr "{} đã bị khoá hoặc vô hiệu hoá"
|
||||||
|
|
||||||
|
msgid "Data Invalid"
|
||||||
|
msgstr "Dữ liệu không hợp lệ"
|
||||||
|
|
||||||
|
msgid "{} invalid"
|
||||||
|
msgstr "Thông tin về {} không hợp lệ"
|
||||||
|
|
||||||
|
msgid "Field required"
|
||||||
|
msgstr "Trường thông tin không được bỏ trống"
|
||||||
|
|
||||||
|
msgid "{} param is required"
|
||||||
|
msgstr "Trường thông tin {} không được bỏ trống"
|
||||||
|
|
||||||
|
msgid "Data duplicate"
|
||||||
|
msgstr "Dữ liệu bị trùng lặp"
|
||||||
|
|
||||||
|
msgid "{} duplicate"
|
||||||
|
msgstr "{} bị trùng lặp"
|
||||||
|
|
||||||
|
msgid "Data not found"
|
||||||
|
msgstr "Không tìm thấy dữ liệu"
|
||||||
|
|
||||||
|
msgid "{} not found"
|
||||||
|
msgstr "Không tìm thấy {}"
|
||||||
|
|
||||||
|
msgid "File invalid type"
|
||||||
|
msgstr "Định dạng tập tin không hợp lệ"
|
||||||
|
|
||||||
|
msgid "File must have type {}"
|
||||||
|
msgstr "Tập tin cần có định dạng {}"
|
||||||
|
|
||||||
|
msgid "Token expired or invalid"
|
||||||
|
msgstr "Mã truy cập không hợp lệ hoặc đã hết hạn"
|
||||||
|
|
||||||
|
msgid "Data reach limit"
|
||||||
|
msgstr "Dữ liệu đã đạt mức giới hạn"
|
||||||
|
|
||||||
|
msgid "Limit reached. {} limit at {} {}"
|
||||||
|
msgstr "{} được giới hạn ở {} {}"
|
||||||
|
|
||||||
|
# Message variables
|
||||||
|
|
||||||
|
msgid "template_id"
|
||||||
|
msgstr "Mã tài liệu mẫu"
|
||||||
|
|
||||||
|
msgid "requestId"
|
||||||
|
msgstr "Mã yêu cầu"
|
||||||
|
|
||||||
|
msgid "plan"
|
||||||
|
msgstr "Gói thuê bao"
|
||||||
|
|
||||||
|
msgid "Number of request"
|
||||||
|
msgstr "Số lần yêu cầu"
|
||||||
|
|
||||||
|
msgid "Number of template"
|
||||||
|
msgstr "Số mẫu tài liệu"
|
||||||
|
|
||||||
|
msgid "times"
|
||||||
|
msgstr "lượt"
|
||||||
|
|
||||||
|
# LabelMap Invoice
|
||||||
|
msgid "no_value"
|
||||||
|
msgstr "Số hóa đơn"
|
||||||
|
|
||||||
|
msgid "form_value"
|
||||||
|
msgstr "Số mẫu"
|
||||||
|
|
||||||
|
msgid "serial_value"
|
||||||
|
msgstr "Số sê-ri"
|
||||||
|
|
||||||
|
msgid "date"
|
||||||
|
msgstr "Ngày"
|
||||||
|
|
||||||
|
msgid "seller_company_name_value"
|
||||||
|
msgstr "Đơn vị bán hàng"
|
||||||
|
|
||||||
|
msgid "seller_tax_code_value"
|
||||||
|
msgstr "Mã số thuế bên bán"
|
||||||
|
|
||||||
|
msgid "seller_address_value"
|
||||||
|
msgstr "Địa chỉ bên bán"
|
||||||
|
|
||||||
|
msgid "seller_mobile_value"
|
||||||
|
msgstr "Số điện thoại bên bán"
|
||||||
|
|
||||||
|
msgid "buyer_company_name_value"
|
||||||
|
msgstr "Công ty bên mua"
|
||||||
|
|
||||||
|
msgid "buyer_name_value"
|
||||||
|
msgstr "Tên người mua"
|
||||||
|
|
||||||
|
msgid "buyer_tax_code_value"
|
||||||
|
msgstr "Mã số thuế bên mua"
|
||||||
|
|
||||||
|
msgid "buyer_address_value"
|
||||||
|
msgstr "Địa chỉ bên mua"
|
||||||
|
|
||||||
|
msgid "buyer_mobile_value"
|
||||||
|
msgstr "Số điện thoại bên mua"
|
||||||
|
|
||||||
|
msgid "VAT_amount_value"
|
||||||
|
msgstr "Tổng tiền VAT"
|
||||||
|
|
||||||
|
msgid "total_value"
|
||||||
|
msgstr "Tổng tiền"
|
||||||
|
|
||||||
|
msgid "total_in_words_value"
|
||||||
|
msgstr "Tổng tiền bằng chữ"
|
||||||
|
|
||||||
|
# LabelMap InvoiceAP
|
||||||
|
|
||||||
|
msgid "Date_value"
|
||||||
|
msgstr "Ngày"
|
||||||
|
|
||||||
|
msgid "Store_name_value"
|
||||||
|
msgstr "Đơn vị bán hàng"
|
||||||
|
|
||||||
|
msgid "Total_value"
|
||||||
|
msgstr "Tổng tiền"
|
||||||
|
|
||||||
|
msgid "Receipt Number"
|
||||||
|
msgstr "Biên lai"
|
||||||
|
|
||||||
|
# LabelMap IdCard & Driver
|
||||||
|
msgid "Number"
|
||||||
|
msgstr "Số"
|
||||||
|
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Tên"
|
||||||
|
|
||||||
|
msgid "Birthday"
|
||||||
|
msgstr "Ngày sinh"
|
||||||
|
|
||||||
|
msgid "Home Town"
|
||||||
|
msgstr "Quê quán"
|
||||||
|
|
||||||
|
msgid "Address"
|
||||||
|
msgstr "Địa chỉ"
|
||||||
|
|
||||||
|
msgid "Sex"
|
||||||
|
msgstr "Giới tính"
|
||||||
|
|
||||||
|
msgid "Nationality"
|
||||||
|
msgstr "Quốc tịch"
|
||||||
|
|
||||||
|
msgid "Expiry Date"
|
||||||
|
msgstr "Ngày hết hạn"
|
||||||
|
|
||||||
|
msgid "Nation"
|
||||||
|
msgstr "Dân tộc"
|
||||||
|
|
||||||
|
msgid "Religion"
|
||||||
|
msgstr "Tôn giáo"
|
||||||
|
|
||||||
|
msgid "Date Range"
|
||||||
|
msgstr "Ngày cấp"
|
||||||
|
|
||||||
|
msgid "Issued By"
|
||||||
|
msgstr "Nơi cấp"
|
||||||
|
|
||||||
|
msgid "others"
|
||||||
|
msgstr "Khác"
|
||||||
|
|
||||||
|
msgid "Rank"
|
||||||
|
msgstr "Hạng"
|
||||||
|
|
||||||
|
msgid "table"
|
||||||
|
msgstr "Bảng"
|
22
cope2n-api/manage.py
Executable file
22
cope2n-api/manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fwd.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
51
cope2n-api/requirements.txt
Executable file
51
cope2n-api/requirements.txt
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
asgiref==3.5.2
|
||||||
|
attrs==22.1.0
|
||||||
|
certifi==2022.9.24
|
||||||
|
cffi==1.15.1
|
||||||
|
charset-normalizer==2.1.1
|
||||||
|
click==8.1.3
|
||||||
|
colorama==0.4.6
|
||||||
|
coreapi==2.3.3
|
||||||
|
coreschema==0.0.4
|
||||||
|
cryptography==39.0.1
|
||||||
|
Django==4.1.3
|
||||||
|
django-appconf==1.0.5
|
||||||
|
django-cors-headers==3.13.0
|
||||||
|
django-environ==0.9.0
|
||||||
|
djangorestframework==3.14.0
|
||||||
|
drf-spectacular==0.24.2
|
||||||
|
drf-spectacular-sidecar==2022.12.1
|
||||||
|
h11==0.14.0
|
||||||
|
idna==3.4
|
||||||
|
inflection==0.5.1
|
||||||
|
itypes==1.2.0
|
||||||
|
Jinja2==3.1.2
|
||||||
|
jsonschema==4.17.1
|
||||||
|
MarkupSafe==2.1.1
|
||||||
|
packaging==21.3
|
||||||
|
pdf2image==1.16.0
|
||||||
|
Pillow==9.3.0
|
||||||
|
psycopg2==2.9.5
|
||||||
|
psycopg2-binary==2.9.5
|
||||||
|
pycparser==2.21
|
||||||
|
pyparsing==3.0.9
|
||||||
|
PyPDF2==2.11.2
|
||||||
|
pyrsistent==0.19.2
|
||||||
|
pytz==2022.6
|
||||||
|
PyYAML==6.0
|
||||||
|
requests==2.28.1
|
||||||
|
ruamel.yaml==0.17.21
|
||||||
|
ruamel.yaml.clib==0.2.7
|
||||||
|
sqlparse==0.4.3
|
||||||
|
tzdata==2022.6
|
||||||
|
uritemplate==4.1.1
|
||||||
|
urllib3==1.26.13
|
||||||
|
uvicorn==0.20.0
|
||||||
|
|
||||||
|
celery~=5.2.7
|
||||||
|
kombu~=5.2.4
|
||||||
|
PyJWT~=2.6.0
|
||||||
|
whitenoise==6.4.0
|
||||||
|
PyMuPDF==1.21.1
|
||||||
|
djangorestframework-xml==2.0.0
|
||||||
|
boto3==1.29.7
|
30
cope2n-api/scripts/create_user.py
Executable file
30
cope2n-api/scripts/create_user.py
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
import os, sys
|
||||||
|
cur_dir = os.path.dirname(__file__)
|
||||||
|
sys.path.append(os.path.dirname(cur_dir))
|
||||||
|
from fwd_api.models.UserProfile import UserProfile
|
||||||
|
from fwd_api.models.SubscriptionRequest import SubscriptionRequest
|
||||||
|
from fwd_api.models.Subscription import Subscription
|
||||||
|
from fwd_api.models.PricingPlan import PricingPlan
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fwd.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
def add_user(user_name, user_id, _sync_id, sub_id, price_id, page_limit=100000000, day_count=1000000000):
|
||||||
|
print(f"[INFO]: Creating user {user_name} with id: {user_id}")
|
||||||
|
user = UserProfile(id=user_id, full_name=user_name, sync_id=_sync_id, current_total_pages=0, limit_total_pages=page_limit, status=1)
|
||||||
|
pricing = PricingPlan(id=price_id, token_limitations=page_limit, duration=day_count)
|
||||||
|
sub = Subscription(id=sub_id, current_token=0, limit_token=page_limit, pricing_plan=pricing, user=user, expired_at="2099-01-01 01:01", status=1)
|
||||||
|
pricing.save()
|
||||||
|
user.save()
|
||||||
|
sub.save()
|
||||||
|
print("[INFO]: User added")
|
||||||
|
|
||||||
|
def main(user_name, user_id, _sync_id, sub_id, price_id, page_limit=100000000, day_count=1000000000):
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fwd.settings.local")
|
||||||
|
django.setup()
|
||||||
|
add_user(user_name, user_id, _sync_id, sub_id, price_id, page_limit, day_count)
|
||||||
|
|
||||||
|
# if __name__=="__main__":
|
||||||
|
main("TannedCung", user_id=1, price_id=36, sub_id=33, _sync_id="xhuyen")
|
275
cope2n-api/static/admin/css/autocomplete.css
Normal file
275
cope2n-api/static/admin/css/autocomplete.css
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
select.admin-autocomplete {
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single,
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||||
|
color: var(--body-fg);
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: #888 transparent transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 4px 0 4px;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||||
|
left: 1px;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: transparent transparent #888 transparent;
|
||||||
|
border-width: 0 4px 5px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
box-sizing: border-box;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 10px 5px 5px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
margin-top: 5px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 5px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
float: left;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||||
|
border: solid var(--body-quiet-color) 1px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-search--dropdown {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--body-fg);
|
||||||
|
background: var(--body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||||
|
background-color: var(--selected-bg);
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -1em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -2em;
|
||||||
|
padding-left: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -3em;
|
||||||
|
padding-left: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -4em;
|
||||||
|
padding-left: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -5em;
|
||||||
|
padding-left: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--primary-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__group {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
1089
cope2n-api/static/admin/css/base.css
Normal file
1089
cope2n-api/static/admin/css/base.css
Normal file
File diff suppressed because it is too large
Load Diff
325
cope2n-api/static/admin/css/changelists.css
Normal file
325
cope2n-api/static/admin/css/changelists.css
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
/* CHANGELISTS */
|
||||||
|
|
||||||
|
#changelist {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .changelist-form-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .hiddenfields { display:none; }
|
||||||
|
|
||||||
|
.change-list .filtered table {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered .results, .change-list .filtered .paginator,
|
||||||
|
.filtered #toolbar, .filtered div.xfull {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered table tbody th {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-form .results {
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .toplinks {
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .paginator {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
background: var(--body-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CHANGELIST TABLES */
|
||||||
|
|
||||||
|
#changelist table thead th {
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table thead th.action-checkbox-column {
|
||||||
|
width: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table tbody td.action-checkbox {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table tfoot {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TOOLBAR */
|
||||||
|
|
||||||
|
#toolbar {
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-top: 1px solid var(--hairline-color);
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar form input {
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 5px;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar #searchbar {
|
||||||
|
height: 19px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2px 5px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar #searchbar:focus {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar form input[type="submit"] {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: var(--body-bg);
|
||||||
|
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar form input[type="submit"]:focus,
|
||||||
|
#toolbar form input[type="submit"]:hover {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-search img {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-search .help {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FILTER COLUMN */
|
||||||
|
|
||||||
|
#changelist-filter {
|
||||||
|
flex: 0 0 240px;
|
||||||
|
order: 1;
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
border-left: none;
|
||||||
|
margin: 0 0 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter h2 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 5px 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter h3,
|
||||||
|
#changelist-filter details summary {
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details summary > * {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details > summary {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details > summary::before {
|
||||||
|
content: '→';
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details[open] > summary::before {
|
||||||
|
content: '↓';
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter ul {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter ul:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter a {
|
||||||
|
display: block;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected {
|
||||||
|
border-left: 5px solid var(--hairline-color);
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-left: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected a {
|
||||||
|
color: var(--link-selected-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter a:focus, #changelist-filter a:hover,
|
||||||
|
#changelist-filter li.selected a:focus,
|
||||||
|
#changelist-filter li.selected a:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter #changelist-filter-clear a {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DATE DRILLDOWN */
|
||||||
|
|
||||||
|
.change-list ul.toplinks {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list ul.toplinks li {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
list-style-type: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list ul.toplinks .date-back a {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list ul.toplinks .date-back a:focus,
|
||||||
|
.change-list ul.toplinks .date-back a:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACTIONS */
|
||||||
|
|
||||||
|
.filtered .actions {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table input {
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table tbody tr.selected {
|
||||||
|
background-color: var(--selected-row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions {
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions span.all,
|
||||||
|
#changelist .actions span.action-counter,
|
||||||
|
#changelist .actions span.clear,
|
||||||
|
#changelist .actions span.question {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions select {
|
||||||
|
vertical-align: top;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions select:focus {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions label {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions .button {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
33
cope2n-api/static/admin/css/dark_mode.css
Normal file
33
cope2n-api/static/admin/css/dark_mode.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--primary: #264b5d;
|
||||||
|
--primary-fg: #f7f7f7;
|
||||||
|
|
||||||
|
--body-fg: #eeeeee;
|
||||||
|
--body-bg: #121212;
|
||||||
|
--body-quiet-color: #e0e0e0;
|
||||||
|
--body-loud-color: #ffffff;
|
||||||
|
|
||||||
|
--breadcrumbs-link-fg: #e0e0e0;
|
||||||
|
--breadcrumbs-bg: var(--primary);
|
||||||
|
|
||||||
|
--link-fg: #81d4fa;
|
||||||
|
--link-hover-color: #4ac1f7;
|
||||||
|
--link-selected-fg: #6f94c6;
|
||||||
|
|
||||||
|
--hairline-color: #272727;
|
||||||
|
--border-color: #353535;
|
||||||
|
|
||||||
|
--error-fg: #e35f5f;
|
||||||
|
--message-success-bg: #006b1b;
|
||||||
|
--message-warning-bg: #583305;
|
||||||
|
--message-error-bg: #570808;
|
||||||
|
|
||||||
|
--darkened-bg: #212121;
|
||||||
|
--selected-bg: #1b1b1b;
|
||||||
|
--selected-row: #00363a;
|
||||||
|
|
||||||
|
--close-button-bg: #333333;
|
||||||
|
--close-button-hover-bg: #666666;
|
||||||
|
}
|
||||||
|
}
|
26
cope2n-api/static/admin/css/dashboard.css
Normal file
26
cope2n-api/static/admin/css/dashboard.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/* DASHBOARD */
|
||||||
|
|
||||||
|
.dashboard .module table th {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .module table td {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .module table td a {
|
||||||
|
display: block;
|
||||||
|
padding-right: .6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RECENT ACTIONS MODULE */
|
||||||
|
|
||||||
|
.module ul.actionlist {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.actionlist li {
|
||||||
|
list-style-type: none;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
20
cope2n-api/static/admin/css/fonts.css
Normal file
20
cope2n-api/static/admin/css/fonts.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto-Bold-webfont.woff');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto-Regular-webfont.woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto-Light-webfont.woff');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
528
cope2n-api/static/admin/css/forms.css
Normal file
528
cope2n-api/static/admin/css/forms.css
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
@import url('widgets.css');
|
||||||
|
|
||||||
|
/* FORM ROWS */
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row img, .form-row input {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label input[type="checkbox"] {
|
||||||
|
margin-top: 0;
|
||||||
|
vertical-align: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-row p {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FORM LABELS */
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required label, label.required {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RADIO BUTTONS */
|
||||||
|
|
||||||
|
form div.radiolist div {
|
||||||
|
padding-right: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form div.radiolist.inline div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
form div.radiolist label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
form div.radiolist input[type="radio"] {
|
||||||
|
margin: -2px 4px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form ul.inline {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form ul.inline li {
|
||||||
|
float: left;
|
||||||
|
padding-right: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ALIGNED FIELDSETS */
|
||||||
|
|
||||||
|
.aligned label {
|
||||||
|
display: block;
|
||||||
|
padding: 4px 10px 0 0;
|
||||||
|
float: left;
|
||||||
|
width: 160px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned label:not(.vCheckboxLabel):after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned label + p, .aligned label + div.help, .aligned label + div.readonly {
|
||||||
|
padding: 6px 0;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 170px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned ul label {
|
||||||
|
display: inline;
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned .form-row input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned ul {
|
||||||
|
margin-left: 160px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned div.radiolist {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned p.help,
|
||||||
|
form .aligned div.help {
|
||||||
|
clear: left;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 160px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned label + p.help,
|
||||||
|
form .aligned label + div.help {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned p.help:last-child,
|
||||||
|
form .aligned div.help:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned input + p.help,
|
||||||
|
form .aligned textarea + p.help,
|
||||||
|
form .aligned select + p.help,
|
||||||
|
form .aligned input + div.help,
|
||||||
|
form .aligned textarea + div.help,
|
||||||
|
form .aligned select + div.help {
|
||||||
|
margin-left: 160px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned ul li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned table p {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned .vCheckboxLabel {
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: -3px;
|
||||||
|
padding: 0 0 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned .vCheckboxLabel + p.help,
|
||||||
|
.aligned .vCheckboxLabel + div.help {
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
|
||||||
|
width: 610px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row p.help,
|
||||||
|
.checkbox-row div.help {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset .fieldBox {
|
||||||
|
float: left;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WIDE FIELDSETS */
|
||||||
|
|
||||||
|
.wide label {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .wide p,
|
||||||
|
form .wide input + p.help,
|
||||||
|
form .wide input + div.help {
|
||||||
|
margin-left: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .wide p.help,
|
||||||
|
form .wide div.help {
|
||||||
|
padding-left: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form div.help ul {
|
||||||
|
padding-left: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
|
||||||
|
width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COLLAPSED FIELDSETS */
|
||||||
|
|
||||||
|
fieldset.collapsed * {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset.collapsed h2, fieldset.collapsed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset.collapsed {
|
||||||
|
border: 1px solid var(--hairline-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset.collapsed h2 {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset .collapse-toggle {
|
||||||
|
color: var(--header-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset.collapsed .collapse-toggle {
|
||||||
|
background: transparent;
|
||||||
|
display: inline;
|
||||||
|
color: var(--link-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MONOSPACE TEXTAREAS */
|
||||||
|
|
||||||
|
fieldset.monospace textarea {
|
||||||
|
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SUBMIT ROW */
|
||||||
|
|
||||||
|
.submit-row {
|
||||||
|
padding: 12px 14px 7px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
border: 1px solid var(--hairline-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.popup .submit-row {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row input {
|
||||||
|
height: 35px;
|
||||||
|
line-height: 15px;
|
||||||
|
margin: 0 0 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row input.default {
|
||||||
|
margin: 0 0 5px 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row p {
|
||||||
|
margin: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row p.deletelink-box {
|
||||||
|
float: left;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row a.deletelink {
|
||||||
|
display: block;
|
||||||
|
background: var(--delete-button-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
height: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: var(--button-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row a.closelink {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--close-button-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
height: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
margin: 0 0 0 5px;
|
||||||
|
color: var(--button-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row a.deletelink:focus,
|
||||||
|
.submit-row a.deletelink:hover,
|
||||||
|
.submit-row a.deletelink:active {
|
||||||
|
background: var(--delete-button-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row a.closelink:focus,
|
||||||
|
.submit-row a.closelink:hover,
|
||||||
|
.submit-row a.closelink:active {
|
||||||
|
background: var(--close-button-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CUSTOM FORM FIELDS */
|
||||||
|
|
||||||
|
.vSelectMultipleField {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vCheckboxField {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vDateField, .vTimeField {
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vDateField {
|
||||||
|
min-width: 6.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vTimeField {
|
||||||
|
min-width: 4.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vURLField {
|
||||||
|
width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vLargeTextField, .vXMLLargeTextField {
|
||||||
|
width: 48em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpages-flatpage #id_content {
|
||||||
|
height: 40.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module table .vPositiveSmallIntegerField {
|
||||||
|
width: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vIntegerField {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vBigIntegerField {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vForeignKeyRawIdAdminField {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vTextField, .vUUIDField {
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* INLINES */
|
||||||
|
|
||||||
|
.inline-group {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group thead th {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .aligned label {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
border-top: 1px solid var(--hairline-color);
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related h3 span.delete {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related h3 span.delete label {
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related fieldset {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--body-bg);
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related fieldset.module h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 5px 3px 5px;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #bcd;
|
||||||
|
color: var(--body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .tabular fieldset.module {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related.tabular fieldset.module table {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-related fieldset {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .tabular tr.has_original td {
|
||||||
|
padding-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .tabular tr td.original {
|
||||||
|
padding: 2px 0 0 0;
|
||||||
|
width: 0;
|
||||||
|
_position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .tabular th.original {
|
||||||
|
width: 0px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .tabular td.original p {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 1.1em;
|
||||||
|
padding: 2px 9px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.5625rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
_width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group ul.tools {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group ul.tools li {
|
||||||
|
display: inline;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group div.add-row,
|
||||||
|
.inline-group .tabular tr.add-row td {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group .tabular tr.add-row td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-group ul.tools a.add,
|
||||||
|
.inline-group div.add-row a,
|
||||||
|
.inline-group .tabular tr.add-row td a {
|
||||||
|
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
|
||||||
|
padding-left: 16px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RELATED FIELD ADD ONE / LOOKUP */
|
||||||
|
|
||||||
|
.related-lookup {
|
||||||
|
margin-left: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-lookup {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: url(../img/search.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
form .related-widget-wrapper ul {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearable-file-input input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
61
cope2n-api/static/admin/css/login.css
Normal file
61
cope2n-api/static/admin/css/login.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/* LOGIN FORM */
|
||||||
|
|
||||||
|
.login {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #header {
|
||||||
|
height: auto;
|
||||||
|
padding: 15px 16px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #header h1 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #header h1 a {
|
||||||
|
color: var(--header-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #content {
|
||||||
|
padding: 20px 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #container {
|
||||||
|
background: var(--body-bg);
|
||||||
|
border: 1px solid var(--hairline-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 28em;
|
||||||
|
min-width: 300px;
|
||||||
|
margin: 100px auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .form-row {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .form-row label {
|
||||||
|
display: block;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .form-row #id_username, .login .form-row #id_password {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .submit-row {
|
||||||
|
padding: 1em 0 0 0;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .password-reset-link {
|
||||||
|
text-align: center;
|
||||||
|
}
|
139
cope2n-api/static/admin/css/nav_sidebar.css
Normal file
139
cope2n-api/static/admin/css/nav_sidebar.css
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
.sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-nav-sidebar {
|
||||||
|
z-index: 20;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 23px;
|
||||||
|
width: 23px;
|
||||||
|
border: 0;
|
||||||
|
border-right: 1px solid var(--hairline-color);
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--link-fg);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .toggle-nav-sidebar {
|
||||||
|
border-left: 1px solid var(--hairline-color);
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-nav-sidebar:hover,
|
||||||
|
.toggle-nav-sidebar:focus {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar {
|
||||||
|
z-index: 15;
|
||||||
|
flex: 0 0 275px;
|
||||||
|
left: -276px;
|
||||||
|
margin-left: -276px;
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
border-right: 1px solid var(--hairline-color);
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #nav-sidebar {
|
||||||
|
border-left: 1px solid var(--hairline-color);
|
||||||
|
border-right: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
right: -276px;
|
||||||
|
margin-right: -276px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-nav-sidebar::before {
|
||||||
|
content: '\00BB';
|
||||||
|
}
|
||||||
|
|
||||||
|
.main.shifted .toggle-nav-sidebar::before {
|
||||||
|
content: '\00AB';
|
||||||
|
}
|
||||||
|
|
||||||
|
.main.shifted > #nav-sidebar {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .main.shifted > #nav-sidebar {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .module th {
|
||||||
|
width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .module th,
|
||||||
|
#nav-sidebar .module caption {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .module td {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #nav-sidebar .module th,
|
||||||
|
[dir="rtl"] #nav-sidebar .module caption {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .current-app .section:link,
|
||||||
|
#nav-sidebar .current-app .section:visited {
|
||||||
|
color: var(--header-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .current-model {
|
||||||
|
background: var(--selected-row);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > #nav-sidebar + .content {
|
||||||
|
max-width: calc(100% - 23px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main.shifted > #nav-sidebar + .content {
|
||||||
|
max-width: calc(100% - 299px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#nav-sidebar, #toggle-nav-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > #nav-sidebar + .content,
|
||||||
|
.main.shifted > #nav-sidebar + .content {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-filter {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 2px 5px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-filter:focus {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-filter.no-results {
|
||||||
|
background: var(--message-error-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
1015
cope2n-api/static/admin/css/responsive.css
Normal file
1015
cope2n-api/static/admin/css/responsive.css
Normal file
File diff suppressed because it is too large
Load Diff
80
cope2n-api/static/admin/css/responsive_rtl.css
Normal file
80
cope2n-api/static/admin/css/responsive_rtl.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* TABLETS */
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
[dir="rtl"] .colMS {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #user-tools {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #changelist .actions label {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #changelist .actions select {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .change-list .filtered .results,
|
||||||
|
[dir="rtl"] .change-list .filtered .paginator,
|
||||||
|
[dir="rtl"] .filtered #toolbar,
|
||||||
|
[dir="rtl"] .filtered div.xfull,
|
||||||
|
[dir="rtl"] .filtered .actions,
|
||||||
|
[dir="rtl"] #changelist-filter {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .inline-group ul.tools a.add,
|
||||||
|
[dir="rtl"] .inline-group div.add-row a,
|
||||||
|
[dir="rtl"] .inline-group .tabular tr.add-row td a {
|
||||||
|
padding: 8px 26px 8px 10px;
|
||||||
|
background-position: calc(100% - 8px) 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .related-widget-wrapper-link + .selector {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .selector .selector-filter label {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .object-tools li {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .object-tools li + li {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .dashboard .module table td a {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MOBILE */
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
[dir="rtl"] .aligned .related-lookup,
|
||||||
|
[dir="rtl"] .aligned .datetimeshortcuts {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .aligned ul {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] #changelist-filter {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
239
cope2n-api/static/admin/css/rtl.css
Normal file
239
cope2n-api/static/admin/css/rtl.css
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/* GLOBAL */
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module h2, .module caption {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module ul, .module ol {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewlink, .addlink, .changelink {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
background-position: 100% 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deletelink {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
background-position: 100% 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-tools {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th:first-child,
|
||||||
|
tfoot td:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LAYOUT */
|
||||||
|
|
||||||
|
#user-tools {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.breadcrumbs {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-main {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-related {
|
||||||
|
float: left;
|
||||||
|
margin-left: -300px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colMS {
|
||||||
|
margin-left: 300px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SORTABLE TABLES */
|
||||||
|
|
||||||
|
table thead th.sorted .sortoptions {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th.sorted .text {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dashboard styles */
|
||||||
|
|
||||||
|
.dashboard .module table td a {
|
||||||
|
padding-left: .6em;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* changelists styles */
|
||||||
|
|
||||||
|
.change-list .filtered table {
|
||||||
|
border-left: none;
|
||||||
|
border-right: 0px none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected {
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-left: 0;
|
||||||
|
border-right: 5px solid var(--hairline-color);
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-right: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
|
||||||
|
border-right: none;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FORMS */
|
||||||
|
|
||||||
|
.aligned label {
|
||||||
|
padding: 0 0 3px 1em;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row {
|
||||||
|
text-align: left
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row p.deletelink-box {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row input.default {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vDateField, .vTimeField {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned .form-row input {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned p.help, form .aligned div.help {
|
||||||
|
clear: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned ul {
|
||||||
|
margin-right: 163px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form ul.inline li {
|
||||||
|
float: right;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit].default, .submit-row input.default {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset .fieldBox {
|
||||||
|
float: right;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorlist li {
|
||||||
|
background-position: 100% 12px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errornote {
|
||||||
|
background-position: 100% 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WIDGETS */
|
||||||
|
|
||||||
|
.calendarnav-previous {
|
||||||
|
top: 0;
|
||||||
|
left: auto;
|
||||||
|
right: 10px;
|
||||||
|
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarbox .calendarnav-previous:focus,
|
||||||
|
.calendarbox .calendarnav-previous:hover {
|
||||||
|
background-position: 0 -45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarnav-next {
|
||||||
|
top: 0;
|
||||||
|
right: auto;
|
||||||
|
left: 10px;
|
||||||
|
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarbox .calendarnav-next:focus,
|
||||||
|
.calendarbox .calendarnav-next:hover {
|
||||||
|
background-position: 0 -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar caption, .calendarbox h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-filter {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-deletelink {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-row p.datetime {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-widget-wrapper {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MISC */
|
||||||
|
|
||||||
|
.inline-related h2, .inline-group h2 {
|
||||||
|
text-align: right
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related h3 span.delete {
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-left: inherit;
|
||||||
|
left: 10px;
|
||||||
|
right: inherit;
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-related h3 span.delete label {
|
||||||
|
margin-left: inherit;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
21
cope2n-api/static/admin/css/vendor/select2/LICENSE-SELECT2.md
vendored
Normal file
21
cope2n-api/static/admin/css/vendor/select2/LICENSE-SELECT2.md
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
481
cope2n-api/static/admin/css/vendor/select2/select2.css
vendored
Normal file
481
cope2n-api/static/admin/css/vendor/select2/select2.css
vendored
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
.select2-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle; }
|
||||||
|
.select2-container .select2-selection--single {
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
height: 28px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none; }
|
||||||
|
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||||
|
display: block;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap; }
|
||||||
|
.select2-container .select2-selection--single .select2-selection__clear {
|
||||||
|
position: relative; }
|
||||||
|
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-left: 20px; }
|
||||||
|
.select2-container .select2-selection--multiple {
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
min-height: 32px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none; }
|
||||||
|
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-left: 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap; }
|
||||||
|
.select2-container .select2-search--inline {
|
||||||
|
float: left; }
|
||||||
|
.select2-container .select2-search--inline .select2-search__field {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
font-size: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0; }
|
||||||
|
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none; }
|
||||||
|
|
||||||
|
.select2-dropdown {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -100000px;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1051; }
|
||||||
|
|
||||||
|
.select2-results {
|
||||||
|
display: block; }
|
||||||
|
|
||||||
|
.select2-results__options {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0; }
|
||||||
|
|
||||||
|
.select2-results__option {
|
||||||
|
padding: 6px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none; }
|
||||||
|
.select2-results__option[aria-selected] {
|
||||||
|
cursor: pointer; }
|
||||||
|
|
||||||
|
.select2-container--open .select2-dropdown {
|
||||||
|
left: 0; }
|
||||||
|
|
||||||
|
.select2-container--open .select2-dropdown--above {
|
||||||
|
border-bottom: none;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0; }
|
||||||
|
|
||||||
|
.select2-container--open .select2-dropdown--below {
|
||||||
|
border-top: none;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0; }
|
||||||
|
|
||||||
|
.select2-search--dropdown {
|
||||||
|
display: block;
|
||||||
|
padding: 4px; }
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
padding: 4px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box; }
|
||||||
|
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none; }
|
||||||
|
.select2-search--dropdown.select2-search--hide {
|
||||||
|
display: none; }
|
||||||
|
|
||||||
|
.select2-close-mask {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 99;
|
||||||
|
background-color: #fff;
|
||||||
|
filter: alpha(opacity=0); }
|
||||||
|
|
||||||
|
.select2-hidden-accessible {
|
||||||
|
border: 0 !important;
|
||||||
|
clip: rect(0 0 0 0) !important;
|
||||||
|
-webkit-clip-path: inset(50%) !important;
|
||||||
|
clip-path: inset(50%) !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
white-space: nowrap !important; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||||
|
color: #444;
|
||||||
|
line-height: 28px; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||||
|
color: #999; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
width: 20px; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: #888 transparent transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 4px 0 4px;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 0; }
|
||||||
|
|
||||||
|
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||||
|
float: left; }
|
||||||
|
|
||||||
|
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||||
|
left: 1px;
|
||||||
|
right: auto; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--disabled .select2-selection--single {
|
||||||
|
background-color: #eee;
|
||||||
|
cursor: default; }
|
||||||
|
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||||
|
display: none; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: transparent transparent #888 transparent;
|
||||||
|
border-width: 0 4px 5px 4px; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: text; }
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
box-sizing: border-box;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 5px;
|
||||||
|
width: 100%; }
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
||||||
|
list-style: none; }
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 1px; }
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
float: left;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0 5px; }
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 2px; }
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
color: #333; }
|
||||||
|
|
||||||
|
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||||
|
float: right; }
|
||||||
|
|
||||||
|
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: auto; }
|
||||||
|
|
||||||
|
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: auto; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border: solid black 1px;
|
||||||
|
outline: 0; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
||||||
|
background-color: #eee;
|
||||||
|
cursor: default; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
||||||
|
display: none; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0; }
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||||
|
border: 1px solid #aaa; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-search--inline .select2-search__field {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
-webkit-appearance: textfield; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results > .select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results__option[role=group] {
|
||||||
|
padding: 0; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results__option[aria-disabled=true] {
|
||||||
|
color: #999; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results__option[aria-selected=true] {
|
||||||
|
background-color: #ddd; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option {
|
||||||
|
padding-left: 1em; }
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
||||||
|
padding-left: 0; }
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -1em;
|
||||||
|
padding-left: 2em; }
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -2em;
|
||||||
|
padding-left: 3em; }
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -3em;
|
||||||
|
padding-left: 4em; }
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -4em;
|
||||||
|
padding-left: 5em; }
|
||||||
|
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -5em;
|
||||||
|
padding-left: 6em; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: #5897fb;
|
||||||
|
color: white; }
|
||||||
|
|
||||||
|
.select2-container--default .select2-results__group {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
padding: 6px; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-selection--single {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: 0;
|
||||||
|
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||||
|
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||||
|
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||||
|
.select2-container--classic .select2-selection--single:focus {
|
||||||
|
border: 1px solid #5897fb; }
|
||||||
|
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
||||||
|
color: #444;
|
||||||
|
line-height: 28px; }
|
||||||
|
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px; }
|
||||||
|
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
||||||
|
color: #999; }
|
||||||
|
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
||||||
|
background-color: #ddd;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid #aaa;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
width: 20px;
|
||||||
|
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||||
|
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||||
|
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
||||||
|
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: #888 transparent transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 4px 0 4px;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 0; }
|
||||||
|
|
||||||
|
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||||
|
float: left; }
|
||||||
|
|
||||||
|
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid #aaa;
|
||||||
|
border-radius: 0;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
left: 1px;
|
||||||
|
right: auto; }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open .select2-selection--single {
|
||||||
|
border: 1px solid #5897fb; }
|
||||||
|
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
||||||
|
background: transparent;
|
||||||
|
border: none; }
|
||||||
|
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: transparent transparent #888 transparent;
|
||||||
|
border-width: 0 4px 5px 4px; }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
||||||
|
border-top: none;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||||
|
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||||
|
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
||||||
|
border-bottom: none;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||||
|
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||||
|
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-selection--multiple {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: text;
|
||||||
|
outline: 0; }
|
||||||
|
.select2-container--classic .select2-selection--multiple:focus {
|
||||||
|
border: 1px solid #5897fb; }
|
||||||
|
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 5px; }
|
||||||
|
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
||||||
|
display: none; }
|
||||||
|
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
float: left;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0 5px; }
|
||||||
|
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 2px; }
|
||||||
|
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
color: #555; }
|
||||||
|
|
||||||
|
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||||
|
float: right;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: auto; }
|
||||||
|
|
||||||
|
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: auto; }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
||||||
|
border: 1px solid #5897fb; }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||||
|
border-top: none;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0; }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||||
|
border-bottom: none;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
outline: 0; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-search--inline .select2-search__field {
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: none; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-dropdown {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid transparent; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-dropdown--above {
|
||||||
|
border-bottom: none; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-dropdown--below {
|
||||||
|
border-top: none; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-results > .select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-results__option[role=group] {
|
||||||
|
padding: 0; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
||||||
|
color: grey; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: #3875d7;
|
||||||
|
color: white; }
|
||||||
|
|
||||||
|
.select2-container--classic .select2-results__group {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
padding: 6px; }
|
||||||
|
|
||||||
|
.select2-container--classic.select2-container--open .select2-dropdown {
|
||||||
|
border-color: #5897fb; }
|
1
cope2n-api/static/admin/css/vendor/select2/select2.min.css
vendored
Normal file
1
cope2n-api/static/admin/css/vendor/select2/select2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
580
cope2n-api/static/admin/css/widgets.css
Normal file
580
cope2n-api/static/admin/css/widgets.css
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
/* SELECTOR (FILTER INTERFACE) */
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
width: 800px;
|
||||||
|
float: left;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector select {
|
||||||
|
width: 380px;
|
||||||
|
height: 17.2em;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-available, .selector-chosen {
|
||||||
|
width: 380px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-chosen select {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-available h2, .selector-chosen h2 {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-chosen h2 {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--header-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-available h2 {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-filter {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-width: 0 1px;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
font-size: 0.625rem;
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-filter label,
|
||||||
|
.inline-group .aligned .selector .selector-filter label {
|
||||||
|
float: left;
|
||||||
|
margin: 7px 0 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-available input {
|
||||||
|
width: 320px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector ul.selector-chooser {
|
||||||
|
align-self: center;
|
||||||
|
width: 22px;
|
||||||
|
background-color: var(--selected-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 0;
|
||||||
|
transform: translateY(-17px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-chooser li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 3px;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector select {
|
||||||
|
padding: 0 10px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-add, .selector-remove {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
text-indent: -3000px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active.selector-add, .active.selector-remove {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active.selector-add:hover, .active.selector-remove:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-add {
|
||||||
|
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active.selector-add:focus, .active.selector-add:hover {
|
||||||
|
background-position: 0 -112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-remove {
|
||||||
|
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||||
|
background-position: 0 -80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.selector-chooseall, a.selector-clearall {
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
text-align: left;
|
||||||
|
margin: 1px auto 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
|
||||||
|
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||||
|
color: var(--link-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active.selector-chooseall, a.active.selector-clearall {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.selector-chooseall {
|
||||||
|
padding: 0 18px 0 0;
|
||||||
|
background: url(../img/selector-icons.svg) right -160px no-repeat;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
||||||
|
background-position: 100% -176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.selector-clearall {
|
||||||
|
padding: 0 0 0 18px;
|
||||||
|
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||||
|
background-position: 0 -144px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STACKED SELECTORS */
|
||||||
|
|
||||||
|
.stacked {
|
||||||
|
float: left;
|
||||||
|
width: 490px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked select {
|
||||||
|
width: 480px;
|
||||||
|
height: 10.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-available, .stacked .selector-chosen {
|
||||||
|
width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-available {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-available input {
|
||||||
|
width: 422px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked ul.selector-chooser {
|
||||||
|
height: 22px;
|
||||||
|
width: 50px;
|
||||||
|
margin: 0 0 10px 40%;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 10px;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-chooser li {
|
||||||
|
float: left;
|
||||||
|
padding: 3px 3px 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-chooseall, .stacked .selector-clearall {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-add {
|
||||||
|
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .active.selector-add {
|
||||||
|
background-position: 0 -32px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
|
||||||
|
background-position: 0 -48px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .selector-remove {
|
||||||
|
background: url(../img/selector-icons.svg) 0 0 no-repeat;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .active.selector-remove {
|
||||||
|
background-position: 0 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
|
||||||
|
background-position: 0 -16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .help-icon {
|
||||||
|
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: -2px 0 0 2px;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-chosen .help-icon {
|
||||||
|
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .search-label-icon {
|
||||||
|
background: url(../img/search.svg) 0 0 no-repeat;
|
||||||
|
display: inline-block;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DATE AND TIME */
|
||||||
|
|
||||||
|
p.datetime {
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime span {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table p.datetime {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetimeshortcuts .clock-icon {
|
||||||
|
background: url(../img/icon-clock.svg) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetimeshortcuts a:focus .clock-icon,
|
||||||
|
.datetimeshortcuts a:hover .clock-icon {
|
||||||
|
background-position: 0 -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetimeshortcuts .date-icon {
|
||||||
|
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetimeshortcuts a:focus .date-icon,
|
||||||
|
.datetimeshortcuts a:hover .date-icon {
|
||||||
|
background-position: 0 -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timezonewarning {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* URL */
|
||||||
|
|
||||||
|
p.url {
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url a {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FILE UPLOADS */
|
||||||
|
|
||||||
|
p.file-upload {
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned p.file-upload {
|
||||||
|
margin-left: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload a {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload .deletelink {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.clearable-file-input label {
|
||||||
|
color: var(--body-fg);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
display: inline;
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CALENDARS & CLOCKS */
|
||||||
|
|
||||||
|
.calendarbox, .clockbox {
|
||||||
|
margin: 5px auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 19em;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: 1px solid var(--hairline-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clockbox {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar table {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar caption, .calendarbox h2 {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
border-top: none;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #333;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar th {
|
||||||
|
padding: 8px 5px;
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid var(--hairline-color);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td.selected a {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--button-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td.nonday {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td.today a {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td a, .timelist a {
|
||||||
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td a:focus, .timelist a:focus,
|
||||||
|
.calendar td a:hover, .timelist a:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td a:active, .timelist a:active {
|
||||||
|
background: var(--header-bg);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarnav {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #ccc;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarnav a:link, #calendarnav a:visited,
|
||||||
|
#calendarnav a:focus, #calendarnav a:hover {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-shortcuts {
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
line-height: 11px;
|
||||||
|
border-top: 1px solid var(--hairline-color);
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
text-indent: -9999px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarnav-previous {
|
||||||
|
left: 10px;
|
||||||
|
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarbox .calendarnav-previous:focus,
|
||||||
|
.calendarbox .calendarnav-previous:hover {
|
||||||
|
background-position: 0 -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarnav-next {
|
||||||
|
right: 10px;
|
||||||
|
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarbox .calendarnav-next:focus,
|
||||||
|
.calendarbox .calendarnav-next:hover {
|
||||||
|
background-position: 0 -45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cancel {
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: #eee;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cancel:focus, .calendar-cancel:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cancel a {
|
||||||
|
color: black;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.timelist, .timelist li {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timelist a {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EDIT INLINE */
|
||||||
|
|
||||||
|
.inline-deletelink {
|
||||||
|
float: right;
|
||||||
|
text-indent: -9999px;
|
||||||
|
background: url(../img/inline-delete.svg) 0 0 no-repeat;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 0px none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-deletelink:focus, .inline-deletelink:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RELATED WIDGET WRAPPER */
|
||||||
|
.related-widget-wrapper {
|
||||||
|
float: left; /* display properly in form rows with multiple fields */
|
||||||
|
overflow: hidden; /* clear floated contents */
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-widget-wrapper-link {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-widget-wrapper-link:link {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-widget-wrapper-link:link:focus,
|
||||||
|
.related-widget-wrapper-link:link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
select + .related-widget-wrapper-link,
|
||||||
|
.related-widget-wrapper-link + .related-widget-wrapper-link {
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
202
cope2n-api/static/admin/fonts/LICENSE.txt
Normal file
202
cope2n-api/static/admin/fonts/LICENSE.txt
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
3
cope2n-api/static/admin/fonts/README.txt
Normal file
3
cope2n-api/static/admin/fonts/README.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Roboto webfont source: https://www.google.com/fonts/specimen/Roboto
|
||||||
|
WOFF files extracted using https://github.com/majodev/google-webfonts-helper
|
||||||
|
Weights used in this project: Light (300), Regular (400), Bold (700)
|
BIN
cope2n-api/static/admin/fonts/Roboto-Bold-webfont.woff
Normal file
BIN
cope2n-api/static/admin/fonts/Roboto-Bold-webfont.woff
Normal file
Binary file not shown.
BIN
cope2n-api/static/admin/fonts/Roboto-Light-webfont.woff
Normal file
BIN
cope2n-api/static/admin/fonts/Roboto-Light-webfont.woff
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user