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 |