first commit
							
								
								
									
										286
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,286 @@
 | 
			
		|||
.idea
 | 
			
		||||
 | 
			
		||||
# Created by https://www.toptal.com/developers/gitignore/api/django,python,virtualenv
 | 
			
		||||
# Edit at https://www.toptal.com/developers/gitignore?templates=django,python,virtualenv
 | 
			
		||||
 | 
			
		||||
### Django ###
 | 
			
		||||
*.log
 | 
			
		||||
*.pot
 | 
			
		||||
*.pyc
 | 
			
		||||
__pycache__/
 | 
			
		||||
local_settings.py
 | 
			
		||||
*.sqlite3
 | 
			
		||||
db.sqlite3
 | 
			
		||||
db.sqlite3-journal
 | 
			
		||||
media
 | 
			
		||||
#static
 | 
			
		||||
staticfiles
 | 
			
		||||
profile_images
 | 
			
		||||
 | 
			
		||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
 | 
			
		||||
# in your Git repository. Update and uncomment the following line accordingly.
 | 
			
		||||
# <django-project-name>/staticfiles/
 | 
			
		||||
 | 
			
		||||
### Django.Python Stack ###
 | 
			
		||||
# Byte-compiled / optimized / DLL files
 | 
			
		||||
*.py[cod]
 | 
			
		||||
*$py.class
 | 
			
		||||
 | 
			
		||||
# C extensions
 | 
			
		||||
*.so
 | 
			
		||||
 | 
			
		||||
# Distribution / packaging
 | 
			
		||||
.Python
 | 
			
		||||
build/
 | 
			
		||||
develop-eggs/
 | 
			
		||||
#dist/
 | 
			
		||||
downloads/
 | 
			
		||||
eggs/
 | 
			
		||||
.eggs/
 | 
			
		||||
lib/
 | 
			
		||||
lib64/
 | 
			
		||||
parts/
 | 
			
		||||
sdist/
 | 
			
		||||
var/
 | 
			
		||||
wheels/
 | 
			
		||||
share/python-wheels/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
.installed.cfg
 | 
			
		||||
*.egg
 | 
			
		||||
MANIFEST
 | 
			
		||||
 | 
			
		||||
# PyInstaller
 | 
			
		||||
#  Usually these files are written by a python script from a template
 | 
			
		||||
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
			
		||||
*.manifest
 | 
			
		||||
*.spec
 | 
			
		||||
 | 
			
		||||
# Installer logs
 | 
			
		||||
pip-log.txt
 | 
			
		||||
pip-delete-this-directory.txt
 | 
			
		||||
 | 
			
		||||
# Unit test / coverage reports
 | 
			
		||||
htmlcov/
 | 
			
		||||
.tox/
 | 
			
		||||
.nox/
 | 
			
		||||
.coverage
 | 
			
		||||
.coverage.*
 | 
			
		||||
.cache
 | 
			
		||||
nosetests.xml
 | 
			
		||||
coverage.xml
 | 
			
		||||
*.cover
 | 
			
		||||
*.py,cover
 | 
			
		||||
.hypothesis/
 | 
			
		||||
.pytest_cache/
 | 
			
		||||
cover/
 | 
			
		||||
 | 
			
		||||
# Translations
 | 
			
		||||
*.mo
 | 
			
		||||
 | 
			
		||||
# Django stuff:
 | 
			
		||||
 | 
			
		||||
# Flask stuff:
 | 
			
		||||
instance/
 | 
			
		||||
.webassets-cache
 | 
			
		||||
 | 
			
		||||
# Scrapy stuff:
 | 
			
		||||
.scrapy
 | 
			
		||||
 | 
			
		||||
# Sphinx documentation
 | 
			
		||||
docs/_build/
 | 
			
		||||
 | 
			
		||||
# PyBuilder
 | 
			
		||||
.pybuilder/
 | 
			
		||||
target/
 | 
			
		||||
 | 
			
		||||
# Jupyter Notebook
 | 
			
		||||
.ipynb_checkpoints
 | 
			
		||||
 | 
			
		||||
# IPython
 | 
			
		||||
profile_default/
 | 
			
		||||
ipython_config.py
 | 
			
		||||
 | 
			
		||||
# pyenv
 | 
			
		||||
#   For a library or package, you might want to ignore these files since the code is
 | 
			
		||||
#   intended to run in multiple environments; otherwise, check them in:
 | 
			
		||||
# .python-version
 | 
			
		||||
 | 
			
		||||
# pipenv
 | 
			
		||||
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
			
		||||
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
			
		||||
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
			
		||||
#   install all needed dependencies.
 | 
			
		||||
#Pipfile.lock
 | 
			
		||||
 | 
			
		||||
# poetry
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
 | 
			
		||||
#poetry.lock
 | 
			
		||||
 | 
			
		||||
# pdm
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
 | 
			
		||||
#pdm.lock
 | 
			
		||||
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
 | 
			
		||||
#   in version control.
 | 
			
		||||
#   https://pdm.fming.dev/#use-with-ide
 | 
			
		||||
.pdm.toml
 | 
			
		||||
 | 
			
		||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 | 
			
		||||
__pypackages__/
 | 
			
		||||
 | 
			
		||||
# Celery stuff
 | 
			
		||||
celerybeat-schedule
 | 
			
		||||
celerybeat.pid
 | 
			
		||||
 | 
			
		||||
# SageMath parsed files
 | 
			
		||||
*.sage.py
 | 
			
		||||
 | 
			
		||||
# Environments
 | 
			
		||||
.env
 | 
			
		||||
.venv
 | 
			
		||||
env/
 | 
			
		||||
venv/
 | 
			
		||||
ENV/
 | 
			
		||||
env.bak/
 | 
			
		||||
venv.bak/
 | 
			
		||||
 | 
			
		||||
# Spyder project settings
 | 
			
		||||
.spyderproject
 | 
			
		||||
.spyproject
 | 
			
		||||
 | 
			
		||||
# Rope project settings
 | 
			
		||||
.ropeproject
 | 
			
		||||
 | 
			
		||||
# mkdocs documentation
 | 
			
		||||
/site
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
.dmypy.json
 | 
			
		||||
dmypy.json
 | 
			
		||||
 | 
			
		||||
# Pyre type checker
 | 
			
		||||
.pyre/
 | 
			
		||||
 | 
			
		||||
# pytype static type analyzer
 | 
			
		||||
.pytype/
 | 
			
		||||
 | 
			
		||||
# Cython debug symbols
 | 
			
		||||
cython_debug/
 | 
			
		||||
 | 
			
		||||
# PyCharm
 | 
			
		||||
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
 | 
			
		||||
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
 | 
			
		||||
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 | 
			
		||||
#.idea/
 | 
			
		||||
 | 
			
		||||
### Python ###
 | 
			
		||||
# Byte-compiled / optimized / DLL files
 | 
			
		||||
 | 
			
		||||
# C extensions
 | 
			
		||||
 | 
			
		||||
# Distribution / packaging
 | 
			
		||||
 | 
			
		||||
# PyInstaller
 | 
			
		||||
#  Usually these files are written by a python script from a template
 | 
			
		||||
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
			
		||||
 | 
			
		||||
# Installer logs
 | 
			
		||||
 | 
			
		||||
# Unit test / coverage reports
 | 
			
		||||
 | 
			
		||||
# Translations
 | 
			
		||||
 | 
			
		||||
# Django stuff:
 | 
			
		||||
 | 
			
		||||
# Flask stuff:
 | 
			
		||||
 | 
			
		||||
# Scrapy stuff:
 | 
			
		||||
 | 
			
		||||
# Sphinx documentation
 | 
			
		||||
 | 
			
		||||
# PyBuilder
 | 
			
		||||
 | 
			
		||||
# Jupyter Notebook
 | 
			
		||||
 | 
			
		||||
# IPython
 | 
			
		||||
 | 
			
		||||
# pyenv
 | 
			
		||||
#   For a library or package, you might want to ignore these files since the code is
 | 
			
		||||
#   intended to run in multiple environments; otherwise, check them in:
 | 
			
		||||
# .python-version
 | 
			
		||||
 | 
			
		||||
# pipenv
 | 
			
		||||
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
			
		||||
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
			
		||||
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
			
		||||
#   install all needed dependencies.
 | 
			
		||||
 | 
			
		||||
# poetry
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
 | 
			
		||||
 | 
			
		||||
# pdm
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
 | 
			
		||||
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
 | 
			
		||||
#   in version control.
 | 
			
		||||
#   https://pdm.fming.dev/#use-with-ide
 | 
			
		||||
 | 
			
		||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 | 
			
		||||
 | 
			
		||||
# Celery stuff
 | 
			
		||||
 | 
			
		||||
# SageMath parsed files
 | 
			
		||||
 | 
			
		||||
# Environments
 | 
			
		||||
 | 
			
		||||
# Spyder project settings
 | 
			
		||||
 | 
			
		||||
# Rope project settings
 | 
			
		||||
 | 
			
		||||
# mkdocs documentation
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
 | 
			
		||||
# Pyre type checker
 | 
			
		||||
 | 
			
		||||
# pytype static type analyzer
 | 
			
		||||
 | 
			
		||||
# Cython debug symbols
 | 
			
		||||
 | 
			
		||||
# PyCharm
 | 
			
		||||
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
 | 
			
		||||
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
 | 
			
		||||
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 | 
			
		||||
 | 
			
		||||
### Python Patch ###
 | 
			
		||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
 | 
			
		||||
poetry.toml
 | 
			
		||||
 | 
			
		||||
# ruff
 | 
			
		||||
.ruff_cache/
 | 
			
		||||
 | 
			
		||||
# LSP config files
 | 
			
		||||
pyrightconfig.json
 | 
			
		||||
 | 
			
		||||
### VirtualEnv ###
 | 
			
		||||
# Virtualenv
 | 
			
		||||
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
 | 
			
		||||
[Bb]in
 | 
			
		||||
[Ii]nclude
 | 
			
		||||
[Ll]ib
 | 
			
		||||
[Ll]ib64
 | 
			
		||||
[Ll]ocal
 | 
			
		||||
[Ss]cripts
 | 
			
		||||
pyvenv.cfg
 | 
			
		||||
pip-selfcheck.json
 | 
			
		||||
 | 
			
		||||
# End of https://www.toptal.com/developers/gitignore/api/django,python,virtualenv
 | 
			
		||||
 | 
			
		||||
.cursor
 | 
			
		||||
							
								
								
									
										0
									
								
								_base/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										16
									
								
								_base/asgi.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
"""
 | 
			
		||||
ASGI config for _base 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/5.2/howto/deployment/asgi/
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.core.asgi import get_asgi_application
 | 
			
		||||
 | 
			
		||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '_base.settings')
 | 
			
		||||
 | 
			
		||||
application = get_asgi_application()
 | 
			
		||||
							
								
								
									
										172
									
								
								_base/settings.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,172 @@
 | 
			
		|||
"""
 | 
			
		||||
Django settings for _base project.
 | 
			
		||||
 | 
			
		||||
Generated by 'django-admin startproject' using Django 5.2.4.
 | 
			
		||||
 | 
			
		||||
For more information on this file, see
 | 
			
		||||
https://docs.djangoproject.com/en/5.2/topics/settings/
 | 
			
		||||
 | 
			
		||||
For the full list of settings and their values, see
 | 
			
		||||
https://docs.djangoproject.com/en/5.2/ref/settings/
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
 | 
			
		||||
BASE_DIR = Path(__file__).resolve().parent.parent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Quick-start development settings - unsuitable for production
 | 
			
		||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
 | 
			
		||||
 | 
			
		||||
# SECURITY WARNING: keep the secret key used in production secret!
 | 
			
		||||
SECRET_KEY = 'django-insecure-h!2hx$h=f6ktgdks!g2_*pg_s1nnuyk+j2yd*_x8r+3+3iyfy*'
 | 
			
		||||
 | 
			
		||||
# SECURITY WARNING: don't run with debug turned on in production!
 | 
			
		||||
DEBUG = True
 | 
			
		||||
 | 
			
		||||
ALLOWED_HOSTS = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Application definition
 | 
			
		||||
 | 
			
		||||
