Add: BE, FE

This commit is contained in:
dx-tan 2023-11-30 18:19:06 +07:00
parent c2eaccbc23
commit 4e83776907
358 changed files with 54067 additions and 0 deletions

14
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
- Default env for all env variables

10
cope2n-api/add_user.py Executable file
View 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, )

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

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

View 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

View 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

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

16
cope2n-api/fwd/asgi.py Executable file
View 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
View 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
View 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
View 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
View File

3
cope2n-api/fwd_api/admin.py Executable file
View File

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

View File

View 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

View File

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

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

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

View 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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

View 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

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

View File

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

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

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

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

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

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

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

View 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

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

View 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

View 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

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

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

View 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

View 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

View File

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

View 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

View 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

View 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

View File

View File

22
cope2n-api/fwd_api/schema.py Executable file
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

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

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

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

View File

@ -0,0 +1,4 @@
def is_number_repl_isdigit(s):
""" Returns True if string is a number. """
return s.replace('.', '', 1).isdigit()

View 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

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

View File

Binary file not shown.

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

Binary file not shown.

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

File diff suppressed because one or more lines are too long

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

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

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

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More