INSTALLED_APPS = [
 | 
			
		||||
    # ------ theme ------ #
 | 
			
		||||
    'jazzmin',
 | 
			
		||||
    # ------------------- #
 | 
			
		||||
 | 
			
		||||
    'django.contrib.admin',
 | 
			
		||||
    'django.contrib.auth',
 | 
			
		||||
    'django.contrib.contenttypes',
 | 
			
		||||
    'django.contrib.sessions',
 | 
			
		||||
    'django.contrib.messages',
 | 
			
		||||
    'django.contrib.staticfiles',
 | 
			
		||||
    'django.contrib.humanize',
 | 
			
		||||
 | 
			
		||||
    # ------- third party apps ------- #
 | 
			
		||||
    'simple_history',
 | 
			
		||||
    # -------------------------------- #
 | 
			
		||||
 | 
			
		||||
    # ------- my apps ------- #
 | 
			
		||||
    'accounts.apps.AccountsConfig',
 | 
			
		||||
    'locations.apps.LocationsConfig',
 | 
			
		||||
    'wells.apps.WellsConfig',
 | 
			
		||||
    'common.apps.CommonConfig',
 | 
			
		||||
    'processes.apps.ProcessesConfig',
 | 
			
		||||
    'invoices.apps.InvoicesConfig',
 | 
			
		||||
    # ----------------------- #
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
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',
 | 
			
		||||
    'simple_history.middleware.HistoryRequestMiddleware',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
ROOT_URLCONF = '_base.urls'
 | 
			
		||||
 | 
			
		||||
TEMPLATES = [
 | 
			
		||||
    {
 | 
			
		||||
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
 | 
			
		||||
        'DIRS': [os.path.join(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',
 | 
			
		||||
                'common.context_processors.current_year',
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
WSGI_APPLICATION = '_base.wsgi.application'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Database
 | 
			
		||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
 | 
			
		||||
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    'default': {
 | 
			
		||||
        'ENGINE': 'django.db.backends.sqlite3',
 | 
			
		||||
        'NAME': BASE_DIR / 'db.sqlite3',
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Password validation
 | 
			
		||||
# https://docs.djangoproject.com/en/5.2/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/5.2/topics/i18n/
 | 
			
		||||
 | 
			
		||||
LANGUAGES = [
 | 
			
		||||
    ('fa', 'Persian'),
 | 
			
		||||
    # ('en', 'English')
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
LANGUAGE_CODE = 'fa-ir'
 | 
			
		||||
TIME_ZONE = 'Asia/Tehran'
 | 
			
		||||
USE_I18N = True
 | 
			
		||||
USE_L10N = True
 | 
			
		||||
USE_TZ = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Static files (CSS, JavaScript, Images)
 | 
			
		||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
 | 
			
		||||
STATIC_ROOT = 'ss'
 | 
			
		||||
STATIC_URL = 'static/'
 | 
			
		||||
STATICFILES_DIRS = [
 | 
			
		||||
    os.path.join(BASE_DIR, 'static')
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MEDIA_URL = '/media/'
 | 
			
		||||
MEDIA_ROOT = BASE_DIR / 'media'
 | 
			
		||||
 | 
			
		||||
# Default primary key field type
 | 
			
		||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
 | 
			
		||||
 | 
			
		||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
JAZZMIN_SETTINGS = {
 | 
			
		||||
    # title of the window (Will default to current_admin_site.site_title if absent or None)
 | 
			
		||||
    "site_title": "سامانه شفافیت",
 | 
			
		||||
    # Title on the login screen (19 chars max) (defaults to current_admin_site.site_header if absent or None)
 | 
			
		||||
    "site_header": "سامانه شفافیت",
 | 
			
		||||
    # Title on the brand (19 chars max) (defaults to current_admin_site.site_header if absent or None)
 | 
			
		||||
    "site_brand": "سامانه شفافیت",
 | 
			
		||||
    # Welcome text on the login screen
 | 
			
		||||
    "welcome_sign": "به سامانه شفافیت خوش آمدید",
 | 
			
		||||
    # Copyright on the footer
 | 
			
		||||
    "copyright": "سامانه شفافیت",
 | 
			
		||||
    # Logo to use for your site, must be present in static files, used for brand on top left
 | 
			
		||||
    "site_logo": "../static/dist/img/iconlogo.png",
 | 
			
		||||
    # Relative paths to custom CSS/JS scripts (must be present in static files)
 | 
			
		||||
    "custom_css": "../static/admin/css/custom_rtl.css",
 | 
			
		||||
    "custom_js": None,
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								_base/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
"""
 | 
			
		||||
URL configuration for _base project.
 | 
			
		||||
 | 
			
		||||
The `urlpatterns` list routes URLs to views. For more information please see:
 | 
			
		||||
    https://docs.djangoproject.com/en/5.2/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 django.conf import settings
 | 
			
		||||
from django.conf.urls.static import static
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.urls import path, include
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('admin/', admin.site.urls),
 | 
			
		||||
    path('', include('accounts.urls')),
 | 
			
		||||
    path('wells/', include('wells.urls')),
 | 
			
		||||
    path('processes/', include('processes.urls')),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if settings.DEBUG:
 | 
			
		||||
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 | 
			
		||||
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
 | 
			
		||||
							
								
								
									
										16
									
								
								_base/wsgi.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
"""
 | 
			
		||||
WSGI config for _base 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/5.2/howto/deployment/wsgi/
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.core.wsgi import get_wsgi_application
 | 
			
		||||
 | 
			
		||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '_base.settings')
 | 
			
		||||
 | 
			
		||||
application = get_wsgi_application()
 | 
			
		||||
							
								
								
									
										0
									
								
								_helpers/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										192
									
								
								_helpers/jalali.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,192 @@
 | 
			
		|||
# In The Name Of Allah
 | 
			
		||||
#
 | 
			
		||||
# Jalali date converter
 | 
			
		||||
# 2014 07 25
 | 
			
		||||
# Ported from PHP (http://jdf.scr.ir/) to Python (2&3) by Mohammad Javad Naderi <mjnaderi@gmail.com>
 | 
			
		||||
#
 | 
			
		||||
# As mentioned in http://jdf.scr.ir/, the original code is free and open source,
 | 
			
		||||
# and you are not allowed to sell it. You can read more in http://jdf.scr.ir/.
 | 
			
		||||
#
 | 
			
		||||
# Original License Notes:
 | 
			
		||||
#
 | 
			
		||||
#    /** Software Hijri_Shamsi , Solar(Jalali) Date and Time
 | 
			
		||||
#    Copyright(C)2011, Reza Gholampanahi , http://jdf.scr.ir
 | 
			
		||||
#    version 2.55 :: 1391/08/24 = 1433/12/18 = 2012/11/15 */
 | 
			
		||||
#
 | 
			
		||||
#    /** Convertor from and to Gregorian and Jalali (Hijri_Shamsi,Solar) Functions
 | 
			
		||||
#    Copyright(C)2011, Reza Gholampanahi [ http://jdf.scr.ir/jdf ] version 2.50 */
 | 
			
		||||
#
 | 
			
		||||
# Example Usage:
 | 
			
		||||
#
 | 
			
		||||
#  >>> import jalali
 | 
			
		||||
#
 | 
			
		||||
#  >>> jalali.Persian('1393-1-11').gregorian_string()
 | 
			
		||||
#  '2014-3-31'
 | 
			
		||||
#  >>> jalali.Persian(1393, 1, 11).gregorian_datetime()
 | 
			
		||||
#  datetime.date(2014, 3, 31)
 | 
			
		||||
#  >>> jalali.Persian('1393/1/11').gregorian_string("{}/{}/{}")
 | 
			
		||||
#  '2014/3/31'
 | 
			
		||||
#  >>> jalali.Persian((1393, 1, 11)).gregorian_tuple()
 | 
			
		||||
#  (2014, 3, 31)
 | 
			
		||||
#
 | 
			
		||||
#  >>> jalali.Gregorian('2014-3-31').persian_string()
 | 
			
		||||
#  '1393-1-11'
 | 
			
		||||
#  >>> jalali.Gregorian('2014,03,31').persian_tuple()
 | 
			
		||||
#  (1393, 1, 11)
 | 
			
		||||
#  >>> jalali.Gregorian(2014, 3, 31).persian_year
 | 
			
		||||
#  1393
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Gregorian:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *date):
 | 
			
		||||
        # Parse date
 | 
			
		||||
        if len(date) == 1:
 | 
			
		||||
            date = date[0]
 | 
			
		||||
            if type(date) is str:
 | 
			
		||||
                m = re.match(r'^(\d{4})\D(\d{1,2})\D(\d{1,2})$', date)
 | 
			
		||||
                if m:
 | 
			
		||||
                    [year, month, day] = [int(m.group(1)), int(m.group(2)), int(m.group(3))]
 | 
			
		||||
                else:
 | 
			
		||||
                    raise Exception("Invalid Input String")
 | 
			
		||||
            elif type(date) is datetime.date:
 | 
			
		||||
                [year, month, day] = [date.year, date.month, date.day]
 | 
			
		||||
            elif type(date) is tuple:
 | 
			
		||||
                year, month, day = date
 | 
			
		||||
                year = int(year)
 | 
			
		||||
                month = int(month)
 | 
			
		||||
                day = int(day)
 | 
			
		||||
            else:
 | 
			
		||||
                raise Exception("Invalid Input Type")
 | 
			
		||||
        elif len(date) == 3:
 | 
			
		||||
            year = int(date[0])
 | 
			
		||||
            month = int(date[1])
 | 
			
		||||
            day = int(date[2])
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception("Invalid Input")
 | 
			
		||||
 | 
			
		||||
        # Check the validity of input date
 | 
			
		||||
        try:
 | 
			
		||||
            datetime.datetime(year, month, day)
 | 
			
		||||
        except:
 | 
			
		||||
            raise Exception("Invalid Date")
 | 
			
		||||
 | 
			
		||||
        self.gregorian_year = year
 | 
			
		||||
        self.gregorian_month = month
 | 
			
		||||
        self.gregorian_day = day
 | 
			
		||||
 | 
			
		||||
        # Convert date to Jalali
 | 
			
		||||
        d_4 = year % 4
 | 
			
		||||
        g_a = [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
 | 
			
		||||
        doy_g = g_a[month] + day
 | 
			
		||||
        if d_4 == 0 and month > 2:
 | 
			
		||||
            doy_g += 1
 | 
			
		||||
        d_33 = int(((year - 16) % 132) * .0305)
 | 
			
		||||
        a = 286 if (d_33 == 3 or d_33 < (d_4 - 1) or d_4 == 0) else 287
 | 
			
		||||
        if (d_33 == 1 or d_33 == 2) and (d_33 == d_4 or d_4 == 1):
 | 
			
		||||
            b = 78
 | 
			
		||||
        else:
 | 
			
		||||
            b = 80 if (d_33 == 3 and d_4 == 0) else 79
 | 
			
		||||
        if int((year - 10) / 63) == 30:
 | 
			
		||||
            a -= 1
 | 
			
		||||
            b += 1
 | 
			
		||||
        if doy_g > b:
 | 
			
		||||
            jy = year - 621
 | 
			
		||||
            doy_j = doy_g - b
 | 
			
		||||
        else:
 | 
			
		||||
            jy = year - 622
 | 
			
		||||
            doy_j = doy_g + a
 | 
			
		||||
        if doy_j < 187:
 | 
			
		||||
            jm = int((doy_j - 1) / 31)
 | 
			
		||||
            jd = doy_j - (31 * jm)
 | 
			
		||||
            jm += 1
 | 
			
		||||
        else:
 | 
			
		||||
            jm = int((doy_j - 187) / 30)
 | 
			
		||||
            jd = doy_j - 186 - (jm * 30)
 | 
			
		||||
            jm += 7
 | 
			
		||||
        self.persian_year = jy
 | 
			
		||||
        self.persian_month = jm
 | 
			
		||||
        self.persian_day = jd
 | 
			
		||||
 | 
			
		||||
    def persian_tuple(self):
 | 
			
		||||
        return self.persian_year, self.persian_month, self.persian_day
 | 
			
		||||
 | 
			
		||||
    def persian_string(self, date_format="{}-{}-{}"):
 | 
			
		||||
        return date_format.format(self.persian_year, self.persian_month, self.persian_day)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Persian:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *date):
 | 
			
		||||
        # Parse date
 | 
			
		||||
        if len(date) == 1:
 | 
			
		||||
            date = date[0]
 | 
			
		||||
            if type(date) is str:
 | 
			
		||||
                m = re.match(r'^(\d{4})\D(\d{1,2})\D(\d{1,2})$', date)
 | 
			
		||||
                if m:
 | 
			
		||||
                    [year, month, day] = [int(m.group(1)), int(m.group(2)), int(m.group(3))]
 | 
			
		||||
                else:
 | 
			
		||||
                    raise Exception("Invalid Input String")
 | 
			
		||||
            elif type(date) is tuple:
 | 
			
		||||
                year, month, day = date
 | 
			
		||||
                year = int(year)
 | 
			
		||||
                month = int(month)
 | 
			
		||||
                day = int(day)
 | 
			
		||||
            else:
 | 
			
		||||
                raise Exception("Invalid Input Type")
 | 
			
		||||
        elif len(date) == 3:
 | 
			
		||||
            year = int(date[0])
 | 
			
		||||
            month = int(date[1])
 | 
			
		||||
            day = int(date[2])
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception("Invalid Input")
 | 
			
		||||
 | 
			
		||||
        # Check validity of date. TODO better check (leap years)
 | 
			
		||||
        if year < 1 or month < 1 or month > 12 or day < 1 or day > 31 or (month > 6 and day == 31):
 | 
			
		||||
            raise Exception("Incorrect Date")
 | 
			
		||||
 | 
			
		||||
        self.persian_year = year
 | 
			
		||||
        self.persian_month = month
 | 
			
		||||
        self.persian_day = day
 | 
			
		||||
 | 
			
		||||
        # Convert date
 | 
			
		||||
        d_4 = (year + 1) % 4
 | 
			
		||||
        if month < 7:
 | 
			
		||||
            doy_j = ((month - 1) * 31) + day
 | 
			
		||||
        else:
 | 
			
		||||
            doy_j = ((month - 7) * 30) + day + 186
 | 
			
		||||
        d_33 = int(((year - 55) % 132) * .0305)
 | 
			
		||||
        a = 287 if (d_33 != 3 and d_4 <= d_33) else 286
 | 
			
		||||
        if (d_33 == 1 or d_33 == 2) and (d_33 == d_4 or d_4 == 1):
 | 
			
		||||
            b = 78
 | 
			
		||||
        else:
 | 
			
		||||
            b = 80 if (d_33 == 3 and d_4 == 0) else 79
 | 
			
		||||
        if int((year - 19) / 63) == 20:
 | 
			
		||||
            a -= 1
 | 
			
		||||
            b += 1
 | 
			
		||||
        if doy_j <= a:
 | 
			
		||||
            gy = year + 621
 | 
			
		||||
            gd = doy_j + b
 | 
			
		||||
        else:
 | 
			
		||||
            gy = year + 622
 | 
			
		||||
            gd = doy_j - a
 | 
			
		||||
        for gm, v in enumerate([0, 31, 29 if (gy % 4 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]):
 | 
			
		||||
            if gd <= v:
 | 
			
		||||
                break
 | 
			
		||||
            gd -= v
 | 
			
		||||
 | 
			
		||||
        self.gregorian_year = gy
 | 
			
		||||
        self.gregorian_month = gm
 | 
			
		||||
        self.gregorian_day = gd
 | 
			
		||||
 | 
			
		||||
    def gregorian_tuple(self):
 | 
			
		||||
        return self.gregorian_year, self.gregorian_month, self.gregorian_day
 | 
			
		||||
 | 
			
		||||
    def gregorian_string(self, date_format="{}-{}-{}"):
 | 
			
		||||
        return date_format.format(self.gregorian_year, self.gregorian_month, self.gregorian_day)
 | 
			
		||||
 | 
			
		||||
    def gregorian_datetime(self):
 | 
			
		||||
        return datetime.date(self.gregorian_year, self.gregorian_month, self.gregorian_day)
 | 
			
		||||
							
								
								
									
										195
									
								
								_helpers/utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,195 @@
 | 
			
		|||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.core.validators import RegexValidator
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
 | 
			
		||||
from . import jalali
 | 
			
		||||
 | 
			
		||||
__jmonths = [
 | 
			
		||||
    "فروردین", "اردیبهشت", "خرداد",
 | 
			
		||||
    "تیر", "مرداد", "شهریور",
 | 
			
		||||
    "مهر", "آبان", "آذر",
 | 
			
		||||
    "دی", "بهمن", "اسفند",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def persian_numbers_converter(mystr):
 | 
			
		||||
    numbers = {
 | 
			
		||||
        "0": "۰",
 | 
			
		||||
        "1": "۱",
 | 
			
		||||
        "2": "۲",
 | 
			
		||||
        "3": "۳",
 | 
			
		||||
        "4": "۴",
 | 
			
		||||
        "5": "۵",
 | 
			
		||||
        "6": "۶",
 | 
			
		||||
        "7": "۷",
 | 
			
		||||
        "8": "۸",
 | 
			
		||||
        "9": "۹",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for e, p in numbers.items():
 | 
			
		||||
        mystr = mystr.replace(e, p)
 | 
			
		||||
 | 
			
		||||
    return mystr
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def jalali_converter(time):
 | 
			
		||||
    try:
 | 
			
		||||
        time = timezone.localtime(time)
 | 
			
		||||
    except:
 | 
			
		||||
        time = time
 | 
			
		||||
 | 
			
		||||
    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
			
		||||
    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
			
		||||
    time_to_list = list(time_to_tuple)
 | 
			
		||||
 | 
			
		||||
    for index, month in enumerate(__jmonths):
 | 
			
		||||
        if time_to_list[1] == index + 1:
 | 
			
		||||
            time_to_list[1] = month
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    output = "{} {} {}, ساعت {}:{}".format(
 | 
			
		||||
        time_to_list[2],
 | 
			
		||||
        time_to_list[1],
 | 
			
		||||
        time_to_list[0],
 | 
			
		||||
        time.hour,
 | 
			
		||||
        time.minute,
 | 
			
		||||
    )
 | 
			
		||||
    return persian_numbers_converter(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_client_ip(request):
 | 
			
		||||
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
 | 
			
		||||
    if x_forwarded_for:
 | 
			
		||||
        ip = x_forwarded_for.split(',')[0]
 | 
			
		||||
    else:
 | 
			
		||||
        ip = request.META.get('REMOTE_ADDR')
 | 
			
		||||
    return ip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def jalali_converter21(time):
 | 
			
		||||
    time = timezone.localtime(time)
 | 
			
		||||
 | 
			
		||||
    year, month, day = jalali.Gregorian(time.strftime("%Y-%m-%d")).persian_tuple()[:3]
 | 
			
		||||
    hour, minute = time.strftime("%H:%M").split(':')
 | 
			
		||||
 | 
			
		||||
    return f"{day} {__jmonths[month - 1]} {year}, ساعت {hour}:{minute}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def jalali_converter2(time):
 | 
			
		||||
    try:
 | 
			
		||||
        time = timezone.localdate(time)
 | 
			
		||||
    except:
 | 
			
		||||
        time = time
 | 
			
		||||
 | 
			
		||||
    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
			
		||||
    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
			
		||||
    time_to_list = list(time_to_tuple)
 | 
			
		||||
 | 
			
		||||
    for index, month in enumerate(__jmonths):
 | 
			
		||||
        if time_to_list[1] == index + 1:
 | 
			
		||||
            time_to_list[1] = month
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    output = "{} {} {}".format(
 | 
			
		||||
        time_to_list[2],
 | 
			
		||||
        time_to_list[1],
 | 
			
		||||
        time_to_list[0],
 | 
			
		||||
    )
 | 
			
		||||
    return persian_numbers_converter(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def gregorian_converter(time):
 | 
			
		||||
    time_to_list = time.split('/')
 | 
			
		||||
    time_to_str = "{},{},{}".format(time_to_list[0], time_to_list[1], time_to_list[2])
 | 
			
		||||
    return jalali.Persian(time_to_str).gregorian_string()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_client_ip(request):
 | 
			
		||||
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
 | 
			
		||||
    if x_forwarded_for:
 | 
			
		||||
        ip = x_forwarded_for.split(',')[0]
 | 
			
		||||
    else:
 | 
			
		||||
        ip = request.META.get('REMOTE_ADDR')
 | 
			
		||||
    return ip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def persian_converter(time):
 | 
			
		||||
    time = time + datetime.timedelta(days=1)
 | 
			
		||||
    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
			
		||||
    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
			
		||||
    time_to_list = list(time_to_tuple)
 | 
			
		||||
    return time_to_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def persian_converter2(time):
 | 
			
		||||
    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
			
		||||
    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
			
		||||
    time_to_list = list(time_to_tuple)
 | 
			
		||||
 | 
			
		||||
    for index, month in enumerate(__jmonths):
 | 
			
		||||
        if time_to_list[1] == index + 1:
 | 
			
		||||
            time_to_list[1] = month
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    output = "{} {} {}".format(
 | 
			
		||||
        time_to_list[2],
 | 
			
		||||
        time_to_list[1],
 | 
			
		||||
        time_to_list[0],
 | 
			
		||||
    )
 | 
			
		||||
    return persian_numbers_converter(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def persian_converter3(time):
 | 
			
		||||
    time = time + datetime.timedelta(days=1)
 | 
			
		||||
    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
			
		||||
    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
			
		||||
    time_to_list = list(time_to_tuple)
 | 
			
		||||
    time_to_list = [str(item) for item in time_to_list]
 | 
			
		||||
    return '/'.join(time_to_list)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_file(path):
 | 
			
		||||
    """ Deletes file from filesystem. """
 | 
			
		||||
    if os.path.isfile(path):
 | 
			
		||||
        os.remove(path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_phone_number_validator():
 | 
			
		||||
    return RegexValidator(
 | 
			
		||||
        regex=r'^[0]{1}[9]{1}[0-9]{9}',
 | 
			
		||||
        message='مقدار وارد شده صحیح نمیباشد',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_unique_slug(text: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Generates a unique slug from received text
 | 
			
		||||
    """
 | 
			
		||||
    slug = slugify(text)
 | 
			
		||||
    unique_id = str(uuid.uuid4())[:8]
 | 
			
		||||
    unique_slug = f"{slug}-{unique_id}"
 | 
			
		||||
 | 
			
		||||
    return unique_slug
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def normalize_size(size: int) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Normalizes file or volume sizes and returns it with the proper suffix,
 | 
			
		||||
    showing the decimal only if it's non-zero.
 | 
			
		||||
    """
 | 
			
		||||
    size = float(size)
 | 
			
		||||
    if size < 1024:
 | 
			
		||||
        return f"{int(size)} B" if size.is_integer() else f"{size:.1f} B"
 | 
			
		||||
    elif size < 1024 * 1024:
 | 
			
		||||
        size_kb = size / 1024
 | 
			
		||||
        return f"{int(size_kb)} KB" if size_kb.is_integer() else f"{size_kb:.1f} KB"
 | 
			
		||||
    elif size < 1024 * 1024 * 1024:
 | 
			
		||||
        size_mb = size / (1024 * 1024)
 | 
			
		||||
        return f"{int(size_mb)} MB" if size_mb.is_integer() else f"{size_mb:.1f} MB"
 | 
			
		||||
    else:
 | 
			
		||||
        size_gb = size / (1024 * 1024 * 1024)
 | 
			
		||||
        return f"{int(size_gb)} GB" if size_gb.is_integer() else f"{size_gb:.1f} GB"
 | 
			
		||||
							
								
								
									
										0
									
								
								accounts/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										32
									
								
								accounts/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from accounts.models import Role, Profile
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Register your models here.
 | 
			
		||||
@admin.register(Role)
 | 
			
		||||
class RoleAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'slug', 'parent', 'is_active']
 | 
			
		||||
    search_fields = ['name', 'slug']
 | 
			
		||||
    list_filter = ['is_active']
 | 
			
		||||
    ordering = ['parent__name', 'name']
 | 
			
		||||
 | 
			
		||||
@admin.register(Profile)
 | 
			
		||||
class ProfileAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = [
 | 
			
		||||
        "user",
 | 
			
		||||
        "fullname",
 | 
			
		||||
        "pic_tag",
 | 
			
		||||
        "roles_str",
 | 
			
		||||
        "affairs",
 | 
			
		||||
        "county",
 | 
			
		||||
        "broker",
 | 
			
		||||
        "is_completed",
 | 
			
		||||
        "is_active",
 | 
			
		||||
        "jcreated",
 | 
			
		||||
    ]
 | 
			
		||||
    search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__phone_number']
 | 
			
		||||
    list_filter = ['user', 'roles', 'affairs', 'county', 'broker']
 | 
			
		||||
    date_hierarchy = 'created'
 | 
			
		||||
    ordering = ['-created']
 | 
			
		||||
    readonly_fields = ['created', 'updated']
 | 
			
		||||
							
								
								
									
										7
									
								
								accounts/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccountsConfig(AppConfig):
 | 
			
		||||
    default_auto_field = 'django.db.models.BigAutoField'
 | 
			
		||||
    name = 'accounts'
 | 
			
		||||
    verbose_name = "حسابها"
 | 
			
		||||
							
								
								
									
										157
									
								
								accounts/forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,157 @@
 | 
			
		|||
from django import forms
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.contrib.auth.forms import UserCreationForm
 | 
			
		||||
from .models import Profile, Role
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
User = get_user_model()
 | 
			
		||||
 | 
			
		||||
class CustomerForm(forms.ModelForm):
 | 
			
		||||
    """فرم برای افزودن مشترک جدید"""
 | 
			
		||||
    first_name = forms.CharField(
 | 
			
		||||
        max_length=150,
 | 
			
		||||
        label="نام",
 | 
			
		||||
        widget=forms.TextInput(attrs={
 | 
			
		||||
            'class': 'form-control',
 | 
			
		||||
            'placeholder': 'نام'
 | 
			
		||||
        })
 | 
			
		||||
    )
 | 
			
		||||
    last_name = forms.CharField(
 | 
			
		||||
        max_length=150,
 | 
			
		||||
        label="نام خانوادگی",
 | 
			
		||||
        widget=forms.TextInput(attrs={
 | 
			
		||||
            'class': 'form-control',
 | 
			
		||||
            'placeholder': 'نام خانوادگی'
 | 
			
		||||
        })
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Profile
 | 
			
		||||
        fields = [
 | 
			
		||||
            'phone_number_1', 'phone_number_2', 'national_code', 
 | 
			
		||||
            'address', 'card_number', 'account_number'
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'phone_number_1': forms.TextInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': '09123456789'
 | 
			
		||||
            }),
 | 
			
		||||
            'phone_number_2': forms.TextInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': '02112345678'
 | 
			
		||||
            }),
 | 
			
		||||
            'national_code': forms.TextInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': '1234567890',
 | 
			
		||||
                'maxlength': '10',
 | 
			
		||||
                'required': 'required'
 | 
			
		||||
            }),
 | 
			
		||||
            'address': forms.Textarea(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': 'آدرس کامل',
 | 
			
		||||
                'rows': '3'
 | 
			
		||||
            }),
 | 
			
		||||
            'card_number': forms.TextInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': 'شماره کارت بانکی',
 | 
			
		||||
                'maxlength': '16'
 | 
			
		||||
            }),
 | 
			
		||||
            'account_number': forms.TextInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': 'شماره حساب بانکی',
 | 
			
		||||
                'maxlength': '20'
 | 
			
		||||
            }),
 | 
			
		||||
        }
 | 
			
		||||
        labels = {
 | 
			
		||||
            'phone_number_1': 'تلفن ۱',
 | 
			
		||||
            'phone_number_2': 'تلفن ۲',
 | 
			
		||||
            'national_code': 'کد ملی',
 | 
			
		||||
            'address': 'آدرس',
 | 
			
		||||
            'card_number': 'شماره کارت',
 | 
			
		||||
            'account_number': 'شماره حساب',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def clean_national_code(self):
 | 
			
		||||
        national_code = self.cleaned_data.get('national_code')
 | 
			
		||||
        if national_code:
 | 
			
		||||
            # Check if user with this national code exists
 | 
			
		||||
            existing_user = User.objects.filter(username=national_code).first()
 | 
			
		||||
            if existing_user:
 | 
			
		||||
                # If we're editing and the user is the same, it's OK
 | 
			
		||||
                if self.instance and self.instance.user and existing_user == self.instance.user:
 | 
			
		||||
                    return national_code
 | 
			
		||||
                # Otherwise, it's a duplicate
 | 
			
		||||
                raise forms.ValidationError('این کد ملی قبلاً استفاده شده است.')
 | 
			
		||||
        return national_code
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        # Check if this is an update (instance exists)
 | 
			
		||||
        if self.instance and self.instance.pk:
 | 
			
		||||
            # Update existing profile
 | 
			
		||||
            profile = super().save(commit=False)
 | 
			
		||||
            
 | 
			
		||||
            # Update user information
 | 
			
		||||
            user = profile.user
 | 
			
		||||
            user.first_name = self.cleaned_data['first_name']
 | 
			
		||||
            user.last_name = self.cleaned_data['last_name']
 | 
			
		||||
            user.save()
 | 
			
		||||
            
 | 
			
		||||
            # Set affairs, county, and broker from current user's profile
 | 
			
		||||
            if hasattr(self, 'request') and self.request.user.is_authenticated:
 | 
			
		||||
                current_user_profile = getattr(self.request.user, 'profile', None)
 | 
			
		||||
                if current_user_profile:
 | 
			
		||||
                    profile.affairs = current_user_profile.affairs
 | 
			
		||||
                    profile.county = current_user_profile.county
 | 
			
		||||
                    profile.broker = current_user_profile.broker
 | 
			
		||||
            
 | 
			
		||||
            if commit:
 | 
			
		||||
                profile.save()
 | 
			
		||||
                self.save_m2m()
 | 
			
		||||
            
 | 
			
		||||
            return profile
 | 
			
		||||
        else:
 | 
			
		||||
            # Create new profile
 | 
			
		||||
            # Get national code as username
 | 
			
		||||
            national_code = self.cleaned_data.get('national_code')
 | 
			
		||||
            if not national_code:
 | 
			
		||||
                raise forms.ValidationError('کد ملی الزامی است.')
 | 
			
		||||
            
 | 
			
		||||
            # Create User with default password
 | 
			
		||||
            user = User.objects.create_user(
 | 
			
		||||
                username=national_code,
 | 
			
		||||
                email='',  # Empty email
 | 
			
		||||
                password='sooha1234',  # Default password
 | 
			
		||||
                first_name=self.cleaned_data['first_name'],
 | 
			
		||||
                last_name=self.cleaned_data['last_name']
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            # Create Profile
 | 
			
		||||
            profile = super().save(commit=False)
 | 
			
		||||
            profile.user = user
 | 
			
		||||
            profile.owner = user
 | 
			
		||||
            
 | 
			
		||||
            # Set affairs, county, and broker from current user's profile
 | 
			
		||||
            if hasattr(self, 'request') and self.request.user.is_authenticated:
 | 
			
		||||
                current_user_profile = getattr(self.request.user, 'profile', None)
 | 
			
		||||
                if current_user_profile:
 | 
			
		||||
                    profile.affairs = current_user_profile.affairs
 | 
			
		||||
                    profile.county = current_user_profile.county
 | 
			
		||||
                    profile.broker = current_user_profile.broker
 | 
			
		||||
            
 | 
			
		||||
            if commit:
 | 
			
		||||
                profile.save()
 | 
			
		||||
                self.save_m2m()
 | 
			
		||||
                
 | 
			
		||||
                # Add customer role after profile is saved
 | 
			
		||||
                customer_role = Role.objects.filter(slug=UserRoles.CUSTOMER.value).first()
 | 
			
		||||
                if customer_role:
 | 
			
		||||
                    profile.roles.add(customer_role)
 | 
			
		||||
                else:
 | 
			
		||||
                    # Create customer role if it doesn't exist
 | 
			
		||||
                    customer_role = Role.objects.create(
 | 
			
		||||
                        name='مشترک',
 | 
			
		||||
                        slug=UserRoles.CUSTOMER.value
 | 
			
		||||
                    )
 | 
			
		||||
                    profile.roles.add(customer_role)
 | 
			
		||||
            
 | 
			
		||||
            return profile 
 | 
			
		||||
							
								
								
									
										1
									
								
								accounts/management/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
# Management package for accounts app 
 | 
			
		||||
							
								
								
									
										1
									
								
								accounts/management/commands/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
# Commands package for accounts app 
 | 
			
		||||
							
								
								
									
										51
									
								
								accounts/management/commands/create_roles.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import Role
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Generates default roles"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        roles = [
 | 
			
		||||
            {
 | 
			
		||||
                "name": "ادمین",
 | 
			
		||||
                "slug": UserRoles.ADMIN,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "مشترک",
 | 
			
		||||
                "slug": UserRoles.CUSTOMER,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "مدیر",
 | 
			
		||||
                "slug": UserRoles.MANAGER,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "حسابدار",
 | 
			
		||||
                "slug": UserRoles.ACCOUNTANT,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "پیشخوان",
 | 
			
		||||
                "slug": UserRoles.BROKER,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "نصاب",
 | 
			
		||||
                "slug": UserRoles.INSTALLER,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "کارشناس امور",
 | 
			
		||||
                "slug": UserRoles.REGIONAL_WATER_AUTHORITY,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "مدیر منابع آب",
 | 
			
		||||
                "slug": UserRoles.WATER_RESOURCE_MANAGER,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "ستاد آبمنطقهای",
 | 
			
		||||
                "slug": UserRoles.HEADQUARTER,
 | 
			
		||||
            },
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for role in roles:
 | 
			
		||||
            Role.objects.get_or_create(name=role['name'], slug=role['slug'].value)
 | 
			
		||||
							
								
								
									
										62
									
								
								accounts/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Role',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='accounts.role', verbose_name='زیرشاخه')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'نقش',
 | 
			
		||||
                'verbose_name_plural': 'نقش\u200cها',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Profile',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
 | 
			
		||||
                ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
 | 
			
		||||
                ('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')),
 | 
			
		||||
                ('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')),
 | 
			
		||||
                ('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')),
 | 
			
		||||
                ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
 | 
			
		||||
                ('pic', models.ImageField(default='../static/dist/img/profile.jpg', upload_to='profile_images', verbose_name='تصویر')),
 | 
			
		||||
                ('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
 | 
			
		||||
                ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_profiles', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
 | 
			
		||||
                ('roles', models.ManyToManyField(blank=True, related_name='profiles', to='accounts.role', verbose_name='نقش\u200cها')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'پروفایل',
 | 
			
		||||
                'verbose_name_plural': 'پروفایل\u200cها',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-08-07 14:29
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import simple_history.models
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0001_initial'),
 | 
			
		||||
        ('locations', '0001_initial'),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='profile',
 | 
			
		||||
            name='affairs',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='profile',
 | 
			
		||||
            name='broker',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='profile',
 | 
			
		||||
            name='county',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='profile',
 | 
			
		||||
            name='pic',
 | 
			
		||||
            field=models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalProfile',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
 | 
			
		||||
                ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
 | 
			
		||||
                ('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')),
 | 
			
		||||
                ('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')),
 | 
			
		||||
                ('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')),
 | 
			
		||||
                ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
 | 
			
		||||
                ('pic', models.TextField(default='../static/sample_images/profile.jpg', max_length=100, verbose_name='تصویر')),
 | 
			
		||||
                ('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('affairs', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور')),
 | 
			
		||||
                ('broker', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار')),
 | 
			
		||||
                ('county', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical پروفایل',
 | 
			
		||||
                'verbose_name_plural': 'historical پروفایل\u200cها',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								accounts/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										172
									
								
								accounts/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,172 @@
 | 
			
		|||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.html import format_html
 | 
			
		||||
from django.core.validators import RegexValidator
 | 
			
		||||
from simple_history.models import HistoricalRecords
 | 
			
		||||
from common.models import TagModel, BaseModel
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
from locations.models import Affairs, Broker, County
 | 
			
		||||
 | 
			
		||||
# Create your models here.
 | 
			
		||||
 | 
			
		||||
class Role(TagModel):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "نقش"
 | 
			
		||||
        verbose_name_plural = "نقشها"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Profile(BaseModel):
 | 
			
		||||
    user = models.OneToOneField(
 | 
			
		||||
        get_user_model(),
 | 
			
		||||
        null=True,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        verbose_name="کاربر",
 | 
			
		||||
        related_name="profile"
 | 
			
		||||
    )
 | 
			
		||||
    national_code = models.CharField(
 | 
			
		||||
        max_length=10,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="کد ملی",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex=r'^\d+$',
 | 
			
		||||
                message='کد ملی باید فقط شامل اعداد باشد.',
 | 
			
		||||
                code='invalid_national_code'
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
    address = models.TextField(
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="آدرس",
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    card_number = models.CharField(
 | 
			
		||||
        max_length=16,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره کارت",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex=r'^\d+$',
 | 
			
		||||
                message='شماره کارت باید فقط شامل اعداد باشد.',
 | 
			
		||||
                code='invalid_card_number'
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
    account_number = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره حساب",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex=r'^\d+$',
 | 
			
		||||
                message='شماره حساب باید فقط شامل اعداد باشد.',
 | 
			
		||||
                code='invalid_account_number'
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
    phone_number_1 = models.CharField(
 | 
			
		||||
        max_length=11,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره تماس ۱",
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    phone_number_2 = models.CharField(
 | 
			
		||||
        max_length=11,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره تماس ۲",
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    pic = models.ImageField(
 | 
			
		||||
        upload_to="profile_images",
 | 
			
		||||
        verbose_name="تصویر",
 | 
			
		||||
        default="../static/sample_images/profile.jpg"
 | 
			
		||||
    )
 | 
			
		||||
    roles = models.ManyToManyField(
 | 
			
		||||
        Role,
 | 
			
		||||
        verbose_name="نقشها",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="profiles"
 | 
			
		||||
    )
 | 
			
		||||
    is_completed = models.BooleanField(
 | 
			
		||||
        default=False,
 | 
			
		||||
        verbose_name="پروفایل تکمیل شده"
 | 
			
		||||
    )
 | 
			
		||||
    owner = models.ForeignKey(
 | 
			
		||||
        get_user_model(),
 | 
			
		||||
        null=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name="ایجاد کننده",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="owned_profiles"
 | 
			
		||||
    )
 | 
			
		||||
    affairs = models.ForeignKey(
 | 
			
		||||
        Affairs,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name="امور",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    county = models.ForeignKey(
 | 
			
		||||
        County,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name="شهرستان",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True
 | 
			
		||||
        )
 | 
			
		||||
    broker = models.ForeignKey(
 | 
			
		||||
        Broker,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name="کارگزار",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "پروفایل"
 | 
			
		||||
        verbose_name_plural = "پروفایلها"
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return str(self.user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def first_name(self):
 | 
			
		||||
        return self.user.first_name
 | 
			
		||||
 | 
			
		||||
    first_name.short_description = "نام"
 | 
			
		||||
 | 
			
		||||
    def last_name(self):
 | 
			
		||||
        return self.user.last_name
 | 
			
		||||
 | 
			
		||||
    last_name.short_description = "نام خانوادگی"
 | 
			
		||||
 | 
			
		||||
    def fullname(self):
 | 
			
		||||
        return self.user.get_full_name()
 | 
			
		||||
 | 
			
		||||
    fullname.short_description = "نام و نام خانوادگی"
 | 
			
		||||
 | 
			
		||||
    def roles_str(self):
 | 
			
		||||
        return ', '.join([role.name for role in self.roles.all()])
 | 
			
		||||
 | 
			
		||||
    roles_str.short_description = "نقشها"
 | 
			
		||||
 | 
			
		||||
    def has_role(self, role: UserRoles):
 | 
			
		||||
        return self.roles.filter(slug=role.value).exists()
 | 
			
		||||
 | 
			
		||||
    def has_any_of(self, roles: list[UserRoles]):
 | 
			
		||||
        allowed_roles = [role.value for role in roles]
 | 
			
		||||
        return len(self.roles.filter(slug__in=allowed_roles)) > 0
 | 
			
		||||
 | 
			
		||||
    def has_none_of(self, roles: list[UserRoles]):
 | 
			
		||||
        return not self.has_any_of(roles)
 | 
			
		||||
 | 
			
		||||
    def pic_tag(self):
 | 
			
		||||
        return format_html(f"<img style='width:30px;' src='{self.pic.url}'>")
 | 
			
		||||
 | 
			
		||||
    pic_tag.short_description = "تصویر"
 | 
			
		||||
							
								
								
									
										489
									
								
								accounts/templates/accounts/customer_list.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,489 @@
 | 
			
		|||
{% extends '_base.html' %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block sidebar %}
 | 
			
		||||
    {% include 'sidebars/admin.html' %}
 | 
			
		||||
{% endblock sidebar %}
 | 
			
		||||
 | 
			
		||||
{% block navbar %}
 | 
			
		||||
    {% include 'navbars/admin.html' %}
 | 
			
		||||
{% endblock navbar %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block title %}کاربرها{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block style %}
 | 
			
		||||
<!-- DataTables CSS -->
 | 
			
		||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-bs5/datatables.bootstrap5.css' %}">
 | 
			
		||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.css' %}">
 | 
			
		||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.css' %}">
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- Include Toasts -->
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
 | 
			
		||||
<div class="row py-3 mb-4 card-header flex-column flex-md-row pb-0">
 | 
			
		||||
  <div class="d-md-flex justify-content-between align-items-center dt-layout-start col-md-auto me-auto mt-0">
 | 
			
		||||
    <h5 class="card-title mb-0 text-md-start text-center fw-bold">لیست کاربرها</h5>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
 | 
			
		||||
    <div class="dt-buttons btn-group flex-wrap mb-0">
 | 
			
		||||
      <div class="btn-group">
 | 
			
		||||
        <button class="btn buttons-collection btn-label-primary dropdown-toggle me-4" tabindex="0" aria-controls="DataTables_Table_0" type="button" aria-haspopup="dialog" aria-expanded="false">
 | 
			
		||||
          <span>
 | 
			
		||||
            <span class="d-flex align-items-center gap-2">
 | 
			
		||||
              <i class="icon-base bx bx-export me-sm-1">
 | 
			
		||||
              </i>
 | 
			
		||||
              <span class="d-none d-sm-inline-block">
 | 
			
		||||
                خروجی
 | 
			
		||||
              </span>
 | 
			
		||||
            </span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </button>
 | 
			
		||||
        <button class="btn btn-primary " onclick="prepareAddForm()">
 | 
			
		||||
          <i class="bx bx-plus me-1"></i>
 | 
			
		||||
          افزودن کاربر جدید
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
  <div class="card">
 | 
			
		||||
    <div class="card-datatable table-responsive">
 | 
			
		||||
      <table class="datatables-basic table border-top" id="customers-table">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>ردیف</th>
 | 
			
		||||
            <th>کاربر</th>
 | 
			
		||||
            <th>کد ملی</th>
 | 
			
		||||
            <th>تلفن</th>
 | 
			
		||||
            <th>آدرس</th>
 | 
			
		||||
            <th>وضعیت</th>
 | 
			
		||||
            <th>عملیات</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          {% for customer in customers %}
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td>{{forloop.counter}}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="d-flex justify-content-start align-items-center user-name">
 | 
			
		||||
                <div class="avatar-wrapper">
 | 
			
		||||
                  <div class="avatar me-2">
 | 
			
		||||
                    {% if customer.pic and customer.pic.url %}
 | 
			
		||||
                      <img src="{{ customer.pic.url }}" alt="Avatar" class="rounded-circle" style="width: 40px; height: 40px; object-fit: cover;">
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                      <span class="avatar-initial rounded-circle bg-label-primary">
 | 
			
		||||
                        {% if customer.user.first_name and customer.user.last_name %}
 | 
			
		||||
                          {{ customer.user.first_name|first|upper }}{{ customer.user.last_name|first|upper }}
 | 
			
		||||
                        {% elif customer.user.first_name %}
 | 
			
		||||
                          {{ customer.user.first_name|first|upper }}
 | 
			
		||||
                        {% elif customer.user.last_name %}
 | 
			
		||||
                          {{ customer.user.last_name|first|upper }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                          {{ customer.user.username|first|upper }}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                      </span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="d-flex flex-column">
 | 
			
		||||
                  <span class="emp_name text-truncate text-heading">{{ customer.user.get_full_name|default:customer.user.username }}</span>
 | 
			
		||||
                  <small class="emp_post text-truncate">{{ customer.user.username }}</small>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>{{ customer.national_code|default:"کد ملی ثبت نشده" }}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="d-flex flex-column">
 | 
			
		||||
                {% if customer.phone_number_1 %}
 | 
			
		||||
                  <span class="fw-medium">{{ customer.phone_number_1 }}</span>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if customer.phone_number_2 %}
 | 
			
		||||
                  <small class="text-muted">{{ customer.phone_number_2 }}</small>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if not customer.phone_number_1 and not customer.phone_number_2 %}
 | 
			
		||||
                  <span class="text-muted">تلفن ثبت نشده</span>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
              {% if customer.address %}
 | 
			
		||||
                <span class="text-truncate d-inline-block" style="max-width: 200px;" title="{{ customer.address }}">
 | 
			
		||||
                  {{ customer.address|truncatechars:50 }}
 | 
			
		||||
                </span>
 | 
			
		||||
              {% else %}
 | 
			
		||||
                <span class="text-muted">آدرس ثبت نشده</span>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
              {% if customer.is_completed %}
 | 
			
		||||
                <span class="badge bg-label-success">تکمیل شده</span>
 | 
			
		||||
              {% else %}
 | 
			
		||||
                <span class="badge bg-label-warning">ناقص</span>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="d-inline-block">
 | 
			
		||||
                <a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
 | 
			
		||||
                  <i class="icon-base bx bx-dots-vertical-rounded"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
                <ul class="dropdown-menu dropdown-menu-end m-0">
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a href="#" class="dropdown-item" data-customer-id="{{ customer.id }}" onclick="viewCustomer(this.getAttribute('data-customer-id'))">
 | 
			
		||||
                      <i class="bx bx-show me-1"></i>مشاهده جزئیات
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a href="#" class="dropdown-item" data-customer-id="{{ customer.id }}" onclick="editCustomer(this.getAttribute('data-customer-id'))">
 | 
			
		||||
                      <i class="bx bx-edit me-1"></i>ویرایش
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <div class="dropdown-divider"></div>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a href="#" class="dropdown-item text-danger" data-customer-id="{{ customer.id }}" data-customer-name="{{ customer.user.get_full_name|default:customer.user.username }}" onclick="deleteCustomer(this.getAttribute('data-customer-id'), this.getAttribute('data-customer-name'))">
 | 
			
		||||
                      <i class="bx bx-trash me-1"></i>حذف
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
              </div>
 | 
			
		||||
              <a href="#" class="btn btn-icon item-edit" data-customer-id="{{ customer.id }}" onclick="editCustomer(this.getAttribute('data-customer-id'))">
 | 
			
		||||
                <i class="icon-base bx bx-edit icon-sm"></i>
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
                      {% empty %}
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td colspan="7" class="text-center py-4">
 | 
			
		||||
                <div class="d-flex flex-column align-items-center">
 | 
			
		||||
                  <i class="bx bx-user-x bx-lg text-muted mb-2"></i>
 | 
			
		||||
                  <span class="text-muted">هیچ کاربری یافت نشد</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Modal to add/edit record -->
 | 
			
		||||
<div class="offcanvas offcanvas-end" id="add-new-record">
 | 
			
		||||
  <div class="offcanvas-header border-bottom">
 | 
			
		||||
    <h5 class="offcanvas-title" id="exampleModalLabel">افزودن کاربر جدید</h5>
 | 
			
		||||
    <button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="offcanvas-body flex-grow-1">
 | 
			
		||||
    <form class="add-new-record pt-0 row g-2" id="form-add-new-record" method="post" enctype="multipart/form-data">
 | 
			
		||||
      {% csrf_token %}
 | 
			
		||||
      <input type="hidden" id="customer-id" name="customer_id" value="">
 | 
			
		||||
      
 | 
			
		||||
      <!-- User Information -->
 | 
			
		||||
     
 | 
			
		||||
      <div class="col-sm-6">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-user"></i></span>
 | 
			
		||||
          {{ form.first_name }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.first_name.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.first_name.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-6">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-user"></i></span>
 | 
			
		||||
          {{ form.last_name }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.last_name.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.last_name.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-6">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.phone_number_1.id_for_label }}">{{ form.phone_number_1.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-phone"></i></span>
 | 
			
		||||
          {{ form.phone_number_1 }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.phone_number_1.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.phone_number_1.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-6">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.phone_number_2.id_for_label }}">{{ form.phone_number_2.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-phone"></i></span>
 | 
			
		||||
          {{ form.phone_number_2 }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.phone_number_2.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.phone_number_2.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-12">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.national_code.id_for_label }}">{{ form.national_code.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-id-card"></i></span>
 | 
			
		||||
          {{ form.national_code }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.national_code.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.national_code.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-12">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.card_number.id_for_label }}">{{ form.card_number.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-credit-card"></i></span>
 | 
			
		||||
          {{ form.card_number }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.card_number.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.card_number.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-12">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.account_number.id_for_label }}">{{ form.account_number.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-credit-card"></i></span>
 | 
			
		||||
          {{ form.account_number }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.account_number.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.account_number.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-12">
 | 
			
		||||
        <label class="form-label fw-bold" for="{{ form.address.id_for_label }}">{{ form.address.label }}</label>
 | 
			
		||||
        <div class="input-group input-group-merge">
 | 
			
		||||
          <span class="input-group-text"><i class="bx bx-map"></i></span>
 | 
			
		||||
          {{ form.address }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if form.address.errors %}
 | 
			
		||||
          <div class="invalid-feedback d-block">{{ form.address.errors.0 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="col-sm-12">
 | 
			
		||||
        <button type="submit" class="btn btn-primary data-submit me-sm-3 me-1">ذخیره</button>
 | 
			
		||||
        <button type="reset" class="btn btn-outline-secondary" data-bs-dismiss="offcanvas">انصراف</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Delete Confirmation Modal -->
 | 
			
		||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
 | 
			
		||||
  <div class="modal-dialog">
 | 
			
		||||
    <div class="modal-content">
 | 
			
		||||
      <div class="modal-header">
 | 
			
		||||
        <h5 class="modal-title" id="deleteConfirmModalLabel">تایید حذف</h5>
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-body">
 | 
			
		||||
        <p id="deleteConfirmText">آیا از حذف این کاربر اطمینان دارید؟</p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
			
		||||
        <button type="button" class="btn btn-danger" id="confirmDeleteBtn">حذف</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block script %}
 | 
			
		||||
<!-- DataTables JS -->
 | 
			
		||||
<script src="{% static 'assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js' %}"></script>
 | 
			
		||||
<!-- Persian DataTable Language -->
 | 
			
		||||
<script src="{% static 'assets/js/persian-datatable.js' %}"></script>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  $(document).ready(function() {
 | 
			
		||||
    // Initialize DataTable with Persian language
 | 
			
		||||
    $('#customers-table').DataTable({
 | 
			
		||||
      pageLength: 10,
 | 
			
		||||
      lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
 | 
			
		||||
      order: [[0, 'asc']],
 | 
			
		||||
      responsive: true,
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Handle form submission
 | 
			
		||||
    $('#form-add-new-record').on('submit', function(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      
 | 
			
		||||
      const form = this;
 | 
			
		||||
      const formData = new FormData(form);
 | 
			
		||||
      const customerId = $('#customer-id').val();
 | 
			
		||||
      
 | 
			
		||||
      // Determine URL based on whether we're editing or adding
 | 
			
		||||
      const url = customerId ? '{% url "accounts:edit_customer_ajax" 0 %}'.replace('0', customerId) : '{% url "accounts:add_customer_ajax" %}';
 | 
			
		||||
      
 | 
			
		||||
      // Show loading state
 | 
			
		||||
      const submitBtn = $(form).find('button[type="submit"]');
 | 
			
		||||
      const originalText = submitBtn.text();
 | 
			
		||||
      submitBtn.prop('disabled', true).text('در حال ذخیره...');
 | 
			
		||||
      
 | 
			
		||||
      $.ajax({
 | 
			
		||||
        url: url,
 | 
			
		||||
        type: 'POST',
 | 
			
		||||
        data: formData,
 | 
			
		||||
        processData: false,
 | 
			
		||||
        contentType: false,
 | 
			
		||||
        success: function(response) {
 | 
			
		||||
          if (response.success) {
 | 
			
		||||
            // Show success message
 | 
			
		||||
            showToast(response.message, 'success');
 | 
			
		||||
            
 | 
			
		||||
            // Close offcanvas and reset form
 | 
			
		||||
            $('#add-new-record').offcanvas('hide');
 | 
			
		||||
            form.reset();
 | 
			
		||||
            
 | 
			
		||||
            // Reload page to show new customer
 | 
			
		||||
            setTimeout(function() {
 | 
			
		||||
              location.reload();
 | 
			
		||||
            }, 1500);
 | 
			
		||||
          } else {
 | 
			
		||||
            // Show error message
 | 
			
		||||
            showToast(response.message, 'danger');
 | 
			
		||||
            
 | 
			
		||||
            // Show form errors if any
 | 
			
		||||
            if (response.errors) {
 | 
			
		||||
              Object.keys(response.errors).forEach(function(field) {
 | 
			
		||||
                const errorMsg = response.errors[field][0];
 | 
			
		||||
                const fieldElement = $('[name="' + field + '"]');
 | 
			
		||||
                fieldElement.addClass('is-invalid');
 | 
			
		||||
                fieldElement.siblings('.invalid-feedback').remove();
 | 
			
		||||
                fieldElement.after('<div class="invalid-feedback d-block">' + errorMsg + '</div>');
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        error: function(xhr, status, error) {
 | 
			
		||||
          // Show error message
 | 
			
		||||
          showToast('خطا در ارتباط با سرور', 'danger');
 | 
			
		||||
        },
 | 
			
		||||
        complete: function() {
 | 
			
		||||
          // Reset button state
 | 
			
		||||
          submitBtn.prop('disabled', false).text(originalText);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Reset form when offcanvas is hidden
 | 
			
		||||
    $('#add-new-record').on('hidden.bs.offcanvas', function() {
 | 
			
		||||
      const form = $('#form-add-new-record')[0];
 | 
			
		||||
      form.reset();
 | 
			
		||||
      
 | 
			
		||||
      // Reset form for adding new customer
 | 
			
		||||
      $('#customer-id').val('');
 | 
			
		||||
      $('#exampleModalLabel').text('افزودن کاربر جدید');
 | 
			
		||||
      $('.data-submit').text('ذخیره');
 | 
			
		||||
      
 | 
			
		||||
      // Clear validation errors
 | 
			
		||||
      $('.is-invalid').removeClass('is-invalid');
 | 
			
		||||
      $('.invalid-feedback').remove();
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Clear validation errors when user starts typing
 | 
			
		||||
    $('input, textarea').on('input', function() {
 | 
			
		||||
      $(this).removeClass('is-invalid');
 | 
			
		||||
      $(this).siblings('.invalid-feedback').remove();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // Customer functions
 | 
			
		||||
  function viewCustomer(id) {
 | 
			
		||||
    // Implement view functionality
 | 
			
		||||
    console.log('View customer:', id);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  function editCustomer(id) {
 | 
			
		||||
    // Load customer data and open edit form
 | 
			
		||||
    $.ajax({
 | 
			
		||||
      url: '{% url "accounts:get_customer_data" 0 %}'.replace('0', id),
 | 
			
		||||
      type: 'GET',
 | 
			
		||||
      success: function(response) {
 | 
			
		||||
        if (response.success) {
 | 
			
		||||
          const customer = response.customer;
 | 
			
		||||
          
 | 
			
		||||
          // Fill form with customer data
 | 
			
		||||
          const fieldsMap = {
 | 
			
		||||
            'customer-id': customer.id,
 | 
			
		||||
            'id_first_name': customer.first_name,
 | 
			
		||||
            'id_last_name': customer.last_name,
 | 
			
		||||
            'id_phone_number_1': customer.phone_number_1,
 | 
			
		||||
            'id_phone_number_2': customer.phone_number_2,
 | 
			
		||||
            'id_national_code': customer.national_code,
 | 
			
		||||
            'id_card_number': customer.card_number,
 | 
			
		||||
            'id_account_number': customer.account_number,
 | 
			
		||||
            'id_address': customer.address
 | 
			
		||||
          };
 | 
			
		||||
          
 | 
			
		||||
          // Loop through fields for easier maintenance
 | 
			
		||||
          Object.keys(fieldsMap).forEach(function(fieldId) {
 | 
			
		||||
            $('#' + fieldId).val(fieldsMap[fieldId] || '');
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Update modal title and button
 | 
			
		||||
          $('#exampleModalLabel').text('ویرایش کاربر');
 | 
			
		||||
          $('.data-submit').text('ویرایش');
 | 
			
		||||
          
 | 
			
		||||
          // Open modal
 | 
			
		||||
          $('#add-new-record').offcanvas('show');
 | 
			
		||||
        } else {
 | 
			
		||||
          showToast('خطا در بارگذاری اطلاعات کاربر', 'danger');
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      error: function() {
 | 
			
		||||
        showToast('خطا در ارتباط با سرور', 'danger');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  function deleteCustomer(id, name) {
 | 
			
		||||
    // Set modal content
 | 
			
		||||
    document.getElementById('deleteConfirmText').textContent = `آیا از حذف کاربر "${name}" اطمینان دارید؟`;
 | 
			
		||||
    
 | 
			
		||||
    // Show modal
 | 
			
		||||
    const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
 | 
			
		||||
    modal.show();
 | 
			
		||||
    
 | 
			
		||||
    // Handle confirm button click
 | 
			
		||||
    document.getElementById('confirmDeleteBtn').onclick = function() {
 | 
			
		||||
      // Implement delete functionality
 | 
			
		||||
      console.log('Delete customer:', id);
 | 
			
		||||
      showToast('کاربر با موفقیت حذف شد.', 'success');
 | 
			
		||||
      modal.hide();
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  function prepareAddForm() {
 | 
			
		||||
    // Reset form for adding new customer
 | 
			
		||||
    const form = $('#form-add-new-record')[0];
 | 
			
		||||
    form.reset();
 | 
			
		||||
    $('#customer-id').val('');
 | 
			
		||||
    $('#exampleModalLabel').text('افزودن کاربر جدید');
 | 
			
		||||
    $('.data-submit').text('ذخیره');
 | 
			
		||||
    
 | 
			
		||||
    // Clear validation errors
 | 
			
		||||
    $('.is-invalid').removeClass('is-invalid');
 | 
			
		||||
    $('.invalid-feedback').remove();
 | 
			
		||||
    
 | 
			
		||||
    // Open modal
 | 
			
		||||
    $('#add-new-record').offcanvas('show');
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										1072
									
								
								accounts/templates/accounts/dashboard.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										2849
									
								
								accounts/templates/accounts/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										137
									
								
								accounts/templates/accounts/login.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,137 @@
 | 
			
		|||
{% extends '_base.html' %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block layout %}
 | 
			
		||||
layout-wide customizer-hide
 | 
			
		||||
{% endblock layout %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
  ورود
 | 
			
		||||
{% endblock title %}
 | 
			
		||||
 | 
			
		||||
{% block style %}
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/pages/page-auth.css' %}">
 | 
			
		||||
{% endblock style %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-xxl">
 | 
			
		||||
  <div class="authentication-wrapper authentication-basic container-p-y">
 | 
			
		||||
    <div class="authentication-inner">
 | 
			
		||||
      <!-- Register -->
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <!-- Logo -->
 | 
			
		||||
          <div class="app-brand justify-content-center">
 | 
			
		||||
            <a href="index.html" class="app-brand-link gap-2">
 | 
			
		||||
              <span class="app-brand-logo demo">
 | 
			
		||||
 | 
			
		||||
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <path d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z" id="path-1"></path>
 | 
			
		||||
    <path d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z" id="path-3"></path>
 | 
			
		||||
    <path d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z" id="path-4"></path>
 | 
			
		||||
    <path d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z" id="path-5"></path>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
 | 
			
		||||
    <g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
 | 
			
		||||
      <g id="Icon" transform="translate(27.000000, 15.000000)">
 | 
			
		||||
        <g id="Mask" transform="translate(0.000000, 8.000000)">
 | 
			
		||||
          <mask id="mask-2" fill="white">
 | 
			
		||||
            <use xlink:href="#path-1"></use>
 | 
			
		||||
          </mask>
 | 
			
		||||
          <use fill="#696cff" xlink:href="#path-1"></use>
 | 
			
		||||
          <g id="Path-3" mask="url(#mask-2)">
 | 
			
		||||
            <use fill="#696cff" xlink:href="#path-3"></use>
 | 
			
		||||
            <use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
 | 
			
		||||
          </g>
 | 
			
		||||
          <g id="Path-4" mask="url(#mask-2)">
 | 
			
		||||
            <use fill="#696cff" xlink:href="#path-4"></use>
 | 
			
		||||
            <use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
 | 
			
		||||
          </g>
 | 
			
		||||
        </g>
 | 
			
		||||
        <g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
 | 
			
		||||
          <use fill="#696cff" xlink:href="#path-5"></use>
 | 
			
		||||
          <use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
 | 
			
		||||
</span>
 | 
			
		||||
              <span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- /Logo -->
 | 
			
		||||
          <h4 class="mb-2">Welcome to Sneat! 👋</h4>
 | 
			
		||||
          <p class="mb-4">Please sign-in to your account and start the adventure</p>
 | 
			
		||||
 | 
			
		||||
          <form id="formAuthentication" class="mb-3 fv-plugins-bootstrap5 fv-plugins-framework"  method="post" novalidate="novalidate">
 | 
			
		||||
            {% csrf_token %}
 | 
			
		||||
            <div class="mb-3 fv-plugins-icon-container">
 | 
			
		||||
              <label for="email" class="form-label">Email or Username</label>
 | 
			
		||||
              <input type="text" class="form-control" id="email" name="username" placeholder="Enter your email or username" autofocus="">
 | 
			
		||||
            <div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div></div>
 | 
			
		||||
            <div class="mb-3 form-password-toggle fv-plugins-icon-container">
 | 
			
		||||
              <div class="d-flex justify-content-between">
 | 
			
		||||
                <label class="form-label" for="password">Password</label>
 | 
			
		||||
                <a href="auth-forgot-password-basic.html">
 | 
			
		||||
                  <small>Forgot Password?</small>
 | 
			
		||||
                </a>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="input-group input-group-merge has-validation">
 | 
			
		||||
                <input type="password" id="password" class="form-control" name="password" placeholder="············" aria-describedby="password">
 | 
			
		||||
                <span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
 | 
			
		||||
              </div><div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="mb-3">
 | 
			
		||||
              <div class="form-check">
 | 
			
		||||
                <input class="form-check-input" type="checkbox" id="remember-me">
 | 
			
		||||
                <label class="form-check-label" for="remember-me">
 | 
			
		||||
                  Remember Me
 | 
			
		||||
                </label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="mb-3">
 | 
			
		||||
              <button class="btn btn-primary d-grid w-100" type="submit">Sign in</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          <input type="hidden"></form>
 | 
			
		||||
 | 
			
		||||
          <p class="text-center">
 | 
			
		||||
            <span>New on our platform?</span>
 | 
			
		||||
            <a href="auth-register-basic.html">
 | 
			
		||||
              <span>Create an account</span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <div class="divider my-4">
 | 
			
		||||
            <div class="divider-text">or</div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="d-flex justify-content-center">
 | 
			
		||||
            <a href="javascript:;" class="btn btn-icon btn-label-facebook me-3">
 | 
			
		||||
              <i class="tf-icons bx bxl-facebook"></i>
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <a href="javascript:;" class="btn btn-icon btn-label-google-plus me-3">
 | 
			
		||||
              <i class="tf-icons bx bxl-google-plus"></i>
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <a href="javascript:;" class="btn btn-icon btn-label-twitter">
 | 
			
		||||
              <i class="tf-icons bx bxl-twitter"></i>
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- /Register -->
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script src="{% static 'assets/js/pages-auth.js' %}"></script>
 | 
			
		||||
{% endblock scripts %}
 | 
			
		||||
							
								
								
									
										60
									
								
								accounts/templatetags/accounts_tags.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
from django import template
 | 
			
		||||
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def has_item(list, item):
 | 
			
		||||
    return item in list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _has_profile(user):
 | 
			
		||||
    return hasattr(user, "profile") and user.profile
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def has_role(user, role_slugs):
 | 
			
		||||
    return _has_profile(user) and len(user.profile.roles.filter(slug__in=role_slugs.split(","))) > 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_admin(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.ADMIN)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_manager(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.MANAGER)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_accountant(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.ACCOUNTANT)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_broker(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.BROKER)
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_installer(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.INSTALLER)
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_regional_water_authority(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.REGIONAL_WATER_AUTHORITY)
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_water_resource_manager(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.WATER_RESOURCE_MANAGER)
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_headquarter(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.HEADQUARTER)
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_customer(user):
 | 
			
		||||
    return _has_profile(user) and user.profile.has_role(UserRoles.CUSTOMER)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								accounts/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										13
									
								
								accounts/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from accounts.views import login_view, dashboard, customer_list, add_customer_ajax, edit_customer_ajax, get_customer_data
 | 
			
		||||
 | 
			
		||||
app_name = "accounts"
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login/', login_view, name='login'),
 | 
			
		||||
    path('dashboard/', dashboard, name='dashboard'),
 | 
			
		||||
    path('customers/', customer_list, name='customer_list'),
 | 
			
		||||
    path('customers/add/', add_customer_ajax, name='add_customer_ajax'),
 | 
			
		||||
    path('customers/<int:customer_id>/data/', get_customer_data, name='get_customer_data'),
 | 
			
		||||
    path('customers/<int:customer_id>/edit/', edit_customer_ajax, name='edit_customer_ajax'),
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										163
									
								
								accounts/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,163 @@
 | 
			
		|||
from django.contrib import messages
 | 
			
		||||
from django.contrib.auth import login, authenticate
 | 
			
		||||
from django.shortcuts import render, redirect, get_object_or_404
 | 
			
		||||
from django.http import JsonResponse
 | 
			
		||||
from django.views.decorators.http import require_POST, require_GET
 | 
			
		||||
from django.views.decorators.csrf import csrf_exempt
 | 
			
		||||
from django import forms
 | 
			
		||||
 | 
			
		||||
from accounts.models import Profile
 | 
			
		||||
from accounts.forms import CustomerForm
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Create your views here.
 | 
			
		||||
def login_view(request):
 | 
			
		||||
    """
 | 
			
		||||
    renders login page and authenticating user POST requests
 | 
			
		||||
    to log user in
 | 
			
		||||
    """
 | 
			
		||||
    if request.method == "POST":
 | 
			
		||||
        username = request.POST.get("username")
 | 
			
		||||
        password = request.POST.get("password")
 | 
			
		||||
        user = authenticate(request, username=username, password=password)
 | 
			
		||||
        # if user is not None:
 | 
			
		||||
        #     login(request, user)
 | 
			
		||||
        #     if user.profile.has_none_of([UserRoles.MANAGER]):
 | 
			
		||||
        #         return redirect("dashboard:dashboard")
 | 
			
		||||
        #     else:
 | 
			
		||||
        #         return redirect("dashboard:admin_dashboard")
 | 
			
		||||
        # else:
 | 
			
		||||
        #     messages.error(request, "کاربری با این مشخصات یافت نشد!")
 | 
			
		||||
        #     return redirect("accounts:login")
 | 
			
		||||
 | 
			
		||||
    return render(request, "accounts/login.html")
 | 
			
		||||
 | 
			
		||||
def dashboard(request):
 | 
			
		||||
    return render(request, "accounts/dashboard.html")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def customer_list(request):
 | 
			
		||||
    # Get all profiles that have customer role
 | 
			
		||||
    customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
 | 
			
		||||
    
 | 
			
		||||
    form = CustomerForm()
 | 
			
		||||
    return render(request, "accounts/customer_list.html", {
 | 
			
		||||
        "customers": customers,
 | 
			
		||||
        "form": form
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
def add_customer_ajax(request):
 | 
			
		||||
    """AJAX endpoint for adding customers"""
 | 
			
		||||
    form = CustomerForm(request.POST, request.FILES)
 | 
			
		||||
    form.request = request  # Pass request to form
 | 
			
		||||
    if form.is_valid():
 | 
			
		||||
        try:
 | 
			
		||||
            customer = form.save()
 | 
			
		||||
            return JsonResponse({
 | 
			
		||||
                'success': True,
 | 
			
		||||
                'message': 'مشترک با موفقیت اضافه شد!',
 | 
			
		||||
                'customer': {
 | 
			
		||||
                    'id': customer.id,
 | 
			
		||||
                    'name': customer.user.get_full_name(),
 | 
			
		||||
                    'username': customer.user.username,
 | 
			
		||||
                    'phone': customer.phone_number_1 or 'ثبت نشده',
 | 
			
		||||
                    'national_code': customer.national_code or 'ثبت نشده',
 | 
			
		||||
                    'status': 'تکمیل شده' if customer.is_completed else 'ناقص'
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        except forms.ValidationError as e:
 | 
			
		||||
            return JsonResponse({
 | 
			
		||||
                'success': False,
 | 
			
		||||
                'message': str(e)
 | 
			
		||||
            })
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return JsonResponse({
 | 
			
		||||
                'success': False,
 | 
			
		||||
                'message': f'خطا در ذخیره مشترک: {str(e)}'
 | 
			
		||||
            })
 | 
			
		||||
    else:
 | 
			
		||||
        return JsonResponse({
 | 
			
		||||
            'success': False,
 | 
			
		||||
            'message': 'خطا در اعتبارسنجی فرم',
 | 
			
		||||
            'errors': form.errors
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
def edit_customer_ajax(request, customer_id):
 | 
			
		||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
			
		||||
    form = CustomerForm(request.POST, request.FILES, instance=customer)
 | 
			
		||||
    form.request = request  # Pass request to form
 | 
			
		||||
    if form.is_valid():
 | 
			
		||||
        try:
 | 
			
		||||
            customer = form.save()
 | 
			
		||||
            return JsonResponse({
 | 
			
		||||
                'success': True,
 | 
			
		||||
                'message': 'مشترک با موفقیت ویرایش شد!',
 | 
			
		||||
                'customer': {
 | 
			
		||||
                    'id': customer.id,
 | 
			
		||||
                    'name': customer.user.get_full_name(),
 | 
			
		||||
                    'username': customer.user.username,
 | 
			
		||||
                    'phone': customer.phone_number_1 or 'ثبت نشده',
 | 
			
		||||
                    'national_code': customer.national_code or 'ثبت نشده',
 | 
			
		||||
                    'status': 'تکمیل شده' if customer.is_completed else 'ناقص'
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        except forms.ValidationError as e:
 | 
			
		||||
            return JsonResponse({
 | 
			
		||||
                'success': False,
 | 
			
		||||
                'message': str(e)
 | 
			
		||||
            })
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return JsonResponse({
 | 
			
		||||
                'success': False,
 | 
			
		||||
                'message': f'خطا در ویرایش مشترک: {str(e)}'
 | 
			
		||||
            })
 | 
			
		||||
    else:
 | 
			
		||||
        return JsonResponse({
 | 
			
		||||
            'success': False,
 | 
			
		||||
            'message': 'خطا در اعتبارسنجی فرم',
 | 
			
		||||
            'errors': form.errors
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
@require_GET
 | 
			
		||||
def get_customer_data(request, customer_id):
 | 
			
		||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
			
		||||
    
 | 
			
		||||
    # Create form with existing customer data
 | 
			
		||||
    form = CustomerForm(instance=customer, initial={
 | 
			
		||||
        'first_name': customer.user.first_name,
 | 
			
		||||
        'last_name': customer.user.last_name,
 | 
			
		||||
    })
 | 
			
		||||
    
 | 
			
		||||
    # Render form fields as HTML
 | 
			
		||||
    form_html = {
 | 
			
		||||
        'first_name': str(form['first_name']),
 | 
			
		||||
        'last_name': str(form['last_name']),
 | 
			
		||||
        'phone_number_1': str(form['phone_number_1']),
 | 
			
		||||
        'phone_number_2': str(form['phone_number_2']),
 | 
			
		||||
        'national_code': str(form['national_code']),
 | 
			
		||||
        'card_number': str(form['card_number']),
 | 
			
		||||
        'account_number': str(form['account_number']),
 | 
			
		||||
        'address': str(form['address']),
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return JsonResponse({
 | 
			
		||||
        'success': True,
 | 
			
		||||
        'customer': {
 | 
			
		||||
            'id': customer.id,
 | 
			
		||||
            'first_name': customer.user.first_name,
 | 
			
		||||
            'last_name': customer.user.last_name,
 | 
			
		||||
            'phone_number_1': customer.phone_number_1 or '',
 | 
			
		||||
            'phone_number_2': customer.phone_number_2 or '',
 | 
			
		||||
            'national_code': customer.national_code or '',
 | 
			
		||||
            'card_number': customer.card_number or '',
 | 
			
		||||
            'account_number': customer.account_number or '',
 | 
			
		||||
            'address': customer.address or ''
 | 
			
		||||
        },
 | 
			
		||||
        'form_html': form_html
 | 
			
		||||
    })
 | 
			
		||||
							
								
								
									
										0
									
								
								common/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3
									
								
								common/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
# Register your models here.
 | 
			
		||||
							
								
								
									
										6
									
								
								common/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommonConfig(AppConfig):
 | 
			
		||||
    default_auto_field = 'django.db.models.BigAutoField'
 | 
			
		||||
    name = 'common'
 | 
			
		||||
							
								
								
									
										13
									
								
								common/consts.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
from enum import Enum
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserRoles(Enum):
 | 
			
		||||
    ADMIN = "adm" #ادمین
 | 
			
		||||
    CUSTOMER = "cus" #مشترک
 | 
			
		||||
    MANAGER = "mng" #مدیر
 | 
			
		||||
    ACCOUNTANT = "aco" #حسابدار
 | 
			
		||||
    BROKER = "bro" # کارگزار - پیشخان
 | 
			
		||||
    INSTALLER = "inst" # نصاب
 | 
			
		||||
    REGIONAL_WATER_AUTHORITY = "rwa" # کارشناس امور
 | 
			
		||||
    WATER_RESOURCE_MANAGER = "wrm" # مدیر منابع آب
 | 
			
		||||
    HEADQUARTER = "hdq" # ستاد آب منطقهای
 | 
			
		||||
							
								
								
									
										9
									
								
								common/context_processors.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
def current_year(request):
 | 
			
		||||
    """
 | 
			
		||||
    Context processor to add current year to all templates
 | 
			
		||||
    """
 | 
			
		||||
    return {
 | 
			
		||||
        'current_year': datetime.now().year
 | 
			
		||||
    } 
 | 
			
		||||
							
								
								
									
										67
									
								
								common/decorators.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
from functools import wraps
 | 
			
		||||
 | 
			
		||||
from django.http import JsonResponse, HttpResponse
 | 
			
		||||
from django.shortcuts import redirect
 | 
			
		||||
 | 
			
		||||
from extensions.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def require_ajax(view_func):
 | 
			
		||||
    @wraps(view_func)
 | 
			
		||||
    def _wrapped_view(request, *args, **kwargs):
 | 
			
		||||
        if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
 | 
			
		||||
            return JsonResponse({'error': 'Only AJAX requests are allowed.'}, status=400)
 | 
			
		||||
        return view_func(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    return _wrapped_view
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def allowed_roles(allowed_roles: list[UserRoles]):
 | 
			
		||||
    """
 | 
			
		||||
    @param allowed_roles must not be empty
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def decorator(views_func):
 | 
			
		||||
        def wrapper_func(request, *args, **kwargs):
 | 
			
		||||
            roles = [role.slug for role in request.user.profile.roles.all()]
 | 
			
		||||
            allowed_role_names = [role.value for role in allowed_roles]
 | 
			
		||||
            if any(item in roles for item in allowed_role_names):
 | 
			
		||||
                return views_func(request, *args, **kwargs)
 | 
			
		||||
            else:
 | 
			
		||||
                return HttpResponse('you are not allow', status=401)
 | 
			
		||||
 | 
			
		||||
        return wrapper_func
 | 
			
		||||
 | 
			
		||||
    return decorator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def profile_complete_needed(view_func):
 | 
			
		||||
    @wraps(view_func)
 | 
			
		||||
    def _wrapped_view(request, *args, **kwargs):
 | 
			
		||||
        if not request.user.profile or not request.user.profile.is_completed:
 | 
			
		||||
            return redirect("accounts:profile")
 | 
			
		||||
        return view_func(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    return _wrapped_view
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def superuser_required(views_func):
 | 
			
		||||
    def wrapper_func(request, *args, **kwargs):
 | 
			
		||||
        user = request.user
 | 
			
		||||
        if user.is_superuser:
 | 
			
		||||
            return views_func(request, *args, **kwargs)
 | 
			
		||||
        else:
 | 
			
		||||
            return redirect('dashboard:vodDashboard')
 | 
			
		||||
 | 
			
		||||
    return wrapper_func
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def staffuser_required(views_func):
 | 
			
		||||
    def wrapper_func(request, *args, **kwargs):
 | 
			
		||||
        user = request.user
 | 
			
		||||
        if user.is_staff:
 | 
			
		||||
            return views_func(request, *args, **kwargs)
 | 
			
		||||
        else:
 | 
			
		||||
            return redirect('dashboard:vodDashboard')
 | 
			
		||||
 | 
			
		||||
    return wrapper_func
 | 
			
		||||
							
								
								
									
										0
									
								
								common/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										128
									
								
								common/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,128 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
from _helpers.utils import jalali_converter, generate_unique_slug
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ObjectsQuerySet(models.QuerySet):
 | 
			
		||||
    def deleted_objects(self):
 | 
			
		||||
        return self.filter(is_deleted=True)
 | 
			
		||||
 | 
			
		||||
    def available_objects(self):
 | 
			
		||||
        return self.filter(is_deleted=False)
 | 
			
		||||
 | 
			
		||||
    def active_objects(self):
 | 
			
		||||
        return self.filter(is_active=True)
 | 
			
		||||
 | 
			
		||||
    def inactive_objects(self):
 | 
			
		||||
        return self.filter(is_active=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseModel(models.Model):
 | 
			
		||||
    created = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
 | 
			
		||||
    updated = models.DateTimeField(auto_now=True, verbose_name="تاریخ بروزرسانی")
 | 
			
		||||
    is_active = models.BooleanField(default=True, verbose_name="فعال")
 | 
			
		||||
    is_deleted = models.BooleanField(default=False, verbose_name="حذف شده")
 | 
			
		||||
    deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ حذف")
 | 
			
		||||
 | 
			
		||||
    objects = ObjectsQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.is_deleted:
 | 
			
		||||
            self.is_active = False
 | 
			
		||||
        super(BaseModel, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def delete(self, *args, **kwargs):
 | 
			
		||||
        self.is_deleted = True
 | 
			
		||||
        self.deleted_at = timezone.now()
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    def hard_delete(self):
 | 
			
		||||
        super().delete()
 | 
			
		||||
 | 
			
		||||
    def jcreated(self):
 | 
			
		||||
        return jalali_converter(self.created)
 | 
			
		||||
 | 
			
		||||
    jcreated.short_description = "تاریخ ایجاد"
 | 
			
		||||
 | 
			
		||||
    def jcreated_date(self):
 | 
			
		||||
        return self.jcreated().split(',')[0]
 | 
			
		||||
 | 
			
		||||
    jcreated_date.short_description = "تاریخ ایجاد"
 | 
			
		||||
 | 
			
		||||
    def jupdated(self):
 | 
			
		||||
        return jalali_converter(self.updated)
 | 
			
		||||
 | 
			
		||||
    jupdated.short_description = "تاریخ بروزرسانی"
 | 
			
		||||
 | 
			
		||||
    def jupdated_date(self):
 | 
			
		||||
        return self.jupdated().split(',')[0]
 | 
			
		||||
 | 
			
		||||
    jupdated_date.short_description = "تاریخ بروزرسانی"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SluggedModel(BaseModel):
 | 
			
		||||
    slug = models.SlugField(max_length=100, unique=True, verbose_name="اسلاگ")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if not self.slug:
 | 
			
		||||
            if hasattr(self, 'name'):
 | 
			
		||||
                self.slug = generate_unique_slug(self.name)
 | 
			
		||||
            else:
 | 
			
		||||
                self.slug = generate_unique_slug(str(self.created))
 | 
			
		||||
        super(BaseModel, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SelfParentModel(BaseModel):
 | 
			
		||||
    parent = models.ForeignKey('self', related_name='children',
 | 
			
		||||
                               on_delete=models.SET_NULL, null=True, blank=True, default=None,
 | 
			
		||||
                               verbose_name='زیرشاخه')
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NameSlugModel(SluggedModel):
 | 
			
		||||
    name = models.CharField(max_length=100, verbose_name="نام")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NameSlugAdminModel(admin.ModelAdmin):
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SelfParentNameSlugModel(SelfParentModel, NameSlugModel):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TagModel(SelfParentModel, NameSlugModel):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
        ordering = ['parent__id', 'name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TagAdminModel(admin.ModelAdmin):
 | 
			
		||||
    fields = ['name', 'slug', 'parent', 'is_active']
 | 
			
		||||
    list_display = ['name', 'slug', 'parent', 'is_active', 'is_deleted', 'jcreated']
 | 
			
		||||
    list_filter = ['is_active', 'slug', 'parent', 'is_deleted']
 | 
			
		||||
    search_fields = ['name', 'slug', 'parent__name']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
							
								
								
									
										3
									
								
								common/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										3
									
								
								common/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.shortcuts import render
 | 
			
		||||
 | 
			
		||||
# Create your views here.
 | 
			
		||||
							
								
								
									
										0
									
								
								invoices/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										65
									
								
								invoices/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
from simple_history.admin import SimpleHistoryAdmin
 | 
			
		||||
from django.utils.html import format_html
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from .models import Item, Quote, QuoteItem, Invoice, InvoiceItem, Payment
 | 
			
		||||
 | 
			
		||||
@admin.register(Item)
 | 
			
		||||
class ItemAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['name', 'unit_price', 'default_quantity', 'is_default_in_quotes', 'is_active', 'created_by']
 | 
			
		||||
    list_filter = ['is_default_in_quotes', 'is_active', 'created_by']
 | 
			
		||||
    search_fields = ['name', 'description']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
    readonly_fields = ['deleted_at', 'created', 'updated']
 | 
			
		||||
 | 
			
		||||
class QuoteItemInline(admin.TabularInline):
 | 
			
		||||
    model = QuoteItem
 | 
			
		||||
    extra = 1
 | 
			
		||||
    fields = ['item', 'quantity', 'unit_price', 'total_price', 'notes']
 | 
			
		||||
 | 
			
		||||
@admin.register(Quote)
 | 
			
		||||
class QuoteAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['name', 'process_instance', 'customer', 'status_display', 'total_amount', 'final_amount', 'valid_until', 'created_by']
 | 
			
		||||
    list_filter = ['status', 'created', 'valid_until', 'process_instance__process']
 | 
			
		||||
    search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
    readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount']
 | 
			
		||||
    inlines = [QuoteItemInline]
 | 
			
		||||
    ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def status_display(self, obj):
 | 
			
		||||
        return mark_safe(obj.get_status_display_with_color())
 | 
			
		||||
    status_display.short_description = "وضعیت"
 | 
			
		||||
 | 
			
		||||
class InvoiceItemInline(admin.TabularInline):
 | 
			
		||||
    model = InvoiceItem
 | 
			
		||||
    extra = 1
 | 
			
		||||
    fields = ['item', 'quantity', 'unit_price', 'total_price', 'notes']
 | 
			
		||||
 | 
			
		||||
class PaymentInline(admin.TabularInline):
 | 
			
		||||
    model = Payment
 | 
			
		||||
    extra = 1
 | 
			
		||||
    fields = ['amount', 'payment_method', 'reference_number', 'payment_date', 'notes']
 | 
			
		||||
    readonly_fields = ['created_by', 'created']
 | 
			
		||||
 | 
			
		||||
@admin.register(Invoice)
 | 
			
		||||
class InvoiceAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount', 'remaining_amount', 'due_date']
 | 
			
		||||
    list_filter = ['status', 'created', 'due_date', 'process_instance__process']
 | 
			
		||||
    search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
    readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount', 'remaining_amount']
 | 
			
		||||
    inlines = [InvoiceItemInline, PaymentInline]
 | 
			
		||||
    ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def status_display(self, obj):
 | 
			
		||||
        return mark_safe(obj.get_status_display_with_color())
 | 
			
		||||
    status_display.short_description = "وضعیت"
 | 
			
		||||
 | 
			
		||||
@admin.register(Payment)
 | 
			
		||||
class PaymentAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['invoice', 'amount', 'payment_method', 'payment_date', 'created_by']
 | 
			
		||||
    list_filter = ['payment_method', 'payment_date', 'created_by']
 | 
			
		||||
    search_fields = ['invoice__name', 'reference_number', 'notes']
 | 
			
		||||
    readonly_fields = ['created']
 | 
			
		||||
    ordering = ['-payment_date']
 | 
			
		||||
							
								
								
									
										7
									
								
								invoices/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvoicesConfig(AppConfig):
 | 
			
		||||
    default_auto_field = 'django.db.models.BigAutoField'
 | 
			
		||||
    name = 'invoices'
 | 
			
		||||
    verbose_name = 'فاکتورها'
 | 
			
		||||
							
								
								
									
										363
									
								
								invoices/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,363 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import simple_history.models
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('processes', '0001_initial'),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalItem',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('description', models.TextField(blank=True, verbose_name='توضیحات')),
 | 
			
		||||
                ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
 | 
			
		||||
                ('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')),
 | 
			
		||||
                ('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical آیتم',
 | 
			
		||||
                'verbose_name_plural': 'historical آیتم\u200cها',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalQuote',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('accepted', 'تایید شده'), ('rejected', 'رد شده'), ('expired', 'منقضی شده')], default='draft', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
 | 
			
		||||
                ('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
 | 
			
		||||
                ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
 | 
			
		||||
                ('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('valid_until', models.DateField(verbose_name='معتبر تا')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical پیش\u200cفاکتور',
 | 
			
		||||
                'verbose_name_plural': 'historical پیش\u200cفاکتورها',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Invoice',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('paid', 'پرداخت شده'), ('partially_paid', 'نیمه پرداخت شده'), ('overdue', 'معوق'), ('cancelled', 'لغو شده')], default='draft', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
 | 
			
		||||
                ('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
 | 
			
		||||
                ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
 | 
			
		||||
                ('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
 | 
			
		||||
                ('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ پرداخت شده')),
 | 
			
		||||
                ('remaining_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ باقی\u200cمانده')),
 | 
			
		||||
                ('due_date', models.DateField(verbose_name='تاریخ سررسید')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_invoices', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
 | 
			
		||||
                ('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processinstance', verbose_name='نمونه فرآیند')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'فاکتور',
 | 
			
		||||
                'verbose_name_plural': 'فاکتورها',
 | 
			
		||||
                'ordering': ['-created'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalPayment',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')),
 | 
			
		||||
                ('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')),
 | 
			
		||||
                ('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')),
 | 
			
		||||
                ('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('invoice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.invoice', verbose_name='فاکتور')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical پرداخت',
 | 
			
		||||
                'verbose_name_plural': 'historical پرداخت\u200cها',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Item',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('description', models.TextField(blank=True, verbose_name='توضیحات')),
 | 
			
		||||
                ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
 | 
			
		||||
                ('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')),
 | 
			
		||||
                ('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
 | 
			
		||||
                ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'آیتم',
 | 
			
		||||
                'verbose_name_plural': 'آیتم\u200cها',
 | 
			
		||||
                'ordering': ['name'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='InvoiceItem',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
 | 
			
		||||
                ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
 | 
			
		||||
                ('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.invoice', verbose_name='فاکتور')),
 | 
			
		||||
                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item', verbose_name='آیتم')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'آیتم فاکتور',
 | 
			
		||||
                'verbose_name_plural': 'آیتم\u200cهای فاکتور',
 | 
			
		||||
                'ordering': ['invoice', 'item__name'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalInvoiceItem',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
 | 
			
		||||
                ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
 | 
			
		||||
                ('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('invoice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.invoice', verbose_name='فاکتور')),
 | 
			
		||||
                ('item', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.item', verbose_name='آیتم')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical آیتم فاکتور',
 | 
			
		||||
                'verbose_name_plural': 'historical آیتم\u200cهای فاکتور',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Payment',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')),
 | 
			
		||||
                ('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')),
 | 
			
		||||
                ('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')),
 | 
			
		||||
                ('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')),
 | 
			
		||||
                ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice', verbose_name='فاکتور')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'پرداخت',
 | 
			
		||||
                'verbose_name_plural': 'پرداخت\u200cها',
 | 
			
		||||
                'ordering': ['-payment_date'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Quote',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('accepted', 'تایید شده'), ('rejected', 'رد شده'), ('expired', 'منقضی شده')], default='draft', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
 | 
			
		||||
                ('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
 | 
			
		||||
                ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
 | 
			
		||||
                ('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('valid_until', models.DateField(verbose_name='معتبر تا')),
 | 
			
		||||
                ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_quotes', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
 | 
			
		||||
                ('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processinstance', verbose_name='نمونه فرآیند')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'پیش\u200cفاکتور',
 | 
			
		||||
                'verbose_name_plural': 'پیش\u200cفاکتورها',
 | 
			
		||||
                'ordering': ['-created'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='invoice',
 | 
			
		||||
            name='quote',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='invoices.quote', verbose_name='پیش\u200cفاکتور مربوطه'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalQuoteItem',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
 | 
			
		||||
                ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
 | 
			
		||||
                ('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('item', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.item', verbose_name='آیتم')),
 | 
			
		||||
                ('quote', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.quote', verbose_name='پیش\u200cفاکتور')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical آیتم پیش\u200cفاکتور',
 | 
			
		||||
                'verbose_name_plural': 'historical آیتم\u200cهای پیش\u200cفاکتور',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalInvoice',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('paid', 'پرداخت شده'), ('partially_paid', 'نیمه پرداخت شده'), ('overdue', 'معوق'), ('cancelled', 'لغو شده')], default='draft', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
 | 
			
		||||
                ('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
 | 
			
		||||
                ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
 | 
			
		||||
                ('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
 | 
			
		||||
                ('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ پرداخت شده')),
 | 
			
		||||
                ('remaining_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ باقی\u200cمانده')),
 | 
			
		||||
                ('due_date', models.DateField(verbose_name='تاریخ سررسید')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
 | 
			
		||||
                ('quote', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.quote', verbose_name='پیش\u200cفاکتور مربوطه')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical فاکتور',
 | 
			
		||||
                'verbose_name_plural': 'historical فاکتورها',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='QuoteItem',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
 | 
			
		||||
                ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
 | 
			
		||||
                ('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item', verbose_name='آیتم')),
 | 
			
		||||
                ('quote', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.quote', verbose_name='پیش\u200cفاکتور')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'آیتم پیش\u200cفاکتور',
 | 
			
		||||
                'verbose_name_plural': 'آیتم\u200cهای پیش\u200cفاکتور',
 | 
			
		||||
                'ordering': ['quote', 'item__name'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								invoices/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										331
									
								
								invoices/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,331 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from common.models import NameSlugModel, BaseModel
 | 
			
		||||
from simple_history.models import HistoricalRecords
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
 | 
			
		||||
User = get_user_model()
 | 
			
		||||
 | 
			
		||||
class Item(NameSlugModel):
 | 
			
		||||
    """مدل آیتمهای پیشفرض"""
 | 
			
		||||
    description = models.TextField(verbose_name="توضیحات", blank=True)
 | 
			
		||||
    unit_price = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        verbose_name="قیمت واحد"
 | 
			
		||||
    )
 | 
			
		||||
    default_quantity = models.PositiveIntegerField(
 | 
			
		||||
        default=1, 
 | 
			
		||||
        verbose_name="تعداد پیشفرض"
 | 
			
		||||
    )
 | 
			
		||||
    is_default_in_quotes = models.BooleanField(
 | 
			
		||||
        default=False, 
 | 
			
		||||
        verbose_name="پیشفرض در پیشفاکتورها",
 | 
			
		||||
        help_text="این آیتم به صورت پیشفرض در همه پیشفاکتورها قرار میگیرد"
 | 
			
		||||
    )
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ایجاد کننده")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "آیتم"
 | 
			
		||||
        verbose_name_plural = "آیتمها"
 | 
			
		||||
        ordering = ['name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.name} - {self.unit_price} تومان"
 | 
			
		||||
 | 
			
		||||
class Quote(NameSlugModel):
 | 
			
		||||
    """مدل پیشفاکتور"""
 | 
			
		||||
    process_instance = models.ForeignKey(
 | 
			
		||||
        'processes.ProcessInstance', 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="نمونه فرآیند"
 | 
			
		||||
    )
 | 
			
		||||
    customer = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="مشترک")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('draft', 'پیشنویس'),
 | 
			
		||||
            ('sent', 'ارسال شده'),
 | 
			
		||||
            ('accepted', 'تایید شده'),
 | 
			
		||||
            ('rejected', 'رد شده'),
 | 
			
		||||
            ('expired', 'منقضی شده'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='draft',
 | 
			
		||||
        verbose_name="وضعیت"
 | 
			
		||||
    )
 | 
			
		||||
    total_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ کل"
 | 
			
		||||
    )
 | 
			
		||||
    discount_percent = models.DecimalField(
 | 
			
		||||
        max_digits=5, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="درصد تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    discount_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    final_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ نهایی"
 | 
			
		||||
    )
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    valid_until = models.DateField(verbose_name="معتبر تا")
 | 
			
		||||
    created_by = models.ForeignKey(
 | 
			
		||||
        User, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="ایجاد کننده",
 | 
			
		||||
        related_name='created_quotes'
 | 
			
		||||
    )
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "پیشفاکتور"
 | 
			
		||||
        verbose_name_plural = "پیشفاکتورها"
 | 
			
		||||
        ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"پیشفاکتور {self.name} - {self.customer.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def calculate_totals(self):
 | 
			
		||||
        """محاسبه مبالغ کل"""
 | 
			
		||||
        total = sum(item.total_price for item in self.items.all())
 | 
			
		||||
        self.total_amount = total
 | 
			
		||||
        
 | 
			
		||||
        # محاسبه تخفیف
 | 
			
		||||
        if self.discount_percent > 0:
 | 
			
		||||
            self.discount_amount = (total * self.discount_percent) / 100
 | 
			
		||||
        else:
 | 
			
		||||
            self.discount_amount = 0
 | 
			
		||||
        
 | 
			
		||||
        self.final_amount = self.total_amount - self.discount_amount
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    def get_status_display_with_color(self):
 | 
			
		||||
        """نمایش وضعیت با رنگ"""
 | 
			
		||||
        status_colors = {
 | 
			
		||||
            'draft': 'secondary',
 | 
			
		||||
            'sent': 'primary',
 | 
			
		||||
            'accepted': 'success',
 | 
			
		||||
            'rejected': 'danger',
 | 
			
		||||
            'expired': 'warning',
 | 
			
		||||
        }
 | 
			
		||||
        color = status_colors.get(self.status, 'secondary')
 | 
			
		||||
        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
			
		||||
 | 
			
		||||
class QuoteItem(BaseModel):
 | 
			
		||||
    """مدل آیتمهای پیشفاکتور"""
 | 
			
		||||
    quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیشفاکتور")
 | 
			
		||||
    item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name="آیتم")
 | 
			
		||||
    quantity = models.PositiveIntegerField(verbose_name="تعداد")
 | 
			
		||||
    unit_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت واحد")
 | 
			
		||||
    total_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت کل")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "آیتم پیشفاکتور"
 | 
			
		||||
        verbose_name_plural = "آیتمهای پیشفاکتور"
 | 
			
		||||
        ordering = ['quote', 'item__name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.item.name} - {self.quantity} عدد"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """محاسبه قیمت کل"""
 | 
			
		||||
        self.total_price = self.quantity * self.unit_price
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
        # بروزرسانی مبالغ پیشفاکتور
 | 
			
		||||
        self.quote.calculate_totals()
 | 
			
		||||
 | 
			
		||||
class Invoice(NameSlugModel):
 | 
			
		||||
    """مدل فاکتور نهایی"""
 | 
			
		||||
    quote = models.ForeignKey(
 | 
			
		||||
        Quote, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="پیشفاکتور مربوطه",
 | 
			
		||||
        null=True, 
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    process_instance = models.ForeignKey(
 | 
			
		||||
        'processes.ProcessInstance', 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="نمونه فرآیند"
 | 
			
		||||
    )
 | 
			
		||||
    customer = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="مشترک")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('draft', 'پیشنویس'),
 | 
			
		||||
            ('sent', 'ارسال شده'),
 | 
			
		||||
            ('paid', 'پرداخت شده'),
 | 
			
		||||
            ('partially_paid', 'نیمه پرداخت شده'),
 | 
			
		||||
            ('overdue', 'معوق'),
 | 
			
		||||
            ('cancelled', 'لغو شده'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='draft',
 | 
			
		||||
        verbose_name="وضعیت"
 | 
			
		||||
    )
 | 
			
		||||
    total_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ کل"
 | 
			
		||||
    )
 | 
			
		||||
    discount_percent = models.DecimalField(
 | 
			
		||||
        max_digits=5, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="درصد تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    discount_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    final_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ نهایی"
 | 
			
		||||
    )
 | 
			
		||||
    paid_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ پرداخت شده"
 | 
			
		||||
    )
 | 
			
		||||
    remaining_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ باقیمانده"
 | 
			
		||||
    )
 | 
			
		||||
    due_date = models.DateField(verbose_name="تاریخ سررسید")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    created_by = models.ForeignKey(
 | 
			
		||||
        User, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="ایجاد کننده",
 | 
			
		||||
        related_name='created_invoices'
 | 
			
		||||
    )
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "فاکتور"
 | 
			
		||||
        verbose_name_plural = "فاکتورها"
 | 
			
		||||
        ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"فاکتور {self.name} - {self.customer.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def calculate_totals(self):
 | 
			
		||||
        """محاسبه مبالغ کل"""
 | 
			
		||||
        total = sum(item.total_price for item in self.items.all())
 | 
			
		||||
        self.total_amount = total
 | 
			
		||||
        
 | 
			
		||||
        # محاسبه تخفیف
 | 
			
		||||
        if self.discount_percent > 0:
 | 
			
		||||
            self.discount_amount = (total * self.discount_percent) / 100
 | 
			
		||||
        else:
 | 
			
		||||
            self.discount_amount = 0
 | 
			
		||||
        
 | 
			
		||||
        self.final_amount = self.total_amount - self.discount_amount
 | 
			
		||||
        self.remaining_amount = self.final_amount - self.paid_amount
 | 
			
		||||
        
 | 
			
		||||
        # بروزرسانی وضعیت
 | 
			
		||||
        if self.remaining_amount <= 0:
 | 
			
		||||
            self.status = 'paid'
 | 
			
		||||
        elif self.paid_amount > 0:
 | 
			
		||||
            self.status = 'partially_paid'
 | 
			
		||||
        else:
 | 
			
		||||
            self.status = 'sent'
 | 
			
		||||
        
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    def get_status_display_with_color(self):
 | 
			
		||||
        """نمایش وضعیت با رنگ"""
 | 
			
		||||
        status_colors = {
 | 
			
		||||
            'draft': 'secondary',
 | 
			
		||||
            'sent': 'primary',
 | 
			
		||||
            'paid': 'success',
 | 
			
		||||
            'partially_paid': 'info',
 | 
			
		||||
            'overdue': 'danger',
 | 
			
		||||
            'cancelled': 'warning',
 | 
			
		||||
        }
 | 
			
		||||
        color = status_colors.get(self.status, 'secondary')
 | 
			
		||||
        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
			
		||||
 | 
			
		||||
class InvoiceItem(BaseModel):
 | 
			
		||||
    """مدل آیتمهای فاکتور"""
 | 
			
		||||
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='items', verbose_name="فاکتور")
 | 
			
		||||
    item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name="آیتم")
 | 
			
		||||
    quantity = models.PositiveIntegerField(verbose_name="تعداد")
 | 
			
		||||
    unit_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت واحد")
 | 
			
		||||
    total_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت کل")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "آیتم فاکتور"
 | 
			
		||||
        verbose_name_plural = "آیتمهای فاکتور"
 | 
			
		||||
        ordering = ['invoice', 'item__name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.item.name} - {self.quantity} عدد"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """محاسبه قیمت کل"""
 | 
			
		||||
        self.total_price = self.quantity * self.unit_price
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
        # بروزرسانی مبالغ فاکتور
 | 
			
		||||
        self.invoice.calculate_totals()
 | 
			
		||||
 | 
			
		||||
class Payment(BaseModel):
 | 
			
		||||
    """مدل پرداختها"""
 | 
			
		||||
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments', verbose_name="فاکتور")
 | 
			
		||||
    amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="مبلغ پرداخت")
 | 
			
		||||
    payment_method = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('cash', 'نقدی'),
 | 
			
		||||
            ('bank_transfer', 'انتقال بانکی'),
 | 
			
		||||
            ('check', 'چک'),
 | 
			
		||||
            ('card', 'کارت بانکی'),
 | 
			
		||||
            ('other', 'سایر'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='cash',
 | 
			
		||||
        verbose_name="روش پرداخت"
 | 
			
		||||
    )
 | 
			
		||||
    reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True)
 | 
			
		||||
    payment_date = models.DateField(verbose_name="تاریخ پرداخت")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ثبت کننده")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "پرداخت"
 | 
			
		||||
        verbose_name_plural = "پرداختها"
 | 
			
		||||
        ordering = ['-payment_date']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"پرداخت {self.amount} تومان - {self.invoice.name}"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """بروزرسانی مبالغ فاکتور"""
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
        # بروزرسانی مبلغ پرداخت شده فاکتور
 | 
			
		||||
        total_paid = sum(payment.amount for payment in self.invoice.payments.all())
 | 
			
		||||
        self.invoice.paid_amount = total_paid
 | 
			
		||||
        self.invoice.calculate_totals()
 | 
			
		||||
							
								
								
									
										3
									
								
								invoices/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										3
									
								
								invoices/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.shortcuts import render
 | 
			
		||||
 | 
			
		||||
# Create your views here.
 | 
			
		||||
							
								
								
									
										0
									
								
								locations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										35
									
								
								locations/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
from .models import City, County, Affairs, Broker
 | 
			
		||||
 | 
			
		||||
# Register your models here.
 | 
			
		||||
 | 
			
		||||
@admin.register(City)
 | 
			
		||||
class CityAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'slug']
 | 
			
		||||
    search_fields = ['name']
 | 
			
		||||
    readonly_fields = ['deleted_at']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
 | 
			
		||||
@admin.register(County)
 | 
			
		||||
class CountyAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'city', 'slug']
 | 
			
		||||
    list_filter = ['city']
 | 
			
		||||
    search_fields = ['name', 'city__name']
 | 
			
		||||
    readonly_fields = ['deleted_at']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
 | 
			
		||||
@admin.register(Affairs)
 | 
			
		||||
class AffairsAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'county', 'slug']
 | 
			
		||||
    list_filter = ['county__city', 'county']
 | 
			
		||||
    search_fields = ['name', 'county__name', 'county__city__name']
 | 
			
		||||
    readonly_fields = ['deleted_at']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
 | 
			
		||||
@admin.register(Broker)
 | 
			
		||||
class BrokerAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'affairs', 'slug']
 | 
			
		||||
    list_filter = ['affairs__county__city', 'affairs__county', 'affairs']
 | 
			
		||||
    search_fields = ['name', 'affairs__name', 'affairs__county__name']
 | 
			
		||||
    readonly_fields = ['deleted_at']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
							
								
								
									
										7
									
								
								locations/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationsConfig(AppConfig):
 | 
			
		||||
    default_auto_field = 'django.db.models.BigAutoField'
 | 
			
		||||
    name = 'locations'
 | 
			
		||||
    verbose_name = "مکانها"
 | 
			
		||||
							
								
								
									
										90
									
								
								locations/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,90 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Affairs',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'امور',
 | 
			
		||||
                'verbose_name_plural': 'امورها',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='City',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'شهر',
 | 
			
		||||
                'verbose_name_plural': 'شهرها',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Broker',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('affairs', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.affairs', verbose_name='امور')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'کارگزار',
 | 
			
		||||
                'verbose_name_plural': 'کارگزارها',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='County',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.city', verbose_name='شهرستان')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'شهرستان',
 | 
			
		||||
                'verbose_name_plural': 'شهرستان\u200cها',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='affairs',
 | 
			
		||||
            name='county',
 | 
			
		||||
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.county', verbose_name='شهرستان'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								locations/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										41
									
								
								locations/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from common.models import NameSlugModel
 | 
			
		||||
 | 
			
		||||
# Create your models here.
 | 
			
		||||
class City(NameSlugModel):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "شهر"
 | 
			
		||||
        verbose_name_plural = "شهرها"
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
class County(NameSlugModel):
 | 
			
		||||
    city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name="شهرستان")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "شهرستان"
 | 
			
		||||
        verbose_name_plural = "شهرستانها"
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
    
 | 
			
		||||
class Affairs(NameSlugModel):
 | 
			
		||||
    county = models.ForeignKey(County, on_delete=models.CASCADE, verbose_name="شهرستان")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "امور"
 | 
			
		||||
        verbose_name_plural = "امورها"
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
class Broker(NameSlugModel):
 | 
			
		||||
    affairs = models.ForeignKey(Affairs, on_delete=models.CASCADE, verbose_name="امور")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "کارگزار"
 | 
			
		||||
        verbose_name_plural = "کارگزارها"
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
							
								
								
									
										3
									
								
								locations/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										3
									
								
								locations/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.shortcuts import render
 | 
			
		||||
 | 
			
		||||
# Create your views here.
 | 
			
		||||
							
								
								
									
										22
									
								
								manage.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
"""Django's command-line utility for administrative tasks."""
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    """Run administrative tasks."""
 | 
			
		||||
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', '_base.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()
 | 
			
		||||
							
								
								
									
										0
									
								
								processes/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										107
									
								
								processes/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,107 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
from simple_history.admin import SimpleHistoryAdmin
 | 
			
		||||
from django.utils.html import format_html
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepRevision
 | 
			
		||||
 | 
			
		||||
@admin.register(Process)
 | 
			
		||||
class ProcessAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['name', 'is_active', 'created_by', 'created', 'steps_count']
 | 
			
		||||
    list_filter = ['is_active', 'created']
 | 
			
		||||
    search_fields = ['name', 'description', 'created_by__username']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
    readonly_fields = ['deleted_at', 'created', 'updated']
 | 
			
		||||
    
 | 
			
		||||
    def steps_count(self, obj):
 | 
			
		||||
        return obj.steps.count()
 | 
			
		||||
    steps_count.short_description = "تعداد مراحل"
 | 
			
		||||
 | 
			
		||||
@admin.register(ProcessStep)
 | 
			
		||||
class ProcessStepAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['name', 'process', 'order', 'is_required', 'dependencies_display', 'blocks_previous', 'can_go_back']
 | 
			
		||||
    list_filter = ['process', 'is_required', 'blocks_previous', 'can_go_back']
 | 
			
		||||
    search_fields = ['name', 'process__name', 'description']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
    readonly_fields = ['deleted_at']
 | 
			
		||||
    ordering = ['process', 'order']
 | 
			
		||||
 | 
			
		||||
    def dependencies_display(self, obj):
 | 
			
		||||
        dependencies = obj.get_dependencies()
 | 
			
		||||
        if dependencies:
 | 
			
		||||
            dependency_names = ProcessStep.objects.filter(id__in=dependencies).values_list('name', flat=True)
 | 
			
		||||
            return ", ".join(dependency_names)
 | 
			
		||||
        return "بدون وابستگی"
 | 
			
		||||
    dependencies_display.short_description = "وابسته به"
 | 
			
		||||
 | 
			
		||||
@admin.register(StepDependency)
 | 
			
		||||
class StepDependencyAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['dependent_step', 'dependency_step', 'process_display', 'created_at']
 | 
			
		||||
    list_filter = ['dependent_step__process', 'created_at']
 | 
			
		||||
    search_fields = ['dependent_step__name', 'dependency_step__name']
 | 
			
		||||
    ordering = ['dependent_step__process', 'dependent_step__order']
 | 
			
		||||
 | 
			
		||||
    def process_display(self, obj):
 | 
			
		||||
        return obj.dependent_step.process.name
 | 
			
		||||
    process_display.short_description = "فرآیند"
 | 
			
		||||
 | 
			
		||||
@admin.register(ProcessInstance)
 | 
			
		||||
class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['name', 'process', 'requester', 'current_step', 'status', 'started_at', 'progress_display']
 | 
			
		||||
    list_filter = ['process', 'status', 'started_at']
 | 
			
		||||
    search_fields = ['name', 'process__name', 'requester__username', 'requester__first_name']
 | 
			
		||||
    readonly_fields = ['deleted_at', 'started_at', 'completed_at']
 | 
			
		||||
    ordering = ['-started_at']
 | 
			
		||||
 | 
			
		||||
    def progress_display(self, obj):
 | 
			
		||||
        total_steps = obj.process.steps.count()
 | 
			
		||||
        completed_steps = obj.step_instances.filter(status='completed').count()
 | 
			
		||||
        percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
 | 
			
		||||
        percentage_int = int(percentage)
 | 
			
		||||
        return format_html(
 | 
			
		||||
            '<div class="progress" style="width: 100px;"><div class="progress-bar" style="width: {}%">{}/{} ({}%)</div></div>',
 | 
			
		||||
            percentage_int, completed_steps, total_steps, percentage_int
 | 
			
		||||
        )
 | 
			
		||||
    progress_display.short_description = "پیشرفت"
 | 
			
		||||
 | 
			
		||||
@admin.register(StepInstance)
 | 
			
		||||
class StepInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['process_instance', 'step', 'assigned_to', 'status_display', 'rejection_count', 'started_at', 'completed_at']
 | 
			
		||||
    list_filter = ['status', 'step__process', 'started_at']
 | 
			
		||||
    search_fields = ['process_instance__name', 'step__name', 'assigned_to__username']
 | 
			
		||||
    readonly_fields = ['started_at', 'completed_at']
 | 
			
		||||
    ordering = ['process_instance', 'step__order']
 | 
			
		||||
 | 
			
		||||
    def status_display(self, obj):
 | 
			
		||||
        return mark_safe(obj.get_status_display_with_color())
 | 
			
		||||
    status_display.short_description = "وضعیت"
 | 
			
		||||
 | 
			
		||||
    def rejection_count(self, obj):
 | 
			
		||||
        count = obj.get_rejection_count()
 | 
			
		||||
        if count > 0:
 | 
			
		||||
            return format_html('<span class="badge bg-danger">{}</span>', count)
 | 
			
		||||
        return format_html('<span class="badge bg-success">0</span>')
 | 
			
		||||
    rejection_count.short_description = "تعداد رد شدنها"
 | 
			
		||||
 | 
			
		||||
@admin.register(StepRejection)
 | 
			
		||||
class StepRejectionAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at']
 | 
			
		||||
    list_filter = ['rejected_by', 'created_at', 'step_instance__step__process']
 | 
			
		||||
    search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason']
 | 
			
		||||
    readonly_fields = ['created_at']
 | 
			
		||||
    ordering = ['-created_at']
 | 
			
		||||
 | 
			
		||||
    def reason_short(self, obj):
 | 
			
		||||
        return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason
 | 
			
		||||
    reason_short.short_description = "دلیل رد شدن"
 | 
			
		||||
 | 
			
		||||
@admin.register(StepRevision)
 | 
			
		||||
class StepRevisionAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['step_instance', 'rejection', 'revised_by', 'changes_short', 'created_at']
 | 
			
		||||
    list_filter = ['revised_by', 'created_at', 'step_instance__step__process']
 | 
			
		||||
    search_fields = ['step_instance__step__name', 'revised_by__username', 'changes_description']
 | 
			
		||||
    readonly_fields = ['created_at']
 | 
			
		||||
    ordering = ['-created_at']
 | 
			
		||||
 | 
			
		||||
    def changes_short(self, obj):
 | 
			
		||||
        return obj.changes_description[:50] + "..." if len(obj.changes_description) > 50 else obj.changes_description
 | 
			
		||||
    changes_short.short_description = "تغییرات"
 | 
			
		||||
							
								
								
									
										7
									
								
								processes/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProcessesConfig(AppConfig):
 | 
			
		||||
    default_auto_field = 'django.db.models.BigAutoField'
 | 
			
		||||
    name = 'processes'
 | 
			
		||||
    verbose_name = 'فرآیندها'
 | 
			
		||||
							
								
								
									
										19
									
								
								processes/forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
from django import forms
 | 
			
		||||
from .models import ProcessInstance, StepInstance
 | 
			
		||||
 | 
			
		||||
class ProcessInstanceForm(forms.ModelForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ProcessInstance
 | 
			
		||||
        fields = ['name']
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'name': forms.TextInput(attrs={'class': 'form-control'})
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
class StepInstanceForm(forms.ModelForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = StepInstance
 | 
			
		||||
        fields = ['status', 'notes']
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'status': forms.Select(attrs={'class': 'form-control'}),
 | 
			
		||||
            'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3})
 | 
			
		||||
        } 
 | 
			
		||||
							
								
								
									
										314
									
								
								processes/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,314 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import simple_history.models
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalProcess',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('description', models.TextField(blank=True, verbose_name='توضیحات')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical فرآیند',
 | 
			
		||||
                'verbose_name_plural': 'historical فرآیندها',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Process',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('description', models.TextField(blank=True, verbose_name='توضیحات')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'فرآیند',
 | 
			
		||||
                'verbose_name_plural': 'فرآیندها',
 | 
			
		||||
                'ordering': ['-created'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalProcessStep',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('order', models.PositiveIntegerField(verbose_name='ترتیب')),
 | 
			
		||||
                ('description', models.TextField(blank=True, verbose_name='توضیحات')),
 | 
			
		||||
                ('is_required', models.BooleanField(default=True, verbose_name='اجباری')),
 | 
			
		||||
                ('estimated_duration', models.PositiveIntegerField(blank=True, null=True, verbose_name='مدت زمان تخمینی (روز)')),
 | 
			
		||||
                ('blocks_previous', models.BooleanField(default=False, help_text='اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش می\u200cشوند', verbose_name='مسدود کننده مراحل قبلی')),
 | 
			
		||||
                ('can_go_back', models.BooleanField(default=True, help_text='آیا می\u200cتوان به مراحل قبلی بازگشت', verbose_name='قابل بازگشت')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('process', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.process', verbose_name='فرآیند')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical مرحله فرآیند',
 | 
			
		||||
                'verbose_name_plural': 'historical مراحل فرآیند',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='ProcessStep',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('order', models.PositiveIntegerField(verbose_name='ترتیب')),
 | 
			
		||||
                ('description', models.TextField(blank=True, verbose_name='توضیحات')),
 | 
			
		||||
                ('is_required', models.BooleanField(default=True, verbose_name='اجباری')),
 | 
			
		||||
                ('estimated_duration', models.PositiveIntegerField(blank=True, null=True, verbose_name='مدت زمان تخمینی (روز)')),
 | 
			
		||||
                ('blocks_previous', models.BooleanField(default=False, help_text='اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش می\u200cشوند', verbose_name='مسدود کننده مراحل قبلی')),
 | 
			
		||||
                ('can_go_back', models.BooleanField(default=True, help_text='آیا می\u200cتوان به مراحل قبلی بازگشت', verbose_name='قابل بازگشت')),
 | 
			
		||||
                ('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='processes.process', verbose_name='فرآیند')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'مرحله فرآیند',
 | 
			
		||||
                'verbose_name_plural': 'مراحل فرآیند',
 | 
			
		||||
                'ordering': ['process', 'order'],
 | 
			
		||||
                'unique_together': {('process', 'order')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='ProcessInstance',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('started_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ شروع')),
 | 
			
		||||
                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
			
		||||
                ('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='processes.process', verbose_name='فرآیند')),
 | 
			
		||||
                ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
 | 
			
		||||
                ('current_step', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='processes.processstep', verbose_name='مرحله فعلی')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'نمونه فرآیند',
 | 
			
		||||
                'verbose_name_plural': 'نمونه\u200cهای فرآیند',
 | 
			
		||||
                'ordering': ['-started_at'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalStepInstance',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('started_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ شروع')),
 | 
			
		||||
                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('assigned_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='واگذار شده به')),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
 | 
			
		||||
                ('step', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processstep', verbose_name='مرحله')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical نمونه مرحله',
 | 
			
		||||
                'verbose_name_plural': 'historical نمونه\u200cهای مرحله',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalProcessInstance',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
			
		||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
			
		||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
			
		||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
			
		||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
			
		||||
                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('started_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ شروع')),
 | 
			
		||||
                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('requester', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
 | 
			
		||||
                ('process', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.process', verbose_name='فرآیند')),
 | 
			
		||||
                ('current_step', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processstep', verbose_name='مرحله فعلی')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical نمونه فرآیند',
 | 
			
		||||
                'verbose_name_plural': 'historical نمونه\u200cهای فرآیند',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='StepInstance',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
			
		||||
                ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
 | 
			
		||||
                ('started_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ شروع')),
 | 
			
		||||
                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
			
		||||
                ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='واگذار شده به')),
 | 
			
		||||
                ('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_instances', to='processes.processinstance', verbose_name='نمونه فرآیند')),
 | 
			
		||||
                ('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processstep', verbose_name='مرحله')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'نمونه مرحله',
 | 
			
		||||
                'verbose_name_plural': 'نمونه\u200cهای مرحله',
 | 
			
		||||
                'ordering': ['process_instance', 'step__order'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalStepRejection',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('reason', models.TextField(help_text='توضیح کامل دلیل رد شدن', verbose_name='دلیل رد شدن')),
 | 
			
		||||
                ('instructions', models.TextField(blank=True, help_text='دستورالعمل\u200cهایی برای اصلاح مرحله', verbose_name='دستورالعمل\u200cهای اصلاح')),
 | 
			
		||||
                ('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ رد شدن')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('rejected_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='رد کننده')),
 | 
			
		||||
                ('step_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.stepinstance', verbose_name='نمونه مرحله')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical رد شدن مرحله',
 | 
			
		||||
                'verbose_name_plural': 'historical رد شدن\u200cهای مراحل',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='StepRejection',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('reason', models.TextField(help_text='توضیح کامل دلیل رد شدن', verbose_name='دلیل رد شدن')),
 | 
			
		||||
                ('instructions', models.TextField(blank=True, help_text='دستورالعمل\u200cهایی برای اصلاح مرحله', verbose_name='دستورالعمل\u200cهای اصلاح')),
 | 
			
		||||
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ رد شدن')),
 | 
			
		||||
                ('rejected_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_rejections', to=settings.AUTH_USER_MODEL, verbose_name='رد کننده')),
 | 
			
		||||
                ('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rejections', to='processes.stepinstance', verbose_name='نمونه مرحله')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'رد شدن مرحله',
 | 
			
		||||
                'verbose_name_plural': 'رد شدن\u200cهای مراحل',
 | 
			
		||||
                'ordering': ['-created_at'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='HistoricalStepRevision',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
			
		||||
                ('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
 | 
			
		||||
                ('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ اصلاح')),
 | 
			
		||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
			
		||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
			
		||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
			
		||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
			
		||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('revised_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='اصلاح کننده')),
 | 
			
		||||
                ('step_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.stepinstance', verbose_name='نمونه مرحله')),
 | 
			
		||||
                ('rejection', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.steprejection', verbose_name='رد شدن مربوطه')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'historical بازبینی مرحله',
 | 
			
		||||
                'verbose_name_plural': 'historical بازبینی\u200cهای مراحل',
 | 
			
		||||
                'ordering': ('-history_date', '-history_id'),
 | 
			
		||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
			
		||||
            },
 | 
			
		||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='StepRevision',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
 | 
			
		||||
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ اصلاح')),
 | 
			
		||||
                ('rejection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.steprejection', verbose_name='رد شدن مربوطه')),
 | 
			
		||||
                ('revised_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_revisions', to=settings.AUTH_USER_MODEL, verbose_name='اصلاح کننده')),
 | 
			
		||||
                ('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.stepinstance', verbose_name='نمونه مرحله')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'بازبینی مرحله',
 | 
			
		||||
                'verbose_name_plural': 'بازبینی\u200cهای مراحل',
 | 
			
		||||
                'ordering': ['-created_at'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='StepDependency',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
 | 
			
		||||
                ('dependency_step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependents', to='processes.processstep', verbose_name='مرحله مورد نیاز')),
 | 
			
		||||
                ('dependent_step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='processes.processstep', verbose_name='مرحله وابسته')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'وابستگی مرحله',
 | 
			
		||||
                'verbose_name_plural': 'وابستگی\u200cهای مراحل',
 | 
			
		||||
                'ordering': ['dependent_step__order', 'dependency_step__order'],
 | 
			
		||||
                'unique_together': {('dependent_step', 'dependency_step')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								processes/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										293
									
								
								processes/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,293 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from common.models import NameSlugModel
 | 
			
		||||
from simple_history.models import HistoricalRecords
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
 | 
			
		||||
User = get_user_model()
 | 
			
		||||
 | 
			
		||||
class Process(NameSlugModel):
 | 
			
		||||
    """مدل فرآیند اصلی"""
 | 
			
		||||
    description = models.TextField(verbose_name="توضیحات", blank=True)
 | 
			
		||||
    is_active = models.BooleanField(default=True, verbose_name="فعال")
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ایجاد کننده")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "فرآیند"
 | 
			
		||||
        verbose_name_plural = "فرآیندها"
 | 
			
		||||
        ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
class ProcessStep(NameSlugModel):
 | 
			
		||||
    """مدل مراحل فرآیند"""
 | 
			
		||||
    process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='steps', verbose_name="فرآیند")
 | 
			
		||||
    order = models.PositiveIntegerField(verbose_name="ترتیب")
 | 
			
		||||
    description = models.TextField(verbose_name="توضیحات", blank=True)
 | 
			
		||||
    is_required = models.BooleanField(default=True, verbose_name="اجباری")
 | 
			
		||||
    estimated_duration = models.PositiveIntegerField(verbose_name="مدت زمان تخمینی (روز)", null=True, blank=True)
 | 
			
		||||
    
 | 
			
		||||
    # فیلدهای جدید برای کنترل وابستگیها
 | 
			
		||||
    blocks_previous = models.BooleanField(
 | 
			
		||||
        default=False, 
 | 
			
		||||
        verbose_name="مسدود کننده مراحل قبلی",
 | 
			
		||||
        help_text="اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش میشوند"
 | 
			
		||||
    )
 | 
			
		||||
    can_go_back = models.BooleanField(
 | 
			
		||||
        default=True, 
 | 
			
		||||
        verbose_name="قابل بازگشت",
 | 
			
		||||
        help_text="آیا میتوان به مراحل قبلی بازگشت"
 | 
			
		||||
    )
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "مرحله فرآیند"
 | 
			
		||||
        verbose_name_plural = "مراحل فرآیند"
 | 
			
		||||
        ordering = ['process', 'order']
 | 
			
		||||
        unique_together = ['process', 'order']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.process.name} - {self.name}"
 | 
			
		||||
 | 
			
		||||
    def get_dependencies(self):
 | 
			
		||||
        """دریافت مراحل وابسته"""
 | 
			
		||||
        return StepDependency.objects.filter(dependent_step=self).values_list('dependency_step', flat=True)
 | 
			
		||||
 | 
			
		||||
    def get_dependents(self):
 | 
			
		||||
        """دریافت مراحلی که به این مرحله وابسته هستند"""
 | 
			
		||||
        return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True)
 | 
			
		||||
 | 
			
		||||
class StepDependency(models.Model):
 | 
			
		||||
    """مدل وابستگی بین مراحل"""
 | 
			
		||||
    dependent_step = models.ForeignKey(
 | 
			
		||||
        ProcessStep, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        related_name='dependencies',
 | 
			
		||||
        verbose_name="مرحله وابسته"
 | 
			
		||||
    )
 | 
			
		||||
    dependency_step = models.ForeignKey(
 | 
			
		||||
        ProcessStep, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        related_name='dependents',
 | 
			
		||||
        verbose_name="مرحله مورد نیاز"
 | 
			
		||||
    )
 | 
			
		||||
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "وابستگی مرحله"
 | 
			
		||||
        verbose_name_plural = "وابستگیهای مراحل"
 | 
			
		||||
        unique_together = ['dependent_step', 'dependency_step']
 | 
			
		||||
        ordering = ['dependent_step__order', 'dependency_step__order']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.dependent_step} ← {self.dependency_step}"
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        """اعتبارسنجی مدل"""
 | 
			
		||||
        if self.dependent_step == self.dependency_step:
 | 
			
		||||
            raise ValidationError("مرحله نمیتواند به خودش وابسته باشد")
 | 
			
		||||
        
 | 
			
		||||
        if self.dependent_step.process != self.dependency_step.process:
 | 
			
		||||
            raise ValidationError("مراحل باید از یک فرآیند باشند")
 | 
			
		||||
        
 | 
			
		||||
        if self.dependent_step.order <= self.dependency_step.order:
 | 
			
		||||
            raise ValidationError("مرحله وابسته باید بعد از مرحله مورد نیاز باشد")
 | 
			
		||||
 | 
			
		||||
class ProcessInstance(NameSlugModel):
 | 
			
		||||
    """مدل نمونه فرآیند (برای هر درخواست)"""
 | 
			
		||||
    process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='instances', verbose_name="فرآیند")
 | 
			
		||||
    requester = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="درخواست کننده")
 | 
			
		||||
    current_step = models.ForeignKey('ProcessStep', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="مرحله فعلی")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('pending', 'در انتظار'),
 | 
			
		||||
            ('in_progress', 'در حال انجام'),
 | 
			
		||||
            ('completed', 'تکمیل شده'),
 | 
			
		||||
            ('cancelled', 'لغو شده'),
 | 
			
		||||
            ('rejected', 'رد شده'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='pending',
 | 
			
		||||
        verbose_name="وضعیت"
 | 
			
		||||
    )
 | 
			
		||||
    started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
 | 
			
		||||
    completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "نمونه فرآیند"
 | 
			
		||||
        verbose_name_plural = "نمونههای فرآیند"
 | 
			
		||||
        ordering = ['-started_at']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.process.name} - {self.requester.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def get_available_steps(self):
 | 
			
		||||
        """دریافت مراحل قابل دسترس"""
 | 
			
		||||
        available_steps = []
 | 
			
		||||
        for step in self.process.steps.all():
 | 
			
		||||
            if self.can_access_step(step):
 | 
			
		||||
                available_steps.append(step)
 | 
			
		||||
        return available_steps
 | 
			
		||||
 | 
			
		||||
    def can_access_step(self, step):
 | 
			
		||||
        """بررسی امکان دسترسی به مرحله"""
 | 
			
		||||
        # بررسی وابستگیها
 | 
			
		||||
        dependencies = step.get_dependencies()
 | 
			
		||||
        for dependency_id in dependencies:
 | 
			
		||||
            step_instance = self.step_instances.filter(step_id=dependency_id).first()
 | 
			
		||||
            if not step_instance or step_instance.status != 'completed':
 | 
			
		||||
                return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def can_edit_step(self, step):
 | 
			
		||||
        """بررسی امکان ویرایش مرحله"""
 | 
			
		||||
        # اگر مرحله مسدود کننده باشد و مراحل بعدی تکمیل شده باشند
 | 
			
		||||
        if step.blocks_previous:
 | 
			
		||||
            later_steps = self.step_instances.filter(
 | 
			
		||||
                step__order__gt=step.order,
 | 
			
		||||
                status='completed'
 | 
			
		||||
            )
 | 
			
		||||
            if later_steps.exists():
 | 
			
		||||
                return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class StepInstance(models.Model):
 | 
			
		||||
    """مدل نمونه مرحله (برای هر مرحله در هر درخواست)"""
 | 
			
		||||
    process_instance = models.ForeignKey(ProcessInstance, on_delete=models.CASCADE, related_name='step_instances', verbose_name="نمونه فرآیند")
 | 
			
		||||
    step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, verbose_name="مرحله")
 | 
			
		||||
    assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="واگذار شده به")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('pending', 'در انتظار'),
 | 
			
		||||
            ('in_progress', 'در حال انجام'),
 | 
			
		||||
            ('completed', 'تکمیل شده'),
 | 
			
		||||
            ('skipped', 'رد شده'),
 | 
			
		||||
            ('blocked', 'مسدود شده'),
 | 
			
		||||
            ('rejected', 'رد شده و نیاز به اصلاح'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='pending',
 | 
			
		||||
        verbose_name="وضعیت"
 | 
			
		||||
    )
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
 | 
			
		||||
    completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "نمونه مرحله"
 | 
			
		||||
        verbose_name_plural = "نمونههای مرحله"
 | 
			
		||||
        ordering = ['process_instance', 'step__order']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.process_instance} - {self.step.name}"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """ذخیره با اعتبارسنجی"""
 | 
			
		||||
        # بررسی وابستگیها
 | 
			
		||||
        if self.status == 'in_progress' or self.status == 'completed':
 | 
			
		||||
            if not self.process_instance.can_access_step(self.step):
 | 
			
		||||
                raise ValidationError("مراحل وابسته تکمیل نشدهاند")
 | 
			
		||||
        
 | 
			
		||||
        # بررسی امکان ویرایش
 | 
			
		||||
        if self.status == 'completed' and not self.process_instance.can_edit_step(self.step):
 | 
			
		||||
            raise ValidationError("این مرحله قابل ویرایش نیست")
 | 
			
		||||
        
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_status_display_with_color(self):
 | 
			
		||||
        """نمایش وضعیت با رنگ"""
 | 
			
		||||
        status_colors = {
 | 
			
		||||
            'pending': 'secondary',
 | 
			
		||||
            'in_progress': 'primary',
 | 
			
		||||
            'completed': 'success',
 | 
			
		||||
            'skipped': 'warning',
 | 
			
		||||
            'blocked': 'danger',
 | 
			
		||||
            'rejected': 'danger',
 | 
			
		||||
        }
 | 
			
		||||
        color = status_colors.get(self.status, 'secondary')
 | 
			
		||||
        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
			
		||||
 | 
			
		||||
    def get_rejection_count(self):
 | 
			
		||||
        """دریافت تعداد رد شدنها"""
 | 
			
		||||
        return self.rejections.count()
 | 
			
		||||
 | 
			
		||||
    def get_latest_rejection(self):
 | 
			
		||||
        """دریافت آخرین رد شدن"""
 | 
			
		||||
        return self.rejections.order_by('-created_at').first()
 | 
			
		||||
 | 
			
		||||
class StepRejection(models.Model):
 | 
			
		||||
    """مدل رد شدن مرحله"""
 | 
			
		||||
    step_instance = models.ForeignKey(
 | 
			
		||||
        StepInstance, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        related_name='rejections',
 | 
			
		||||
        verbose_name="نمونه مرحله"
 | 
			
		||||
    )
 | 
			
		||||
    rejected_by = models.ForeignKey(
 | 
			
		||||
        User, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="رد کننده",
 | 
			
		||||
        related_name='step_rejections'
 | 
			
		||||
    )
 | 
			
		||||
    reason = models.TextField(verbose_name="دلیل رد شدن", help_text="توضیح کامل دلیل رد شدن")
 | 
			
		||||
    instructions = models.TextField(
 | 
			
		||||
        verbose_name="دستورالعملهای اصلاح", 
 | 
			
		||||
        help_text="دستورالعملهایی برای اصلاح مرحله",
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ رد شدن")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "رد شدن مرحله"
 | 
			
		||||
        verbose_name_plural = "رد شدنهای مراحل"
 | 
			
		||||
        ordering = ['-created_at']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """ذخیره با تغییر وضعیت مرحله"""
 | 
			
		||||
        # تغییر وضعیت مرحله به رد شده
 | 
			
		||||
        self.step_instance.status = 'rejected'
 | 
			
		||||
        self.step_instance.save()
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
class StepRevision(models.Model):
 | 
			
		||||
    """مدل بازبینی و اصلاح مرحله"""
 | 
			
		||||
    step_instance = models.ForeignKey(
 | 
			
		||||
        StepInstance, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        related_name='revisions',
 | 
			
		||||
        verbose_name="نمونه مرحله"
 | 
			
		||||
    )
 | 
			
		||||
    rejection = models.ForeignKey(
 | 
			
		||||
        StepRejection, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        related_name='revisions',
 | 
			
		||||
        verbose_name="رد شدن مربوطه"
 | 
			
		||||
    )
 | 
			
		||||
    revised_by = models.ForeignKey(
 | 
			
		||||
        User, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="اصلاح کننده",
 | 
			
		||||
        related_name='step_revisions'
 | 
			
		||||
    )
 | 
			
		||||
    changes_description = models.TextField(
 | 
			
		||||
        verbose_name="توضیح تغییرات",
 | 
			
		||||
        help_text="توضیح تغییراتی که برای اصلاح انجام شده"
 | 
			
		||||
    )
 | 
			
		||||
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ اصلاح")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "بازبینی مرحله"
 | 
			
		||||
        verbose_name_plural = "بازبینیهای مراحل"
 | 
			
		||||
        ordering = ['-created_at']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"بازبینی {self.step_instance} توسط {self.revised_by.get_full_name()}"
 | 
			
		||||
							
								
								
									
										3
									
								
								processes/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										12
									
								
								processes/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
app_name = 'processes'
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('', views.process_list, name='process_list'),
 | 
			
		||||
    path('<int:process_id>/', views.process_detail, name='process_detail'),
 | 
			
		||||
    path('<int:process_id>/start/', views.start_process, name='start_process'),
 | 
			
		||||
    path('instance/<int:instance_id>/', views.instance_detail, name='instance_detail'),
 | 
			
		||||
    path('my-processes/', views.my_processes, name='my_processes'),
 | 
			
		||||
] 
 | 
			
		||||
							
								
								
									
										79
									
								
								processes/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,79 @@
 | 
			
		|||
from django.shortcuts import render, get_object_or_404, redirect
 | 
			
		||||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.http import JsonResponse
 | 
			
		||||
from django.views.decorators.http import require_POST
 | 
			
		||||
from .models import Process, ProcessInstance, StepInstance
 | 
			
		||||
from .forms import ProcessInstanceForm
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def process_list(request):
 | 
			
		||||
    """نمایش لیست فرآیندهای فعال"""
 | 
			
		||||
    processes = Process.objects.filter(is_active=True)
 | 
			
		||||
    return render(request, 'processes/process_list.html', {
 | 
			
		||||
        'processes': processes
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def process_detail(request, process_id):
 | 
			
		||||
    """نمایش جزئیات فرآیند"""
 | 
			
		||||
    process = get_object_or_404(Process, id=process_id, is_active=True)
 | 
			
		||||
    return render(request, 'processes/process_detail.html', {
 | 
			
		||||
        'process': process
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def start_process(request, process_id):
 | 
			
		||||
    """شروع فرآیند جدید"""
 | 
			
		||||
    process = get_object_or_404(Process, id=process_id, is_active=True)
 | 
			
		||||
    
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        form = ProcessInstanceForm(request.POST)
 | 
			
		||||
        if form.is_valid():
 | 
			
		||||
            instance = form.save(commit=False)
 | 
			
		||||
            instance.process = process
 | 
			
		||||
            instance.requester = request.user
 | 
			
		||||
            instance.save()
 | 
			
		||||
            
 | 
			
		||||
            # ایجاد نمونههای مرحله
 | 
			
		||||
            for step in process.steps.all():
 | 
			
		||||
                StepInstance.objects.create(
 | 
			
		||||
                    process_instance=instance,
 | 
			
		||||
                    step=step
 | 
			
		||||
                )
 | 
			
		||||
            
 | 
			
		||||
            # تنظیم مرحله اول به عنوان مرحله فعلی
 | 
			
		||||
            first_step = process.steps.first()
 | 
			
		||||
            if first_step:
 | 
			
		||||
                instance.current_step = first_step
 | 
			
		||||
                instance.status = 'in_progress'
 | 
			
		||||
                instance.save()
 | 
			
		||||
            
 | 
			
		||||
            messages.success(request, f'فرآیند {process.name} با موفقیت شروع شد.')
 | 
			
		||||
            return redirect('processes:instance_detail', instance_id=instance.id)
 | 
			
		||||
    else:
 | 
			
		||||
        form = ProcessInstanceForm()
 | 
			
		||||
    
 | 
			
		||||
    return render(request, 'processes/start_process.html', {
 | 
			
		||||
        'process': process,
 | 
			
		||||
        'form': form
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def instance_detail(request, instance_id):
 | 
			
		||||
    """نمایش جزئیات نمونه فرآیند"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    return render(request, 'processes/instance_detail.html', {
 | 
			
		||||
        'instance': instance
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def my_processes(request):
 | 
			
		||||
    """نمایش فرآیندهای کاربر"""
 | 
			
		||||
    my_instances = ProcessInstance.objects.filter(requester=request.user)
 | 
			
		||||
    assigned_steps = StepInstance.objects.filter(assigned_to=request.user, status='in_progress')
 | 
			
		||||
    
 | 
			
		||||
    return render(request, 'processes/my_processes.html', {
 | 
			
		||||
        'my_instances': my_instances,
 | 
			
		||||
        'assigned_steps': assigned_steps
 | 
			
		||||
    })
 | 
			
		||||
							
								
								
									
										279
									
								
								static/admin/css/autocomplete.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,279 @@
 | 
			
		|||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.errors .select2-selection {
 | 
			
		||||
    border: 1px solid var(--error-fg);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1179
									
								
								static/admin/css/base.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										343
									
								
								static/admin/css/changelists.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,343 @@
 | 
			
		|||
/* 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: 1.1875rem;
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (forced-colors: active) {
 | 
			
		||||
  #changelist-filter {
 | 
			
		||||
      border: 1px solid;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#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-extra-actions {
 | 
			
		||||
    font-size: 0.8125rem;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    border-bottom: 1px solid var(--hairline-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* DATE DRILLDOWN */
 | 
			
		||||
 | 
			
		||||
.change-list .toplinks {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    padding-bottom: 5px;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    gap: 3px 17px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.change-list .toplinks a {
 | 
			
		||||
    font-size: 0.8125rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.change-list .toplinks .date-back {
 | 
			
		||||
    color: var(--body-quiet-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.change-list .toplinks .date-back:focus,
 | 
			
		||||
.change-list .toplinks .date-back:hover {
 | 
			
		||||
    color: var(--link-hover-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ACTIONS */
 | 
			
		||||
 | 
			
		||||
.filtered .actions {
 | 
			
		||||
    border-right: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#changelist table input {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    vertical-align: baseline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
 | 
			
		||||
   selector and the JS adding the class can be removed. */
 | 
			
		||||
#changelist tbody tr.selected {
 | 
			
		||||
    background-color: var(--selected-row);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#changelist tbody tr:has(.action-select:checked) {
 | 
			
		||||
    background-color: var(--selected-row);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (forced-colors: active) {
 | 
			
		||||
    #changelist tbody tr.selected {
 | 
			
		||||
        background-color: SelectedItem;
 | 
			
		||||
    }
 | 
			
		||||
    #changelist tbody tr:has(.action-select:checked) {
 | 
			
		||||
        background-color: SelectedItem;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#changelist .actions {
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    background: var(--body-bg);
 | 
			
		||||
    border-top: none;
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
    line-height: 1.5rem;
 | 
			
		||||
    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: 1.5rem;
 | 
			
		||||
    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: 1.5rem;
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								static/admin/css/custom_rtl.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
@import "/static/dist/css/Vazirmatn-font-face.css";
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: 'Sahel';
 | 
			
		||||
    src: url('/static/fonts/Sahel-FD.eot');
 | 
			
		||||
    src: url('/static/fonts/Sahel-FD.eot?#iefix') format('embedded-opentype'),
 | 
			
		||||
    url('/static/fonts/Sahel-FD.woff') format('woff'),
 | 
			
		||||
    url('/static/fonts/Sahel-FD.ttf') format('truetype');
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[dir="rtl"],
 | 
			
		||||
[dir="rtl"] * {
 | 
			
		||||
    font-family: 'Sahel', /*'Vazirmatn', 'Vazir',*/ "Font Awesome 6 Free",
 | 
			
		||||
    "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
 | 
			
		||||
    "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
 | 
			
		||||
    "Segoe UI Emoji", "Segoe UI Symbol" !important;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								static/admin/css/dark_mode.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,130 @@
 | 
			
		|||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
      --primary: #264b5d;
 | 
			
		||||
      --primary-fg: #f7f7f7;
 | 
			
		||||
  
 | 
			
		||||
      --body-fg: #eeeeee;
 | 
			
		||||
      --body-bg: #121212;
 | 
			
		||||
      --body-quiet-color: #d0d0d0;
 | 
			
		||||
      --body-medium-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;
 | 
			
		||||
 | 
			
		||||
      color-scheme: dark;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
html[data-theme="dark"] {
 | 
			
		||||
    --primary: #264b5d;
 | 
			
		||||
    --primary-fg: #f7f7f7;
 | 
			
		||||
 | 
			
		||||
    --body-fg: #eeeeee;
 | 
			
		||||
    --body-bg: #121212;
 | 
			
		||||
    --body-quiet-color: #d0d0d0;
 | 
			
		||||
    --body-medium-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;
 | 
			
		||||
 | 
			
		||||
    color-scheme: dark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* THEME SWITCH */
 | 
			
		||||
.theme-toggle {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    border: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    margin-inline-start: 5px;
 | 
			
		||||
    margin-top: -1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-toggle svg {
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    height: 1rem;
 | 
			
		||||
    width: 1rem;
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Fully hide screen reader text so we only show the one matching the current
 | 
			
		||||
theme.
 | 
			
		||||
*/
 | 
			
		||||
.theme-toggle .visually-hidden {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html[data-theme="light"] .theme-toggle .theme-label-when-light {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ICONS */
 | 
			
		||||
.theme-toggle svg.theme-icon-when-auto,
 | 
			
		||||
.theme-toggle svg.theme-icon-when-dark,
 | 
			
		||||
.theme-toggle svg.theme-icon-when-light {
 | 
			
		||||
    fill: var(--header-link-color);
 | 
			
		||||
    color: var(--header-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								static/admin/css/dashboard.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
/* DASHBOARD */
 | 
			
		||||
.dashboard td, .dashboard th {
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										512
									
								
								static/admin/css/forms.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,512 @@
 | 
			
		|||
@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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-multiline {
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-multiline > div {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* FORM LABELS */
 | 
			
		||||
 | 
			
		||||
label {
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    color: var(--body-quiet-color);
 | 
			
		||||
    font-size: 0.8125rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.required label, label.required {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* FIELDSETS */
 | 
			
		||||
 | 
			
		||||
fieldset .fieldset-heading,
 | 
			
		||||
fieldset .inline-heading,
 | 
			
		||||
:not(.inline-related) .collapse summary {
 | 
			
		||||
    border: 1px solid var(--header-bg);
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    font-size: 0.8125rem;
 | 
			
		||||
    background: var(--header-bg);
 | 
			
		||||
    color: var(--header-link-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ALIGNED FIELDSETS */
 | 
			
		||||
 | 
			
		||||
.aligned label {
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: 4px 10px 0 0;
 | 
			
		||||
    min-width: 160px;
 | 
			
		||||
    width: 160px;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aligned label:not(.vCheckboxLabel):after {
 | 
			
		||||
    content: '';
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
 | 
			
		||||
    padding: 6px 0;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    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 {
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    margin-left: 160px;
 | 
			
		||||
    padding-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .aligned p.date div.help.timezonewarning,
 | 
			
		||||
form .aligned p.datetime div.help.timezonewarning,
 | 
			
		||||
form .aligned p.time div.help.timezonewarning {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 select option:checked {
 | 
			
		||||
    background-color: var(--selected-row);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .aligned ul li {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .aligned table p {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aligned .vCheckboxLabel {
 | 
			
		||||
    padding: 1px 0 0 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aligned .vCheckboxLabel + p.help,
 | 
			
		||||
.aligned .vCheckboxLabel + div.help {
 | 
			
		||||
    margin-top: -4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
 | 
			
		||||
    width: 610px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fieldset .fieldBox {
 | 
			
		||||
    margin-right: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* WIDE FIELDSETS */
 | 
			
		||||
 | 
			
		||||
.wide label {
 | 
			
		||||
    width: 200px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .wide p.help,
 | 
			
		||||
form .wide ul.errorlist,
 | 
			
		||||
form .wide div.help {
 | 
			
		||||
    padding-left: 50px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form div.help ul {
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
 | 
			
		||||
    width: 450px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* COLLAPSIBLE FIELDSETS */
 | 
			
		||||
 | 
			
		||||
.collapse summary .fieldset-heading,
 | 
			
		||||
.collapse summary .inline-heading {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: currentColor;
 | 
			
		||||
    display: inline;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* MONOSPACE TEXTAREAS */
 | 
			
		||||
 | 
			
		||||
fieldset.monospace textarea {
 | 
			
		||||
    font-family: var(--font-family-monospace);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* SUBMIT ROW */
 | 
			
		||||
 | 
			
		||||
.submit-row {
 | 
			
		||||
    padding: 12px 14px 12px;
 | 
			
		||||
    margin: 0 0 20px;
 | 
			
		||||
    background: var(--darkened-bg);
 | 
			
		||||
    border: 1px solid var(--hairline-color);
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 10px;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.popup .submit-row {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row input {
 | 
			
		||||
    height: 2.1875rem;
 | 
			
		||||
    line-height: 0.9375rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row input, .submit-row a {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row input.default {
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row a.deletelink {
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row a.deletelink {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background: var(--delete-button-bg);
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    padding: 0.625rem 0.9375rem;
 | 
			
		||||
    height: 0.9375rem;
 | 
			
		||||
    line-height: 0.9375rem;
 | 
			
		||||
    color: var(--button-fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row a.closelink {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    background: var(--close-button-bg);
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    padding: 10px 15px;
 | 
			
		||||
    height: 0.9375rem;
 | 
			
		||||
    line-height: 0.9375rem;
 | 
			
		||||
    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);
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row a.closelink:focus,
 | 
			
		||||
.submit-row a.closelink:hover,
 | 
			
		||||
.submit-row a.closelink:active {
 | 
			
		||||
    background: var(--close-button-hover-bg);
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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 h4,
 | 
			
		||||
.inline-related:not(.tabular) .collapse summary {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    color: var(--body-medium-color);
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    font-size: 0.8125rem;
 | 
			
		||||
    background: var(--darkened-bg);
 | 
			
		||||
    border: 1px solid var(--hairline-color);
 | 
			
		||||
    border-left-color: var(--darkened-bg);
 | 
			
		||||
    border-right-color: var(--darkened-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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-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: 1rem;
 | 
			
		||||
    height: 1rem;
 | 
			
		||||
    background-image: url(../img/search.svg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .related-widget-wrapper ul {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clearable-file-input input {
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								static/admin/css/login.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
/* LOGIN FORM */
 | 
			
		||||
 | 
			
		||||
.login {
 | 
			
		||||
    background: var(--darkened-bg);
 | 
			
		||||
    height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login #header {
 | 
			
		||||
    height: auto;
 | 
			
		||||
    padding: 15px 16px;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login #header h1 {
 | 
			
		||||
    font-size: 1.125rem;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login #header h1 a {
 | 
			
		||||
    color: var(--header-link-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login #content {
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										150
									
								
								static/admin/css/nav_sidebar.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,150 @@
 | 
			
		|||
.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 > #nav-sidebar {
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main.shifted > #nav-sidebar {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (forced-colors: active) {
 | 
			
		||||
    #nav-sidebar .current-model {
 | 
			
		||||
        background-color: SelectedItem;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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%;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										967
									
								
								static/admin/css/responsive.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,967 @@
 | 
			
		|||
/* Tablets */
 | 
			
		||||
 | 
			
		||||
input[type="submit"], button {
 | 
			
		||||
    -webkit-appearance: none;
 | 
			
		||||
    appearance: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 1024px) {
 | 
			
		||||
    /* Basic */
 | 
			
		||||
 | 
			
		||||
    html {
 | 
			
		||||
        -webkit-text-size-adjust: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td, th {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .small {
 | 
			
		||||
        font-size: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Layout */
 | 
			
		||||
 | 
			
		||||
    #container {
 | 
			
		||||
        min-width: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #content {
 | 
			
		||||
        padding: 15px 20px 20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    div.breadcrumbs {
 | 
			
		||||
        padding: 10px 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Header */
 | 
			
		||||
 | 
			
		||||
    #header {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        padding: 15px 30px;
 | 
			
		||||
        justify-content: flex-start;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #site-name {
 | 
			
		||||
        margin: 0 0 8px;
 | 
			
		||||
        line-height: 1.2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #user-tools {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        font-weight: 400;
 | 
			
		||||
        line-height: 1.85;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #user-tools a {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        line-height: 1.4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Dashboard */
 | 
			
		||||
 | 
			
		||||
    .dashboard #content {
 | 
			
		||||
        width: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #content-related {
 | 
			
		||||
        margin-right: -290px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .colSM #content-related {
 | 
			
		||||
        margin-left: -290px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .colMS {
 | 
			
		||||
        margin-right: 290px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .colSM {
 | 
			
		||||
        margin-left: 290px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dashboard .module table td a {
 | 
			
		||||
        padding-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td .changelink, td .addlink {
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Changelist */
 | 
			
		||||
 | 
			
		||||
    #toolbar {
 | 
			
		||||
        border: none;
 | 
			
		||||
        padding: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist-search > div {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-wrap: nowrap;
 | 
			
		||||
        max-width: 480px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist-search label {
 | 
			
		||||
        line-height: 1.375rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #toolbar form #searchbar {
 | 
			
		||||
        flex: 1 0 auto;
 | 
			
		||||
        width: 0;
 | 
			
		||||
        height: 1.375rem;
 | 
			
		||||
        margin: 0 10px 0 6px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #toolbar form input[type=submit] {
 | 
			
		||||
        flex: 0 1 auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist-search .quiet {
 | 
			
		||||
        width: 0;
 | 
			
		||||
        flex: 1 0 auto;
 | 
			
		||||
        margin: 5px 0 0 25px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        padding: 15px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions label {
 | 
			
		||||
        display: flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions select {
 | 
			
		||||
        background: var(--body-bg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions .button {
 | 
			
		||||
        min-width: 48px;
 | 
			
		||||
        margin: 0 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions span.all,
 | 
			
		||||
    #changelist .actions span.clear,
 | 
			
		||||
    #changelist .actions span.question,
 | 
			
		||||
    #changelist .actions span.action-counter {
 | 
			
		||||
        font-size: 0.6875rem;
 | 
			
		||||
        margin: 0 10px 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist-filter {
 | 
			
		||||
        flex-basis: 200px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .change-list .filtered .results,
 | 
			
		||||
    .change-list .filtered .paginator,
 | 
			
		||||
    .filtered #toolbar,
 | 
			
		||||
    .filtered .actions,
 | 
			
		||||
 | 
			
		||||
    #changelist .paginator {
 | 
			
		||||
        border-top-color: var(--hairline-color); /* XXX Is this used at all? */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .results + .paginator {
 | 
			
		||||
        border-top: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Forms */
 | 
			
		||||
 | 
			
		||||
    label {
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    Minifiers remove the default (text) "type" attribute from "input" HTML
 | 
			
		||||
    tags. Add input:not([type]) to make the CSS stylesheet work the same.
 | 
			
		||||
    */
 | 
			
		||||
    .form-row input:not([type]),
 | 
			
		||||
    .form-row input[type=text],
 | 
			
		||||
    .form-row input[type=password],
 | 
			
		||||
    .form-row input[type=email],
 | 
			
		||||
    .form-row input[type=url],
 | 
			
		||||
    .form-row input[type=tel],
 | 
			
		||||
    .form-row input[type=number],
 | 
			
		||||
    .form-row textarea,
 | 
			
		||||
    .form-row select,
 | 
			
		||||
    .form-row .vTextField {
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 6px 8px;
 | 
			
		||||
        min-height: 2.25rem;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-row select {
 | 
			
		||||
        height: 2.25rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-row select[multiple] {
 | 
			
		||||
        height: auto;
 | 
			
		||||
        min-height: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fieldset .fieldBox + .fieldBox {
 | 
			
		||||
        margin-top: 10px;
 | 
			
		||||
        padding-top: 10px;
 | 
			
		||||
        border-top: 1px solid var(--hairline-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    textarea {
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        max-height: 120px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned label {
 | 
			
		||||
        padding-top: 6px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .related-lookup,
 | 
			
		||||
    .aligned .datetimeshortcuts,
 | 
			
		||||
    .aligned .related-lookup + strong {
 | 
			
		||||
        align-self: center;
 | 
			
		||||
        margin-left: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    form .aligned div.radiolist {
 | 
			
		||||
        margin-left: 2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-row {
 | 
			
		||||
        padding: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-row a.deletelink {
 | 
			
		||||
        padding: 10px 7px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .button, input[type=submit], input[type=button], .submit-row input, a.button {
 | 
			
		||||
        padding: 7px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Selector */
 | 
			
		||||
 | 
			
		||||
    .selector {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector .selector-filter {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector .selector-filter label {
 | 
			
		||||
        margin: 0 8px 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector .selector-filter input {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        min-height: 0;
 | 
			
		||||
        flex: 1 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-available, .selector-chosen {
 | 
			
		||||
        width: auto;
 | 
			
		||||
        flex: 1 1;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector select {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        flex: 1 0 auto;
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector ul.selector-chooser {
 | 
			
		||||
        width: 26px;
 | 
			
		||||
        height: 52px;
 | 
			
		||||
        padding: 2px 0;
 | 
			
		||||
        border-radius: 20px;
 | 
			
		||||
        transform: translateY(-10px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-add, .selector-remove {
 | 
			
		||||
        width: 20px;
 | 
			
		||||
        height: 20px;
 | 
			
		||||
        background-size: 20px auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-add {
 | 
			
		||||
        background-position: 0 -120px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-remove {
 | 
			
		||||
        background-position: 0 -80px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a.selector-chooseall, a.selector-clearall {
 | 
			
		||||
        align-self: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        max-width: 480px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked > * {
 | 
			
		||||
        flex: 0 1 auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked select {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .selector-available, .stacked .selector-chosen {
 | 
			
		||||
        width: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked ul.selector-chooser {
 | 
			
		||||
        width: 52px;
 | 
			
		||||
        height: 26px;
 | 
			
		||||
        padding: 0 2px;
 | 
			
		||||
        transform: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .selector-chooser li {
 | 
			
		||||
        padding: 3px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .selector-add, .stacked .selector-remove {
 | 
			
		||||
        background-size: 20px auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .selector-add {
 | 
			
		||||
        background-position: 0 -40px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .active.selector-add {
 | 
			
		||||
        background-position: 0 -40px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .active.selector-add:focus, .active.selector-add:hover {
 | 
			
		||||
        background-position: 0 -140px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
 | 
			
		||||
        background-position: 0 -60px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .selector-remove {
 | 
			
		||||
        background-position: 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .active.selector-remove {
 | 
			
		||||
        background-position: 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .active.selector-remove:focus, .active.selector-remove:hover {
 | 
			
		||||
        background-position: 0 -100px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
 | 
			
		||||
        background-position: 0 -20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .help-tooltip, .selector .help-icon {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .datetime input {
 | 
			
		||||
        width: 50%;
 | 
			
		||||
        max-width: 120px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .datetime span {
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .datetime .timezonewarning {
 | 
			
		||||
        display: block;
 | 
			
		||||
        font-size: 0.6875rem;
 | 
			
		||||
        color: var(--body-quiet-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .datetimeshortcuts {
 | 
			
		||||
        color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
 | 
			
		||||
        width: 75%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group {
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Messages */
 | 
			
		||||
 | 
			
		||||
    ul.messagelist li {
 | 
			
		||||
        padding-left: 55px;
 | 
			
		||||
        background-position: 30px 12px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul.messagelist li.error {
 | 
			
		||||
        background-position: 30px 12px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul.messagelist li.warning {
 | 
			
		||||
        background-position: 30px 14px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Login */
 | 
			
		||||
 | 
			
		||||
    .login #header {
 | 
			
		||||
        padding: 15px 20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login #site-name {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* GIS */
 | 
			
		||||
 | 
			
		||||
    div.olMap {
 | 
			
		||||
        max-width: calc(100vw - 30px);
 | 
			
		||||
        max-height: 300px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .olMap + .clear_features {
 | 
			
		||||
        display: block;
 | 
			
		||||
        margin-top: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Docs */
 | 
			
		||||
 | 
			
		||||
    .module table.xfull {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pre.literal-block {
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Mobile */
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767px) {
 | 
			
		||||
    /* Layout */
 | 
			
		||||
 | 
			
		||||
    #header, #content {
 | 
			
		||||
        padding: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    div.breadcrumbs {
 | 
			
		||||
        padding: 10px 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Dashboard */
 | 
			
		||||
 | 
			
		||||
    .colMS, .colSM {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #content-related, .colSM #content-related {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #content-related .module {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #content-related .module h2 {
 | 
			
		||||
        padding: 10px 15px;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Changelist */
 | 
			
		||||
 | 
			
		||||
    #changelist {
 | 
			
		||||
        align-items: stretch;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #toolbar {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist-filter {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions label {
 | 
			
		||||
        flex: 1 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions select {
 | 
			
		||||
        flex: 1 0;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist .actions span {
 | 
			
		||||
        flex: 1 0 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changelist-filter {
 | 
			
		||||
        position: static;
 | 
			
		||||
        width: auto;
 | 
			
		||||
        margin-top: 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .object-tools {
 | 
			
		||||
        float: none;
 | 
			
		||||
        margin: 0 0 15px;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .object-tools li {
 | 
			
		||||
        height: auto;
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .object-tools li + li {
 | 
			
		||||
        margin-left: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Forms */
 | 
			
		||||
 | 
			
		||||
    .form-row {
 | 
			
		||||
        padding: 15px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .form-row,
 | 
			
		||||
    .aligned .form-row > div {
 | 
			
		||||
        max-width: 100vw;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .form-row > div {
 | 
			
		||||
        width: calc(100vw - 30px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .flex-container {
 | 
			
		||||
        flex-flow: column;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .flex-container.checkbox-row {
 | 
			
		||||
        flex-flow: row;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    textarea {
 | 
			
		||||
        max-width: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .vURLField {
 | 
			
		||||
        width: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fieldset .fieldBox + .fieldBox {
 | 
			
		||||
        margin-top: 15px;
 | 
			
		||||
        padding-top: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned label {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        min-width: auto;
 | 
			
		||||
        padding: 0 0 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned label:after {
 | 
			
		||||
        max-height: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .form-row input,
 | 
			
		||||
    .aligned .form-row select,
 | 
			
		||||
    .aligned .form-row textarea {
 | 
			
		||||
        flex: 1 1 auto;
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .checkbox-row input {
 | 
			
		||||
        flex: 0 1 auto;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .vCheckboxLabel {
 | 
			
		||||
        flex: 1 0;
 | 
			
		||||
        padding: 1px 0 0 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned label + p,
 | 
			
		||||
    .aligned label + div.help,
 | 
			
		||||
    .aligned label + div.readonly {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned p.file-upload {
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    span.clearable-file-input {
 | 
			
		||||
        margin-left: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    span.clearable-file-input label {
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
        padding-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .aligned .timezonewarning {
 | 
			
		||||
        flex: 1 0 100%;
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    form .aligned .form-row div.help {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        margin: 5px 0 0;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    form .aligned ul,
 | 
			
		||||
    form .aligned ul.errorlist {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
        padding-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    form .aligned div.radiolist {
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
        margin-right: 15px;
 | 
			
		||||
        margin-bottom: -3px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    form .aligned div.radiolist:not(.inline) div + div {
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Related widget */
 | 
			
		||||
 | 
			
		||||
    .related-widget-wrapper {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: flex-start;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .related-widget-wrapper .selector {
 | 
			
		||||
        order: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .related-widget-wrapper > a {
 | 
			
		||||
        order: 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .related-widget-wrapper .radiolist ~ a {
 | 
			
		||||
        align-self: flex-end;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .related-widget-wrapper > select ~ a {
 | 
			
		||||
        align-self: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Selector */
 | 
			
		||||
 | 
			
		||||
    .selector {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: 10px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-available, .selector-chosen {
 | 
			
		||||
        flex: 1 1 auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector select {
 | 
			
		||||
        max-height: 96px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector ul.selector-chooser {
 | 
			
		||||
        display: block;
 | 
			
		||||
        width: 52px;
 | 
			
		||||
        height: 26px;
 | 
			
		||||
        padding: 0 2px;
 | 
			
		||||
        transform: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector ul.selector-chooser li {
 | 
			
		||||
        float: left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-remove {
 | 
			
		||||
        background-position: 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .active.selector-remove:focus, .active.selector-remove:hover {
 | 
			
		||||
        background-position: 0 -20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selector-add  {
 | 
			
		||||
        background-position: 0 -40px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .active.selector-add:focus, .active.selector-add:hover {
 | 
			
		||||
        background-position: 0 -60px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Inlines */
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related {
 | 
			
		||||
        border: 1px solid var(--hairline-color);
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        margin-top: 15px;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related > * {
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related .module {
 | 
			
		||||
        padding: 0 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
 | 
			
		||||
        border-top: 1px solid var(--hairline-color);
 | 
			
		||||
        border-bottom: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
 | 
			
		||||
        border-top: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related h3 {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        border-top-width: 0;
 | 
			
		||||
        border-bottom-width: 2px;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
 | 
			
		||||
        margin-right: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
 | 
			
		||||
        float: none;
 | 
			
		||||
        flex: 1 1 100%;
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] .aligned label {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group[data-inline-type="stacked"] div.add-row {
 | 
			
		||||
        margin-top: 15px;
 | 
			
		||||
        border: 1px solid var(--hairline-color);
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group div.add-row,
 | 
			
		||||
    .inline-group .tabular tr.add-row td {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .inline-group div.add-row a,
 | 
			
		||||
    .inline-group .tabular tr.add-row td a {
 | 
			
		||||
        display: block;
 | 
			
		||||
        padding: 8px 10px 8px 26px;
 | 
			
		||||
        background-position: 8px 9px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Submit row */
 | 
			
		||||
 | 
			
		||||
    .submit-row {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        margin: 0 0 15px;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-row input, .submit-row input.default, .submit-row a {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-row a.closelink {
 | 
			
		||||
        padding: 10px 0;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-row a.deletelink {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Messages */
 | 
			
		||||
 | 
			
		||||
    ul.messagelist li {
 | 
			
		||||
        padding-left: 40px;
 | 
			
		||||
        background-position: 15px 12px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul.messagelist li.error {
 | 
			
		||||
        background-position: 15px 12px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul.messagelist li.warning {
 | 
			
		||||
        background-position: 15px 14px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Paginator */
 | 
			
		||||
 | 
			
		||||
    .paginator .this-page, .paginator a:link, .paginator a:visited {
 | 
			
		||||
        padding: 4px 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Login */
 | 
			
		||||
 | 
			
		||||
    body.login {
 | 
			
		||||
        padding: 0 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login #container {
 | 
			
		||||
        width: auto;
 | 
			
		||||
        max-width: 480px;
 | 
			
		||||
        margin: 50px auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login #header,
 | 
			
		||||
    .login #content {
 | 
			
		||||
        padding: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login #content-main {
 | 
			
		||||
        float: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login .form-row {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login .form-row + .form-row {
 | 
			
		||||
        margin-top: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login .form-row label {
 | 
			
		||||
        margin: 0 0 5px;
 | 
			
		||||
        line-height: 1.2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login .submit-row {
 | 
			
		||||
        padding: 15px 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login br {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .login .submit-row input {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .errornote {
 | 
			
		||||
        margin: 0 0 20px;
 | 
			
		||||
        padding: 8px 12px;
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Calendar and clock */
 | 
			
		||||
 | 
			
		||||
    .calendarbox, .clockbox {
 | 
			
		||||
        position: fixed !important;
 | 
			
		||||
        top: 50% !important;
 | 
			
		||||
        left: 50% !important;
 | 
			
		||||
        transform: translate(-50%, -50%);
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        border: none;
 | 
			
		||||
        overflow: visible;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendarbox:before, .clockbox:before {
 | 
			
		||||
        content: '';
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        top: 50%;
 | 
			
		||||
        left: 50%;
 | 
			
		||||
        width: 100vw;
 | 
			
		||||
        height: 100vh;
 | 
			
		||||
        background: rgba(0, 0, 0, 0.75);
 | 
			
		||||
        transform: translate(-50%, -50%);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendarbox > *, .clockbox > * {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        z-index: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendarbox > div:first-child {
 | 
			
		||||
        z-index: 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendarbox .calendar, .clockbox h2 {
 | 
			
		||||
        border-radius: 4px 4px 0 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendarbox .calendar-cancel, .clockbox .calendar-cancel {
 | 
			
		||||
        border-radius: 0 0 4px 4px;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-shortcuts {
 | 
			
		||||
        padding: 10px 0;
 | 
			
		||||
        font-size: 0.75rem;
 | 
			
		||||
        line-height: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-shortcuts a {
 | 
			
		||||
        margin: 0 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .timelist a {
 | 
			
		||||
        background: var(--body-bg);
 | 
			
		||||
        padding: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-cancel {
 | 
			
		||||
        padding: 8px 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .clockbox h2 {
 | 
			
		||||
        padding: 8px 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar caption {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
 | 
			
		||||
        z-index: 1;
 | 
			
		||||
        top: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* History */
 | 
			
		||||
 | 
			
		||||
    table#change-history tbody th, table#change-history tbody td {
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
        word-break: break-word;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table#change-history tbody th {
 | 
			
		||||
        width: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Docs */
 | 
			
		||||
 | 
			
		||||
    table.model tbody th, table.model tbody td {
 | 
			
		||||
        font-size: 0.8125rem;
 | 
			
		||||
        word-break: break-word;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										111
									
								
								static/admin/css/responsive_rtl.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
/* 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"] .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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .selector-add {
 | 
			
		||||
        background-position: 0 -80px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .selector-remove {
 | 
			
		||||
        background-position: 0 -120px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover {
 | 
			
		||||
        background-position: 0 -100px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover {
 | 
			
		||||
        background-position: 0 -140px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* MOBILE */
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767px) {
 | 
			
		||||
    [dir="rtl"] .aligned .related-lookup,
 | 
			
		||||
    [dir="rtl"] .aligned .datetimeshortcuts {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
        margin-right: 15px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .aligned ul,
 | 
			
		||||
    [dir="rtl"] form .aligned ul.errorlist {
 | 
			
		||||
        margin-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] #changelist-filter {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
        margin-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
    [dir="rtl"] .aligned .vCheckboxLabel {
 | 
			
		||||
        padding: 1px 5px 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .selector-remove {
 | 
			
		||||
        background-position: 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover {
 | 
			
		||||
        background-position: 0 -20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .selector-add  {
 | 
			
		||||
        background-position: 0 -40px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover {
 | 
			
		||||
        background-position: 0 -60px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										291
									
								
								static/admin/css/rtl.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,291 @@
 | 
			
		|||
/* 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, .hidelink {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.paginator .end {
 | 
			
		||||
    margin-left: 6px;
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.paginator input {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* FORMS */
 | 
			
		||||
 | 
			
		||||
.aligned label {
 | 
			
		||||
    padding: 0 0 3px 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row a.deletelink {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vDateField, .vTimeField {
 | 
			
		||||
    margin-left: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aligned .form-row input {
 | 
			
		||||
    margin-left: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .aligned ul {
 | 
			
		||||
    margin-right: 163px;
 | 
			
		||||
    padding-right: 10px;
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form ul.inline li {
 | 
			
		||||
    float: right;
 | 
			
		||||
    padding-right: 0;
 | 
			
		||||
    padding-left: 7px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .aligned p.help,
 | 
			
		||||
form .aligned div.help {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    margin-right: 160px;
 | 
			
		||||
    padding-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form div.help ul,
 | 
			
		||||
form .aligned .checkbox-row + .help,
 | 
			
		||||
form .aligned p.date div.help.timezonewarning,
 | 
			
		||||
form .aligned p.datetime div.help.timezonewarning,
 | 
			
		||||
form .aligned p.time div.help.timezonewarning {
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
    padding-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form .wide p.help,
 | 
			
		||||
form .wide ul.errorlist,
 | 
			
		||||
form .wide div.help {
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
    padding-right: 50px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.submit-row {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fieldset .fieldBox {
 | 
			
		||||
    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 -15px no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.calendarnav-next {
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: auto;
 | 
			
		||||
    left: 10px;
 | 
			
		||||
    background: url(../img/calendar-icons.svg) 0 0 no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.calendar caption, .calendarbox h2 {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector {
 | 
			
		||||
    float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector .selector-filter {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-add {
 | 
			
		||||
  background: url(../img/selector-icons.svg) 0 -64px no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.active.selector-add:focus, .active.selector-add:hover {
 | 
			
		||||
  background-position: 0 -80px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-remove {
 | 
			
		||||
  background: url(../img/selector-icons.svg) 0 -96px no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.active.selector-remove:focus, .active.selector-remove:hover {
 | 
			
		||||
  background-position: 0 -112px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.selector-chooseall {
 | 
			
		||||
    background: url(../img/selector-icons.svg) right -128px no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
 | 
			
		||||
    background-position: 100% -144px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.selector-clearall {
 | 
			
		||||
    background: url(../img/selector-icons.svg) 0 -160px no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
 | 
			
		||||
    background-position: 0 -176px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.inline-group .tabular td.original p {
 | 
			
		||||
    right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector .selector-chooser {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								static/admin/css/unusable_password_field.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
/* Hide warnings fields if usable password is selected */
 | 
			
		||||
form:has(#id_usable_password input[value="true"]:checked) .messagelist {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Hide password fields if unusable password is selected */
 | 
			
		||||
form:has(#id_usable_password input[value="false"]:checked) .field-password1,
 | 
			
		||||
form:has(#id_usable_password input[value="false"]:checked) .field-password2 {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Select appropriate submit button */
 | 
			
		||||
form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								static/admin/css/vendor/select2/LICENSE-SELECT2.md
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in
 | 
			
		||||
all copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
THE SOFTWARE.
 | 
			
		||||
							
								
								
									
										481
									
								
								static/admin/css/vendor/select2/select2.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,481 @@
 | 
			
		|||
.select2-container {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  vertical-align: middle; }
 | 
			
		||||
  .select2-container .select2-selection--single {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    display: block;
 | 
			
		||||
    height: 28px;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
    -webkit-user-select: none; }
 | 
			
		||||
    .select2-container .select2-selection--single .select2-selection__rendered {
 | 
			
		||||
      display: block;
 | 
			
		||||
      padding-left: 8px;
 | 
			
		||||
      padding-right: 20px;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      white-space: nowrap; }
 | 
			
		||||
    .select2-container .select2-selection--single .select2-selection__clear {
 | 
			
		||||
      position: relative; }
 | 
			
		||||
  .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
 | 
			
		||||
    padding-right: 8px;
 | 
			
		||||
    padding-left: 20px; }
 | 
			
		||||
  .select2-container .select2-selection--multiple {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    display: block;
 | 
			
		||||
    min-height: 32px;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
    -webkit-user-select: none; }
 | 
			
		||||
    .select2-container .select2-selection--multiple .select2-selection__rendered {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      padding-left: 8px;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      white-space: nowrap; }
 | 
			
		||||
  .select2-container .select2-search--inline {
 | 
			
		||||
    float: left; }
 | 
			
		||||
    .select2-container .select2-search--inline .select2-search__field {
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      border: none;
 | 
			
		||||
      font-size: 100%;
 | 
			
		||||
      margin-top: 5px;
 | 
			
		||||
      padding: 0; }
 | 
			
		||||
      .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
 | 
			
		||||
        -webkit-appearance: none; }
 | 
			
		||||
 | 
			
		||||
.select2-dropdown {
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  border: 1px solid #aaa;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: block;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: -100000px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  z-index: 1051; }
 | 
			
		||||
 | 
			
		||||
.select2-results {
 | 
			
		||||
  display: block; }
 | 
			
		||||
 | 
			
		||||
.select2-results__options {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-results__option {
 | 
			
		||||
  padding: 6px;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; }
 | 
			
		||||
  .select2-results__option[aria-selected] {
 | 
			
		||||
    cursor: pointer; }
 | 
			
		||||
 | 
			
		||||
.select2-container--open .select2-dropdown {
 | 
			
		||||
  left: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--open .select2-dropdown--above {
 | 
			
		||||
  border-bottom: none;
 | 
			
		||||
  border-bottom-left-radius: 0;
 | 
			
		||||
  border-bottom-right-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--open .select2-dropdown--below {
 | 
			
		||||
  border-top: none;
 | 
			
		||||
  border-top-left-radius: 0;
 | 
			
		||||
  border-top-right-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-search--dropdown {
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 4px; }
 | 
			
		||||
  .select2-search--dropdown .select2-search__field {
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    box-sizing: border-box; }
 | 
			
		||||
    .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
 | 
			
		||||
      -webkit-appearance: none; }
 | 
			
		||||
  .select2-search--dropdown.select2-search--hide {
 | 
			
		||||
    display: none; }
 | 
			
		||||
 | 
			
		||||
.select2-close-mask {
 | 
			
		||||
  border: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  display: block;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  min-height: 100%;
 | 
			
		||||
  min-width: 100%;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  width: auto;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  z-index: 99;
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
  filter: alpha(opacity=0); }
 | 
			
		||||
 | 
			
		||||
.select2-hidden-accessible {
 | 
			
		||||
  border: 0 !important;
 | 
			
		||||
  clip: rect(0 0 0 0) !important;
 | 
			
		||||
  -webkit-clip-path: inset(50%) !important;
 | 
			
		||||
  clip-path: inset(50%) !important;
 | 
			
		||||
  height: 1px !important;
 | 
			
		||||
  overflow: hidden !important;
 | 
			
		||||
  padding: 0 !important;
 | 
			
		||||
  position: absolute !important;
 | 
			
		||||
  width: 1px !important;
 | 
			
		||||
  white-space: nowrap !important; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-selection--single {
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
  border: 1px solid #aaa;
 | 
			
		||||
  border-radius: 4px; }
 | 
			
		||||
  .select2-container--default .select2-selection--single .select2-selection__rendered {
 | 
			
		||||
    color: #444;
 | 
			
		||||
    line-height: 28px; }
 | 
			
		||||
  .select2-container--default .select2-selection--single .select2-selection__clear {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    float: right;
 | 
			
		||||
    font-weight: bold; }
 | 
			
		||||
  .select2-container--default .select2-selection--single .select2-selection__placeholder {
 | 
			
		||||
    color: #999; }
 | 
			
		||||
  .select2-container--default .select2-selection--single .select2-selection__arrow {
 | 
			
		||||
    height: 26px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    right: 1px;
 | 
			
		||||
    width: 20px; }
 | 
			
		||||
    .select2-container--default .select2-selection--single .select2-selection__arrow b {
 | 
			
		||||
      border-color: #888 transparent transparent transparent;
 | 
			
		||||
      border-style: solid;
 | 
			
		||||
      border-width: 5px 4px 0 4px;
 | 
			
		||||
      height: 0;
 | 
			
		||||
      left: 50%;
 | 
			
		||||
      margin-left: -4px;
 | 
			
		||||
      margin-top: -2px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      width: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
 | 
			
		||||
  float: left; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
 | 
			
		||||
  left: 1px;
 | 
			
		||||
  right: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--disabled .select2-selection--single {
 | 
			
		||||
  background-color: #eee;
 | 
			
		||||
  cursor: default; }
 | 
			
		||||
  .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
 | 
			
		||||
    display: none; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
 | 
			
		||||
  border-color: transparent transparent #888 transparent;
 | 
			
		||||
  border-width: 0 4px 5px 4px; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-selection--multiple {
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  border: 1px solid #aaa;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  cursor: text; }
 | 
			
		||||
  .select2-container--default .select2-selection--multiple .select2-selection__rendered {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0 5px;
 | 
			
		||||
    width: 100%; }
 | 
			
		||||
    .select2-container--default .select2-selection--multiple .select2-selection__rendered li {
 | 
			
		||||
      list-style: none; }
 | 
			
		||||
  .select2-container--default .select2-selection--multiple .select2-selection__clear {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    float: right;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-top: 5px;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
    padding: 1px; }
 | 
			
		||||
  .select2-container--default .select2-selection--multiple .select2-selection__choice {
 | 
			
		||||
    background-color: #e4e4e4;
 | 
			
		||||
    border: 1px solid #aaa;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
    margin-top: 5px;
 | 
			
		||||
    padding: 0 5px; }
 | 
			
		||||
  .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
 | 
			
		||||
    color: #999;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-right: 2px; }
 | 
			
		||||
    .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
 | 
			
		||||
      color: #333; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
 | 
			
		||||
  float: right; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
 | 
			
		||||
  margin-left: 5px;
 | 
			
		||||
  margin-right: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
 | 
			
		||||
  margin-left: 2px;
 | 
			
		||||
  margin-right: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
 | 
			
		||||
  border: solid black 1px;
 | 
			
		||||
  outline: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--disabled .select2-selection--multiple {
 | 
			
		||||
  background-color: #eee;
 | 
			
		||||
  cursor: default; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
 | 
			
		||||
  display: none; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
 | 
			
		||||
  border-top-left-radius: 0;
 | 
			
		||||
  border-top-right-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
 | 
			
		||||
  border-bottom-left-radius: 0;
 | 
			
		||||
  border-bottom-right-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-search--dropdown .select2-search__field {
 | 
			
		||||
  border: 1px solid #aaa; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-search--inline .select2-search__field {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border: none;
 | 
			
		||||
  outline: 0;
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
  -webkit-appearance: textfield; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results > .select2-results__options {
 | 
			
		||||
  max-height: 200px;
 | 
			
		||||
  overflow-y: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results__option[role=group] {
 | 
			
		||||
  padding: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results__option[aria-disabled=true] {
 | 
			
		||||
  color: #999; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results__option[aria-selected=true] {
 | 
			
		||||
  background-color: #ddd; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results__option .select2-results__option {
 | 
			
		||||
  padding-left: 1em; }
 | 
			
		||||
  .select2-container--default .select2-results__option .select2-results__option .select2-results__group {
 | 
			
		||||
    padding-left: 0; }
 | 
			
		||||
  .select2-container--default .select2-results__option .select2-results__option .select2-results__option {
 | 
			
		||||
    margin-left: -1em;
 | 
			
		||||
    padding-left: 2em; }
 | 
			
		||||
    .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
 | 
			
		||||
      margin-left: -2em;
 | 
			
		||||
      padding-left: 3em; }
 | 
			
		||||
      .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
 | 
			
		||||
        margin-left: -3em;
 | 
			
		||||
        padding-left: 4em; }
 | 
			
		||||
        .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
 | 
			
		||||
          margin-left: -4em;
 | 
			
		||||
          padding-left: 5em; }
 | 
			
		||||
          .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
 | 
			
		||||
            margin-left: -5em;
 | 
			
		||||
            padding-left: 6em; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
 | 
			
		||||
  background-color: #5897fb;
 | 
			
		||||
  color: white; }
 | 
			
		||||
 | 
			
		||||
.select2-container--default .select2-results__group {
 | 
			
		||||
  cursor: default;
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 6px; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-selection--single {
 | 
			
		||||
  background-color: #f7f7f7;
 | 
			
		||||
  border: 1px solid #aaa;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  outline: 0;
 | 
			
		||||
  background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
 | 
			
		||||
  background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
 | 
			
		||||
  background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
 | 
			
		||||
  background-repeat: repeat-x;
 | 
			
		||||
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
 | 
			
		||||
  .select2-container--classic .select2-selection--single:focus {
 | 
			
		||||
    border: 1px solid #5897fb; }
 | 
			
		||||
  .select2-container--classic .select2-selection--single .select2-selection__rendered {
 | 
			
		||||
    color: #444;
 | 
			
		||||
    line-height: 28px; }
 | 
			
		||||
  .select2-container--classic .select2-selection--single .select2-selection__clear {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    float: right;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-right: 10px; }
 | 
			
		||||
  .select2-container--classic .select2-selection--single .select2-selection__placeholder {
 | 
			
		||||
    color: #999; }
 | 
			
		||||
  .select2-container--classic .select2-selection--single .select2-selection__arrow {
 | 
			
		||||
    background-color: #ddd;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-left: 1px solid #aaa;
 | 
			
		||||
    border-top-right-radius: 4px;
 | 
			
		||||
    border-bottom-right-radius: 4px;
 | 
			
		||||
    height: 26px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    right: 1px;
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
 | 
			
		||||
    background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
 | 
			
		||||
    background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
 | 
			
		||||
    background-repeat: repeat-x;
 | 
			
		||||
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
 | 
			
		||||
    .select2-container--classic .select2-selection--single .select2-selection__arrow b {
 | 
			
		||||
      border-color: #888 transparent transparent transparent;
 | 
			
		||||
      border-style: solid;
 | 
			
		||||
      border-width: 5px 4px 0 4px;
 | 
			
		||||
      height: 0;
 | 
			
		||||
      left: 50%;
 | 
			
		||||
      margin-left: -4px;
 | 
			
		||||
      margin-top: -2px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      width: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
 | 
			
		||||
  float: left; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-right: 1px solid #aaa;
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
  border-top-left-radius: 4px;
 | 
			
		||||
  border-bottom-left-radius: 4px;
 | 
			
		||||
  left: 1px;
 | 
			
		||||
  right: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open .select2-selection--single {
 | 
			
		||||
  border: 1px solid #5897fb; }
 | 
			
		||||
  .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border: none; }
 | 
			
		||||
    .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
 | 
			
		||||
      border-color: transparent transparent #888 transparent;
 | 
			
		||||
      border-width: 0 4px 5px 4px; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
 | 
			
		||||
  border-top: none;
 | 
			
		||||
  border-top-left-radius: 0;
 | 
			
		||||
  border-top-right-radius: 0;
 | 
			
		||||
  background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
 | 
			
		||||
  background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
 | 
			
		||||
  background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
 | 
			
		||||
  background-repeat: repeat-x;
 | 
			
		||||
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
 | 
			
		||||
  border-bottom: none;
 | 
			
		||||
  border-bottom-left-radius: 0;
 | 
			
		||||
  border-bottom-right-radius: 0;
 | 
			
		||||
  background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
 | 
			
		||||
  background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
 | 
			
		||||
  background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
 | 
			
		||||
  background-repeat: repeat-x;
 | 
			
		||||
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-selection--multiple {
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  border: 1px solid #aaa;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  cursor: text;
 | 
			
		||||
  outline: 0; }
 | 
			
		||||
  .select2-container--classic .select2-selection--multiple:focus {
 | 
			
		||||
    border: 1px solid #5897fb; }
 | 
			
		||||
  .select2-container--classic .select2-selection--multiple .select2-selection__rendered {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0 5px; }
 | 
			
		||||
  .select2-container--classic .select2-selection--multiple .select2-selection__clear {
 | 
			
		||||
    display: none; }
 | 
			
		||||
  .select2-container--classic .select2-selection--multiple .select2-selection__choice {
 | 
			
		||||
    background-color: #e4e4e4;
 | 
			
		||||
    border: 1px solid #aaa;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
    margin-top: 5px;
 | 
			
		||||
    padding: 0 5px; }
 | 
			
		||||
  .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
 | 
			
		||||
    color: #888;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-right: 2px; }
 | 
			
		||||
    .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
 | 
			
		||||
      color: #555; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
 | 
			
		||||
  float: right;
 | 
			
		||||
  margin-left: 5px;
 | 
			
		||||
  margin-right: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
 | 
			
		||||
  margin-left: 2px;
 | 
			
		||||
  margin-right: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open .select2-selection--multiple {
 | 
			
		||||
  border: 1px solid #5897fb; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
 | 
			
		||||
  border-top: none;
 | 
			
		||||
  border-top-left-radius: 0;
 | 
			
		||||
  border-top-right-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
 | 
			
		||||
  border-bottom: none;
 | 
			
		||||
  border-bottom-left-radius: 0;
 | 
			
		||||
  border-bottom-right-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-search--dropdown .select2-search__field {
 | 
			
		||||
  border: 1px solid #aaa;
 | 
			
		||||
  outline: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-search--inline .select2-search__field {
 | 
			
		||||
  outline: 0;
 | 
			
		||||
  box-shadow: none; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-dropdown {
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  border: 1px solid transparent; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-dropdown--above {
 | 
			
		||||
  border-bottom: none; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-dropdown--below {
 | 
			
		||||
  border-top: none; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-results > .select2-results__options {
 | 
			
		||||
  max-height: 200px;
 | 
			
		||||
  overflow-y: auto; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-results__option[role=group] {
 | 
			
		||||
  padding: 0; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-results__option[aria-disabled=true] {
 | 
			
		||||
  color: grey; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
 | 
			
		||||
  background-color: #3875d7;
 | 
			
		||||
  color: white; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic .select2-results__group {
 | 
			
		||||
  cursor: default;
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 6px; }
 | 
			
		||||
 | 
			
		||||
.select2-container--classic.select2-container--open .select2-dropdown {
 | 
			
		||||
  border-color: #5897fb; }
 | 
			
		||||
							
								
								
									
										1
									
								
								static/admin/css/vendor/select2/select2.min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										593
									
								
								static/admin/css/widgets.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,593 @@
 | 
			
		|||
/* SELECTOR (FILTER INTERFACE) */
 | 
			
		||||
 | 
			
		||||
.selector {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    gap: 0 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector select {
 | 
			
		||||
    height: 17.2em;
 | 
			
		||||
    flex: 1 0 auto;
 | 
			
		||||
    overflow: scroll;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-available, .selector-chosen {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 1 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-available h2, .selector-chosen h2 {
 | 
			
		||||
    border: 1px solid var(--border-color);
 | 
			
		||||
    border-radius: 4px 4px 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-chosen .list-footer-display {
 | 
			
		||||
    border: 1px solid var(--border-color);
 | 
			
		||||
    border-top: none;
 | 
			
		||||
    border-radius: 0 0 4px 4px;
 | 
			
		||||
    margin: 0 0 10px;
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    background: var(--primary);
 | 
			
		||||
    color: var(--header-link-color);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
.selector-chosen .list-footer-display__clear {
 | 
			
		||||
    color: var(--breadcrumbs-fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-chosen h2 {
 | 
			
		||||
    background: var(--secondary);
 | 
			
		||||
    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;
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
    min-width: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector-filter input {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector .selector-available input,
 | 
			
		||||
.selector .selector-chosen input {
 | 
			
		||||
    margin-left: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector ul.selector-chooser {
 | 
			
		||||
    align-self: center;
 | 
			
		||||
    width: 22px;
 | 
			
		||||
    background-color: var(--selected-bg);
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    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 .selector-chosen--with-filtered select {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    height: 14em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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: 0 auto;
 | 
			
		||||
    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: 1.125rem;
 | 
			
		||||
    width: 1.125rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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(--secondary);
 | 
			
		||||
    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: 0.6875rem;
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.calendarnav-next {
 | 
			
		||||
    right: 10px;
 | 
			
		||||
    background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.calendar-cancel {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 4px 0;
 | 
			
		||||
    font-size: 0.75rem;
 | 
			
		||||
    background: var(--close-button-bg);
 | 
			
		||||
    border-top: 1px solid var(--border-color);
 | 
			
		||||
    color: var(--button-fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.calendar-cancel:focus, .calendar-cancel:hover {
 | 
			
		||||
    background: var(--close-button-hover-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.calendar-cancel a {
 | 
			
		||||
    color: var(--button-fg);
 | 
			
		||||
    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 {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 0 10px;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.related-widget-wrapper-link {
 | 
			
		||||
    opacity: .6;
 | 
			
		||||
    filter: grayscale(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.related-widget-wrapper-link:link {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    filter: grayscale(0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* GIS MAPS */
 | 
			
		||||
.dj_map {
 | 
			
		||||
    width: 600px;
 | 
			
		||||
    height: 400px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								static/admin/img/LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2014 Code Charm Ltd
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
							
								
								
									
										7
									
								
								static/admin/img/README.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
All icons are taken from Font Awesome (https://fontawesome.com/) project.
 | 
			
		||||
The Font Awesome font is licensed under the SIL OFL 1.1:
 | 
			
		||||
- https://scripts.sil.org/OFL
 | 
			
		||||
 | 
			
		||||
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
 | 
			
		||||
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
 | 
			
		||||
in current folder).
 | 
			
		||||
							
								
								
									
										63
									
								
								static/admin/img/calendar-icons.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   width="15"
 | 
			
		||||
   height="30"
 | 
			
		||||
   viewBox="0 0 1792 3584"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg5"
 | 
			
		||||
   sodipodi:docname="calendar-icons.svg"
 | 
			
		||||
   inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns:xlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="namedview5"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1.0"
 | 
			
		||||
     inkscape:showpageshadow="2"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pagecheckerboard="0"
 | 
			
		||||
     inkscape:deskcolor="#d1d1d1"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     inkscape:zoom="13.3"
 | 
			
		||||
     inkscape:cx="15.526316"
 | 
			
		||||
     inkscape:cy="20.977444"
 | 
			
		||||
     inkscape:window-width="1920"
 | 
			
		||||
     inkscape:window-height="1011"
 | 
			
		||||
     inkscape:window-x="0"
 | 
			
		||||
     inkscape:window-y="0"
 | 
			
		||||
     inkscape:window-maximized="1"
 | 
			
		||||
     inkscape:current-layer="svg5" />
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs2">
 | 
			
		||||
    <g
 | 
			
		||||
       id="previous">
 | 
			
		||||
      <path
 | 
			
		||||
         d="m 1037,1395 102,-102 q 19,-19 19,-45 0,-26 -19,-45 L 832,896 1139,589 q 19,-19 19,-45 0,-26 -19,-45 L 1037,397 q -19,-19 -45,-19 -26,0 -45,19 L 493,851 q -19,19 -19,45 0,26 19,45 l 454,454 q 19,19 45,19 26,0 45,-19 z m 627,-499 q 0,209 -103,385.5 Q 1458,1458 1281.5,1561 1105,1664 896,1664 687,1664 510.5,1561 334,1458 231,1281.5 128,1105 128,896 128,687 231,510.5 334,334 510.5,231 687,128 896,128 1105,128 1281.5,231 1458,334 1561,510.5 1664,687 1664,896 Z"
 | 
			
		||||
         id="path1" />
 | 
			
		||||
    </g>
 | 
			
		||||
    <g
 | 
			
		||||
       id="next">
 | 
			
		||||
      <path
 | 
			
		||||
         d="m 845,1395 454,-454 q 19,-19 19,-45 0,-26 -19,-45 L 845,397 q -19,-19 -45,-19 -26,0 -45,19 L 653,499 q -19,19 -19,45 0,26 19,45 l 307,307 -307,307 q -19,19 -19,45 0,26 19,45 l 102,102 q 19,19 45,19 26,0 45,-19 z m 819,-499 q 0,209 -103,385.5 Q 1458,1458 1281.5,1561 1105,1664 896,1664 687,1664 510.5,1561 334,1458 231,1281.5 128,1105 128,896 128,687 231,510.5 334,334 510.5,231 687,128 896,128 1105,128 1281.5,231 1458,334 1561,510.5 1664,687 1664,896 Z"
 | 
			
		||||
         id="path2" />
 | 
			
		||||
    </g>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <use
 | 
			
		||||
     xlink:href="#next"
 | 
			
		||||
     x="0"
 | 
			
		||||
     y="5376"
 | 
			
		||||
     fill="#000000"
 | 
			
		||||
     id="use5"
 | 
			
		||||
     transform="translate(0,-3584)" />
 | 
			
		||||
  <use
 | 
			
		||||
     xlink:href="#previous"
 | 
			
		||||
     x="0"
 | 
			
		||||
     y="0"
 | 
			
		||||
     fill="#333333"
 | 
			
		||||
     id="use2"
 | 
			
		||||
     style="fill:#000000;fill-opacity:1" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
							
								
								
									
										1
									
								
								static/admin/img/gis/move_vertex_off.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										1
									
								
								static/admin/img/gis/move_vertex_on.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-addlink.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#5fa225" d="M1600 796v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 331 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-alert.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#efb80b" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 504 B  | 
							
								
								
									
										9
									
								
								static/admin/img/icon-calendar.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <g id="icon">
 | 
			
		||||
      <path d="M192 1664h288v-288h-288v288zm352 0h320v-288h-320v288zm-352-352h288v-320h-288v320zm352 0h320v-320h-320v320zm-352-384h288v-288h-288v288zm736 736h320v-288h-320v288zm-384-736h320v-288h-320v288zm768 736h288v-288h-288v288zm-384-352h320v-320h-320v320zm-352-864v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm736 864h288v-320h-288v320zm-384-384h320v-288h-320v288zm384 0h288v-288h-288v288zm32-480v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm384-64v1280q0 52-38 90t-90 38h-1408q-52 0-90-38t-38-90v-1280q0-52 38-90t90-38h128v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h384v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h128q52 0 90 38t38 90z"/>
 | 
			
		||||
    </g>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
 | 
			
		||||
  <use xlink:href="#icon" x="0" y="1792" fill="#003366" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-changelink.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#b48c08" d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 380 B  | 
							
								
								
									
										9
									
								
								static/admin/img/icon-clock.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <g id="icon">
 | 
			
		||||
      <path d="M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
 | 
			
		||||
    </g>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
 | 
			
		||||
  <use xlink:href="#icon" x="0" y="1792" fill="#003366" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 677 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-deletelink.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#dd4646" d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 392 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-hidelink.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#2b70bf" d="m555 1335 78-141q-87-63-136-159t-49-203q0-121 61-225-229 117-381 353 167 258 427 375zm389-759q0-20-14-34t-34-14q-125 0-214.5 89.5T592 832q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm363-191q0 7-1 9-105 188-315 566t-316 567l-49 89q-10 16-28 16-12 0-134-70-16-10-16-28 0-12 44-87-143-65-263.5-173T20 1029Q0 998 0 960t20-69q153-235 380-371t496-136q89 0 180 17l54-97q10-16 28-16 5 0 18 6t31 15.5 33 18.5 31.5 18.5T1291 358q16 10 16 27zm37 447q0 139-79 253.5T1056 1250l280-502q8 45 8 84zm448 128q0 35-20 69-39 64-109 145-150 172-347.5 267T896 1536l74-132q212-18 392.5-137T1664 960q-115-179-282-294l63-112q95 64 182.5 153T1772 891q20 34 20 69z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 784 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-no.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#dd4646" d="M1277 1122q0-26-19-45l-181-181 181-181q19-19 19-45 0-27-19-46l-90-90q-19-19-46-19-26 0-45 19l-181 181-181-181q-19-19-45-19-27 0-46 19l-90 90q-19 19-19 46 0 26 19 45l181 181-181 181q-19 19-19 45 0 27 19 46l90 90q19 19 46 19 26 0 45-19l181-181 181 181q19 19 45 19 27 0 46-19l90-90q19-19 19-46zm387-226q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 560 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-unknown-alt.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#ffffff" d="M1024 1376v-192q0-14-9-23t-23-9h-192q-14 0-23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23-9t9-23zm256-672q0-88-55.5-163t-138.5-116-170-41q-243 0-371 213-15 24 8 42l132 100q7 6 19 6 16 0 25-12 53-68 86-92 34-24 86-24 48 0 85.5 26t37.5 59q0 38-20 61t-68 45q-63 28-115.5 86.5t-52.5 125.5v36q0 14 9 23t23 9h192q14 0 23-9t9-23q0-19 21.5-49.5t54.5-49.5q32-18 49-28.5t46-35 44.5-48 28-60.5 12.5-81zm384 192q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 655 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-unknown.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#666666" d="M1024 1376v-192q0-14-9-23t-23-9h-192q-14 0-23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23-9t9-23zm256-672q0-88-55.5-163t-138.5-116-170-41q-243 0-371 213-15 24 8 42l132 100q7 6 19 6 16 0 25-12 53-68 86-92 34-24 86-24 48 0 85.5 26t37.5 59q0 38-20 61t-68 45q-63 28-115.5 86.5t-52.5 125.5v36q0 14 9 23t23 9h192q14 0 23-9t9-23q0-19 21.5-49.5t54.5-49.5q32-18 49-28.5t46-35 44.5-48 28-60.5 12.5-81zm384 192q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 655 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-viewlink.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#2b70bf" d="M1664 960q-152-236-381-353 61 104 61 225 0 185-131.5 316.5t-316.5 131.5-316.5-131.5-131.5-316.5q0-121 61-225-229 117-381 353 133 205 333.5 326.5t434.5 121.5 434.5-121.5 333.5-326.5zm-720-384q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm848 384q0 34-20 69-140 230-376.5 368.5t-499.5 138.5-499.5-139-376.5-368q-20-35-20-69t20-69q140-229 376.5-368t499.5-139 499.5 139 376.5 368q20 35 20 69z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 581 B  | 
							
								
								
									
										3
									
								
								static/admin/img/icon-yes.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill="#70bf2b" d="M1412 734q0-28-18-46l-91-90q-19-19-45-19t-45 19l-408 407-226-226q-19-19-45-19t-45 19l-91 90q-18 18-18 46 0 27 18 45l362 362q19 19 45 19 27 0 46-19l543-543q18-18 18-45zm252 162q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 436 B  | 
							
								
								
									
										
											BIN
										
									
								
								static/admin/img/icon_searchbox_rosetta.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 667 B  |