Spaces:
Running
Running
Commit
·
ef287e1
0
Parent(s):
first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Akompta/__init__.py +0 -0
- Akompta/asgi.py +16 -0
- Akompta/settings.py +292 -0
- Akompta/urls.py +22 -0
- Akompta/wsgi.py +16 -0
- Documentation/API_EXEMPLES.MD +469 -0
- api/__init__.py +0 -0
- api/admin.py +239 -0
- api/apps.py +6 -0
- api/exceptions.py +47 -0
- api/gemini_service.py +96 -0
- api/management/__init__.py +0 -0
- api/management/commands/__init__.py +0 -0
- api/management/commands/seed_data.py +213 -0
- api/migrations/0001_initial.py +141 -0
- api/migrations/0002_alter_user_managers.py +18 -0
- api/migrations/0003_notification_supportticket.py +49 -0
- api/migrations/__init__.py +0 -0
- api/models.py +281 -0
- api/serializers.py +234 -0
- api/tests.py +367 -0
- api/tests_new.py +115 -0
- api/urls.py +39 -0
- api/views.py +568 -0
- manage.py +22 -0
- requirements.txt +29 -0
- static/admin/css/autocomplete.css +279 -0
- static/admin/css/base.css +1180 -0
- static/admin/css/changelists.css +343 -0
- static/admin/css/dark_mode.css +130 -0
- static/admin/css/dashboard.css +29 -0
- static/admin/css/forms.css +498 -0
- static/admin/css/login.css +61 -0
- static/admin/css/nav_sidebar.css +150 -0
- static/admin/css/responsive.css +904 -0
- static/admin/css/responsive_rtl.css +89 -0
- static/admin/css/rtl.css +293 -0
- static/admin/css/unusable_password_field.css +19 -0
- static/admin/css/vendor/select2/LICENSE-SELECT2.md +21 -0
- static/admin/css/vendor/select2/select2.css +481 -0
- static/admin/css/vendor/select2/select2.min.css +1 -0
- static/admin/css/widgets.css +613 -0
- static/admin/img/LICENSE +20 -0
- static/admin/img/README.txt +7 -0
- static/admin/img/calendar-icons.svg +63 -0
- static/admin/img/gis/move_vertex_off.svg +1 -0
- static/admin/img/gis/move_vertex_on.svg +1 -0
- static/admin/img/icon-addlink.svg +3 -0
- static/admin/img/icon-alert.svg +3 -0
- static/admin/img/icon-calendar.svg +9 -0
Akompta/__init__.py
ADDED
|
File without changes
|
Akompta/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for Akompta project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Akompta.settings')
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
Akompta/settings.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Django settings for Akompta project.
|
| 3 |
+
|
| 4 |
+
Generated by 'django-admin startproject' using Django 5.2.8.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/topics/settings/
|
| 8 |
+
|
| 9 |
+
For the full list of settings and their values, see
|
| 10 |
+
https://docs.djangoproject.com/en/5.2/ref/settings/
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from datetime import timedelta
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 18 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Quick-start development settings - unsuitable for production
|
| 22 |
+
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
| 23 |
+
|
| 24 |
+
# SECURITY WARNING: keep the secret key used in production secret!
|
| 25 |
+
SECRET_KEY = 'django-insecure-3m1!a3u-z=5k8x9y#-954&3ree&mr&$o97fuy8ds*8dox!(rvx'
|
| 26 |
+
|
| 27 |
+
# SECURITY WARNING: don't run with debug turned on in production!
|
| 28 |
+
DEBUG = True
|
| 29 |
+
|
| 30 |
+
ALLOWED_HOSTS = []
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# Application definition
|
| 34 |
+
|
| 35 |
+
INSTALLED_APPS = [
|
| 36 |
+
'django.contrib.admin',
|
| 37 |
+
'django.contrib.auth',
|
| 38 |
+
'django.contrib.contenttypes',
|
| 39 |
+
'django.contrib.sessions',
|
| 40 |
+
'django.contrib.messages',
|
| 41 |
+
'django.contrib.staticfiles',
|
| 42 |
+
|
| 43 |
+
# Third party
|
| 44 |
+
'rest_framework',
|
| 45 |
+
'rest_framework_simplejwt',
|
| 46 |
+
'corsheaders',
|
| 47 |
+
'django_filters',
|
| 48 |
+
|
| 49 |
+
# Local apps
|
| 50 |
+
'api', # Votre app principale
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
MIDDLEWARE = [
|
| 54 |
+
'django.middleware.security.SecurityMiddleware',
|
| 55 |
+
'corsheaders.middleware.CorsMiddleware',
|
| 56 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 57 |
+
'django.middleware.common.CommonMiddleware',
|
| 58 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 59 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 60 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 61 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
ROOT_URLCONF = 'Akompta.urls'
|
| 65 |
+
|
| 66 |
+
TEMPLATES = [
|
| 67 |
+
{
|
| 68 |
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
| 69 |
+
'DIRS': [],
|
| 70 |
+
'APP_DIRS': True,
|
| 71 |
+
'OPTIONS': {
|
| 72 |
+
'context_processors': [
|
| 73 |
+
'django.template.context_processors.request',
|
| 74 |
+
'django.contrib.auth.context_processors.auth',
|
| 75 |
+
'django.contrib.messages.context_processors.messages',
|
| 76 |
+
],
|
| 77 |
+
},
|
| 78 |
+
},
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
WSGI_APPLICATION = 'Akompta.wsgi.application'
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# Database
|
| 85 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
| 86 |
+
|
| 87 |
+
DATABASES = {
|
| 88 |
+
'default': {
|
| 89 |
+
'ENGINE': 'django.db.backends.sqlite3',
|
| 90 |
+
'NAME': BASE_DIR / 'db.sqlite3',
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# Password validation
|
| 96 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
| 97 |
+
|
| 98 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 99 |
+
{
|
| 100 |
+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
| 104 |
+
'OPTIONS': {
|
| 105 |
+
'min_length': 8,
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
| 113 |
+
},
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# Internationalization
|
| 118 |
+
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
| 119 |
+
|
| 120 |
+
LANGUAGE_CODE = 'fr-fr'
|
| 121 |
+
|
| 122 |
+
TIME_ZONE = 'UTC'
|
| 123 |
+
|
| 124 |
+
USE_I18N = True
|
| 125 |
+
|
| 126 |
+
USE_TZ = True
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# Static files (CSS, JavaScript, Images)
|
| 130 |
+
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
| 131 |
+
|
| 132 |
+
STATIC_URL = 'static/'
|
| 133 |
+
STATIC_ROOT = BASE_DIR / 'static'
|
| 134 |
+
|
| 135 |
+
MEDIA_URL = 'media/'
|
| 136 |
+
MEDIA_ROOT = BASE_DIR / 'media'
|
| 137 |
+
|
| 138 |
+
# Default primary key field type
|
| 139 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
| 140 |
+
|
| 141 |
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
AUTH_USER_MODEL = 'api.User'
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# REST Framework Configuration
|
| 148 |
+
REST_FRAMEWORK = {
|
| 149 |
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
| 150 |
+
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
| 151 |
+
],
|
| 152 |
+
'DEFAULT_PERMISSION_CLASSES': [
|
| 153 |
+
'rest_framework.permissions.IsAuthenticated',
|
| 154 |
+
],
|
| 155 |
+
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
| 156 |
+
'PAGE_SIZE': 20,
|
| 157 |
+
'DEFAULT_FILTER_BACKENDS': [
|
| 158 |
+
'django_filters.rest_framework.DjangoFilterBackend',
|
| 159 |
+
'rest_framework.filters.SearchFilter',
|
| 160 |
+
'rest_framework.filters.OrderingFilter',
|
| 161 |
+
],
|
| 162 |
+
'DEFAULT_THROTTLE_CLASSES': [
|
| 163 |
+
'rest_framework.throttling.AnonRateThrottle',
|
| 164 |
+
'rest_framework.throttling.UserRateThrottle'
|
| 165 |
+
],
|
| 166 |
+
'DEFAULT_THROTTLE_RATES': {
|
| 167 |
+
'anon': '100/hour',
|
| 168 |
+
'user': '1000/hour'
|
| 169 |
+
},
|
| 170 |
+
'EXCEPTION_HANDLER': 'api.exceptions.custom_exception_handler',
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# Simple JWT Configuration
|
| 175 |
+
SIMPLE_JWT = {
|
| 176 |
+
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
|
| 177 |
+
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
|
| 178 |
+
'ROTATE_REFRESH_TOKENS': True,
|
| 179 |
+
'BLACKLIST_AFTER_ROTATION': True,
|
| 180 |
+
'UPDATE_LAST_LOGIN': True,
|
| 181 |
+
|
| 182 |
+
'ALGORITHM': 'HS256',
|
| 183 |
+
'SIGNING_KEY': SECRET_KEY,
|
| 184 |
+
'VERIFYING_KEY': None,
|
| 185 |
+
'AUDIENCE': None,
|
| 186 |
+
'ISSUER': None,
|
| 187 |
+
|
| 188 |
+
'AUTH_HEADER_TYPES': ('Bearer',),
|
| 189 |
+
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
|
| 190 |
+
'USER_ID_FIELD': 'id',
|
| 191 |
+
'USER_ID_CLAIM': 'user_id',
|
| 192 |
+
|
| 193 |
+
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
| 194 |
+
'TOKEN_TYPE_CLAIM': 'token_type',
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# CORS Configuration
|
| 199 |
+
CORS_ALLOWED_ORIGINS = os.environ.get(
|
| 200 |
+
'CORS_ALLOWED_ORIGINS',
|
| 201 |
+
'http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000'
|
| 202 |
+
).split(',')
|
| 203 |
+
|
| 204 |
+
CORS_ALLOW_CREDENTIALS = True
|
| 205 |
+
|
| 206 |
+
CORS_ALLOW_METHODS = [
|
| 207 |
+
'DELETE',
|
| 208 |
+
'GET',
|
| 209 |
+
'OPTIONS',
|
| 210 |
+
'PATCH',
|
| 211 |
+
'POST',
|
| 212 |
+
'PUT',
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
+
CORS_ALLOW_HEADERS = [
|
| 216 |
+
'accept',
|
| 217 |
+
'accept-encoding',
|
| 218 |
+
'authorization',
|
| 219 |
+
'content-type',
|
| 220 |
+
'dnt',
|
| 221 |
+
'origin',
|
| 222 |
+
'user-agent',
|
| 223 |
+
'x-csrftoken',
|
| 224 |
+
'x-requested-with',
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# File Upload Settings
|
| 229 |
+
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
|
| 230 |
+
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
|
| 231 |
+
|
| 232 |
+
# Allowed image formats
|
| 233 |
+
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
| 234 |
+
MAX_IMAGE_SIZE = 5 * 1024 * 1024 # 5MB
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# Email Configuration (pour password reset)
|
| 238 |
+
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
| 239 |
+
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
|
| 240 |
+
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', '587'))
|
| 241 |
+
EMAIL_USE_TLS = True
|
| 242 |
+
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
| 243 |
+
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
|
| 244 |
+
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@akompta.com')
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# Security Settings for Production
|
| 248 |
+
if not DEBUG:
|
| 249 |
+
SECURE_SSL_REDIRECT = True
|
| 250 |
+
SESSION_COOKIE_SECURE = True
|
| 251 |
+
CSRF_COOKIE_SECURE = True
|
| 252 |
+
SECURE_BROWSER_XSS_FILTER = True
|
| 253 |
+
SECURE_CONTENT_TYPE_NOSNIFF = True
|
| 254 |
+
X_FRAME_OPTIONS = 'DENY'
|
| 255 |
+
SECURE_HSTS_SECONDS = 31536000
|
| 256 |
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
| 257 |
+
SECURE_HSTS_PRELOAD = True
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
# Logging Configuration
|
| 261 |
+
LOGGING = {
|
| 262 |
+
'version': 1,
|
| 263 |
+
'disable_existing_loggers': False,
|
| 264 |
+
'formatters': {
|
| 265 |
+
'verbose': {
|
| 266 |
+
'format': '{levelname} {asctime} {module} {message}',
|
| 267 |
+
'style': '{',
|
| 268 |
+
},
|
| 269 |
+
},
|
| 270 |
+
'handlers': {
|
| 271 |
+
'console': {
|
| 272 |
+
'class': 'logging.StreamHandler',
|
| 273 |
+
'formatter': 'verbose',
|
| 274 |
+
},
|
| 275 |
+
'file': {
|
| 276 |
+
'class': 'logging.FileHandler',
|
| 277 |
+
'filename': BASE_DIR / 'logs' / 'django.log',
|
| 278 |
+
'formatter': 'verbose',
|
| 279 |
+
},
|
| 280 |
+
},
|
| 281 |
+
'root': {
|
| 282 |
+
'handlers': ['console', 'file'],
|
| 283 |
+
'level': 'INFO',
|
| 284 |
+
},
|
| 285 |
+
'loggers': {
|
| 286 |
+
'django': {
|
| 287 |
+
'handlers': ['console', 'file'],
|
| 288 |
+
'level': 'INFO',
|
| 289 |
+
'propagate': False,
|
| 290 |
+
},
|
| 291 |
+
},
|
| 292 |
+
}
|
Akompta/urls.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
URLs principales du projet Akompta
|
| 3 |
+
"""
|
| 4 |
+
from django.contrib import admin
|
| 5 |
+
from django.urls import path, include
|
| 6 |
+
from django.conf import settings
|
| 7 |
+
from django.conf.urls.static import static
|
| 8 |
+
|
| 9 |
+
urlpatterns = [
|
| 10 |
+
path('admin/', admin.site.urls),
|
| 11 |
+
path('api/', include('api.urls')),
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
# Servir les fichiers media en développement
|
| 15 |
+
if settings.DEBUG:
|
| 16 |
+
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
| 17 |
+
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
| 18 |
+
|
| 19 |
+
# Configuration de l'admin
|
| 20 |
+
admin.site.site_header = "Akompta AI Administration"
|
| 21 |
+
admin.site.site_title = "Akompta Admin"
|
| 22 |
+
admin.site.index_title = "Bienvenue sur l'administration Akompta"
|
Akompta/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for Akompta project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Akompta.settings')
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|
Documentation/API_EXEMPLES.MD
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Exemples d'Utilisation de l'API Akompta
|
| 2 |
+
|
| 3 |
+
## 🔐 Authentification
|
| 4 |
+
|
| 5 |
+
### Inscription (Compte Personnel)
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
curl -X POST http://localhost:8000/api/auth/register/ \
|
| 9 |
+
-H "Content-Type: application/json" \
|
| 10 |
+
-d '{
|
| 11 |
+
"email": "user@example.com",
|
| 12 |
+
"password": "SecurePass123!",
|
| 13 |
+
"password2": "SecurePass123!",
|
| 14 |
+
"first_name": "John",
|
| 15 |
+
"last_name": "Doe",
|
| 16 |
+
"phone_number": "+22890123456",
|
| 17 |
+
"account_type": "personal",
|
| 18 |
+
"agreed": true
|
| 19 |
+
}'
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
**Réponse :**
|
| 23 |
+
```json
|
| 24 |
+
{
|
| 25 |
+
"user": {
|
| 26 |
+
"id": 1,
|
| 27 |
+
"email": "user@example.com",
|
| 28 |
+
"first_name": "John",
|
| 29 |
+
"last_name": "Doe",
|
| 30 |
+
"account_type": "personal",
|
| 31 |
+
"is_premium": false
|
| 32 |
+
},
|
| 33 |
+
"tokens": {
|
| 34 |
+
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
| 35 |
+
"access": "eyJ0eXAiOiJKV1QiLCJhbGc..."
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### Inscription (Compte Business)
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
curl -X POST http://localhost:8000/api/auth/register/ \
|
| 44 |
+
-H "Content-Type: application/json" \
|
| 45 |
+
-d '{
|
| 46 |
+
"email": "business@example.com",
|
| 47 |
+
"password": "SecurePass123!",
|
| 48 |
+
"password2": "SecurePass123!",
|
| 49 |
+
"first_name": "Jane",
|
| 50 |
+
"last_name": "Smith",
|
| 51 |
+
"phone_number": "+22890987654",
|
| 52 |
+
"account_type": "business",
|
| 53 |
+
"business_name": "AgriTech Solutions",
|
| 54 |
+
"sector": "Agriculture",
|
| 55 |
+
"location": "Lomé, Togo",
|
| 56 |
+
"ifu": "1234567890123",
|
| 57 |
+
"agreed": true,
|
| 58 |
+
"businessAgreed": true
|
| 59 |
+
}'
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### Connexion
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
curl -X POST http://localhost:8000/api/auth/login/ \
|
| 66 |
+
-H "Content-Type: application/json" \
|
| 67 |
+
-d '{
|
| 68 |
+
"email": "user@example.com",
|
| 69 |
+
"password": "SecurePass123!"
|
| 70 |
+
}'
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Rafraîchir le Token
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
|
| 77 |
+
-H "Content-Type: application/json" \
|
| 78 |
+
-d '{
|
| 79 |
+
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
|
| 80 |
+
}'
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Récupérer le Profil
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
curl -X GET http://localhost:8000/api/auth/me/ \
|
| 87 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### Modifier le Profil
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
curl -X PATCH http://localhost:8000/api/auth/me/ \
|
| 94 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 95 |
+
-H "Content-Type: application/json" \
|
| 96 |
+
-d '{
|
| 97 |
+
"phone_number": "+22890999888",
|
| 98 |
+
"dark_mode": true
|
| 99 |
+
}'
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### Changer le Mot de Passe
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
curl -X POST http://localhost:8000/api/auth/change-password/ \
|
| 106 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 107 |
+
-H "Content-Type: application/json" \
|
| 108 |
+
-d '{
|
| 109 |
+
"old_password": "SecurePass123!",
|
| 110 |
+
"new_password": "NewSecurePass456!",
|
| 111 |
+
"new_password2": "NewSecurePass456!"
|
| 112 |
+
}'
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## 📦 Produits
|
| 116 |
+
|
| 117 |
+
### Créer un Produit
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
curl -X POST http://localhost:8000/api/products/ \
|
| 121 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 122 |
+
-H "Content-Type: application/json" \
|
| 123 |
+
-d '{
|
| 124 |
+
"name": "Tomates",
|
| 125 |
+
"description": "Tomates fraîches de qualité",
|
| 126 |
+
"price": "800.00",
|
| 127 |
+
"unit": "Kg",
|
| 128 |
+
"category": "vente",
|
| 129 |
+
"stock_status": "ok"
|
| 130 |
+
}'
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Créer un Produit avec Image
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
curl -X POST http://localhost:8000/api/products/ \
|
| 137 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 138 |
+
-F "name=Tomates" \
|
| 139 |
+
-F "description=Tomates fraîches" \
|
| 140 |
+
-F "price=800.00" \
|
| 141 |
+
-F "unit=Kg" \
|
| 142 |
+
-F "category=vente" \
|
| 143 |
+
-F "stock_status=ok" \
|
| 144 |
+
-F "image=@/path/to/image.jpg"
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### Lister les Produits
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
# Tous les produits
|
| 151 |
+
curl -X GET http://localhost:8000/api/products/ \
|
| 152 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 153 |
+
|
| 154 |
+
# Filtrer par catégorie
|
| 155 |
+
curl -X GET "http://localhost:8000/api/products/?category=vente" \
|
| 156 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 157 |
+
|
| 158 |
+
# Rechercher
|
| 159 |
+
curl -X GET "http://localhost:8000/api/products/?search=tomate" \
|
| 160 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 161 |
+
|
| 162 |
+
# Pagination
|
| 163 |
+
curl -X GET "http://localhost:8000/api/products/?page=2" \
|
| 164 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
### Modifier un Produit
|
| 168 |
+
|
| 169 |
+
```bash
|
| 170 |
+
curl -X PUT http://localhost:8000/api/products/1/ \
|
| 171 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 172 |
+
-H "Content-Type: application/json" \
|
| 173 |
+
-d '{
|
| 174 |
+
"name": "Tomates Premium",
|
| 175 |
+
"price": "1000.00",
|
| 176 |
+
"stock_status": "low"
|
| 177 |
+
}'
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
### Supprimer un Produit
|
| 181 |
+
|
| 182 |
+
```bash
|
| 183 |
+
curl -X DELETE http://localhost:8000/api/products/1/ \
|
| 184 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
### Exporter les Produits en CSV
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
curl -X GET http://localhost:8000/api/products/export/ \
|
| 191 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 192 |
+
-o products.csv
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
## 💰 Transactions
|
| 196 |
+
|
| 197 |
+
### Créer une Transaction
|
| 198 |
+
|
| 199 |
+
```bash
|
| 200 |
+
curl -X POST http://localhost:8000/api/transactions/ \
|
| 201 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 202 |
+
-H "Content-Type: application/json" \
|
| 203 |
+
-d '{
|
| 204 |
+
"name": "Vente de tomates",
|
| 205 |
+
"amount": "5000.00",
|
| 206 |
+
"type": "income",
|
| 207 |
+
"category": "Ventes",
|
| 208 |
+
"date": "2024-11-27T10:30:00Z",
|
| 209 |
+
"currency": "FCFA"
|
| 210 |
+
}'
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### Lister les Transactions
|
| 214 |
+
|
| 215 |
+
```bash
|
| 216 |
+
# Toutes les transactions
|
| 217 |
+
curl -X GET http://localhost:8000/api/transactions/ \
|
| 218 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 219 |
+
|
| 220 |
+
# Filtrer par type
|
| 221 |
+
curl -X GET "http://localhost:8000/api/transactions/?type=income" \
|
| 222 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 223 |
+
|
| 224 |
+
# Filtrer par période
|
| 225 |
+
curl -X GET "http://localhost:8000/api/transactions/?date_range=week" \
|
| 226 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 227 |
+
|
| 228 |
+
# Trier par date décroissante
|
| 229 |
+
curl -X GET "http://localhost:8000/api/transactions/?ordering=-date" \
|
| 230 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 231 |
+
|
| 232 |
+
# Rechercher
|
| 233 |
+
curl -X GET "http://localhost:8000/api/transactions/?search=vente" \
|
| 234 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Résumé des Transactions (Dashboard)
|
| 238 |
+
|
| 239 |
+
```bash
|
| 240 |
+
curl -X GET http://localhost:8000/api/transactions/summary/ \
|
| 241 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
**Réponse :**
|
| 245 |
+
```json
|
| 246 |
+
{
|
| 247 |
+
"balance": "125000.00",
|
| 248 |
+
"income_24h": "15000.00",
|
| 249 |
+
"expenses_24h": "8000.00",
|
| 250 |
+
"income_variation": 12.5,
|
| 251 |
+
"expenses_variation": -5.3
|
| 252 |
+
}
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
## 📊 Analytics
|
| 256 |
+
|
| 257 |
+
### Overview (Graphique en Barres)
|
| 258 |
+
|
| 259 |
+
```bash
|
| 260 |
+
curl -X GET http://localhost:8000/api/analytics/overview/ \
|
| 261 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
**Réponse :**
|
| 265 |
+
```json
|
| 266 |
+
[
|
| 267 |
+
{
|
| 268 |
+
"month": "Oct 2024",
|
| 269 |
+
"income": "85000.00",
|
| 270 |
+
"expenses": "45000.00"
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"month": "Nov 2024",
|
| 274 |
+
"income": "120000.00",
|
| 275 |
+
"expenses": "65000.00"
|
| 276 |
+
}
|
| 277 |
+
]
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
### Breakdown (Graphique Camembert)
|
| 281 |
+
|
| 282 |
+
```bash
|
| 283 |
+
curl -X GET http://localhost:8000/api/analytics/breakdown/ \
|
| 284 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
**Réponse :**
|
| 288 |
+
```json
|
| 289 |
+
[
|
| 290 |
+
{
|
| 291 |
+
"category": "Transport",
|
| 292 |
+
"amount": "25000.00",
|
| 293 |
+
"percentage": 38.5
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"category": "Loyer",
|
| 297 |
+
"amount": "40000.00",
|
| 298 |
+
"percentage": 61.5
|
| 299 |
+
}
|
| 300 |
+
]
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
### KPIs
|
| 304 |
+
|
| 305 |
+
```bash
|
| 306 |
+
curl -X GET http://localhost:8000/api/analytics/kpi/ \
|
| 307 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
**Réponse :**
|
| 311 |
+
```json
|
| 312 |
+
{
|
| 313 |
+
"average_basket": "12500.00",
|
| 314 |
+
"estimated_mrr": "120000.00",
|
| 315 |
+
"cac": "5000.00"
|
| 316 |
+
}
|
| 317 |
+
```
|
| 318 |
+
|
| 319 |
+
## 🎯 Budgets
|
| 320 |
+
|
| 321 |
+
### Créer un Budget
|
| 322 |
+
|
| 323 |
+
```bash
|
| 324 |
+
curl -X POST http://localhost:8000/api/budgets/ \
|
| 325 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 326 |
+
-H "Content-Type: application/json" \
|
| 327 |
+
-d '{
|
| 328 |
+
"category": "Transport",
|
| 329 |
+
"limit": "50000.00",
|
| 330 |
+
"color": "#3B82F6"
|
| 331 |
+
}'
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
### Lister les Budgets
|
| 335 |
+
|
| 336 |
+
```bash
|
| 337 |
+
curl -X GET http://localhost:8000/api/budgets/ \
|
| 338 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
**Réponse :**
|
| 342 |
+
```json
|
| 343 |
+
{
|
| 344 |
+
"count": 3,
|
| 345 |
+
"results": [
|
| 346 |
+
{
|
| 347 |
+
"id": 1,
|
| 348 |
+
"category": "Transport",
|
| 349 |
+
"limit": "50000.00",
|
| 350 |
+
"color": "#3B82F6",
|
| 351 |
+
"spent_amount": "32500.00",
|
| 352 |
+
"percentage": 65.0
|
| 353 |
+
}
|
| 354 |
+
]
|
| 355 |
+
}
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
### Modifier un Budget
|
| 359 |
+
|
| 360 |
+
```bash
|
| 361 |
+
curl -X PUT http://localhost:8000/api/budgets/1/ \
|
| 362 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 363 |
+
-H "Content-Type: application/json" \
|
| 364 |
+
-d '{
|
| 365 |
+
"limit": "75000.00"
|
| 366 |
+
}'
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
### Supprimer un Budget
|
| 370 |
+
|
| 371 |
+
```bash
|
| 372 |
+
curl -X DELETE http://localhost:8000/api/budgets/1/ \
|
| 373 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 374 |
+
```
|
| 375 |
+
|
| 376 |
+
## 📢 Annonces
|
| 377 |
+
|
| 378 |
+
### Lister les Annonces (Public)
|
| 379 |
+
|
| 380 |
+
```bash
|
| 381 |
+
curl -X GET http://localhost:8000/api/ads/
|
| 382 |
+
```
|
| 383 |
+
|
| 384 |
+
### Créer une Annonce
|
| 385 |
+
|
| 386 |
+
```bash
|
| 387 |
+
curl -X POST http://localhost:8000/api/ads/ \
|
| 388 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
| 389 |
+
-F "product_name=Engrais Bio" \
|
| 390 |
+
-F "owner_name=FertiTogo" \
|
| 391 |
+
-F "description=Engrais biologique de qualité" \
|
| 392 |
+
-F "whatsapp=+22890123456" \
|
| 393 |
+
-F "location=Lomé, Togo" \
|
| 394 |
+
-F "website=https://fertitogo.com" \
|
| 395 |
+
-F "image=@/path/to/image.jpg"
|
| 396 |
+
```
|
| 397 |
+
|
| 398 |
+
### Rechercher des Annonces
|
| 399 |
+
|
| 400 |
+
```bash
|
| 401 |
+
curl -X GET "http://localhost:8000/api/ads/?search=engrais" \
|
| 402 |
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
| 403 |
+
```
|
| 404 |
+
|
| 405 |
+
## ⚠️ Gestion des Erreurs
|
| 406 |
+
|
| 407 |
+
### Erreur de Validation
|
| 408 |
+
|
| 409 |
+
```json
|
| 410 |
+
{
|
| 411 |
+
"type": "validation_error",
|
| 412 |
+
"errors": {
|
| 413 |
+
"email": ["Cet email existe déjà."],
|
| 414 |
+
"ifu": ["Ce champ est obligatoire pour les comptes professionnels."]
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
```
|
| 418 |
+
|
| 419 |
+
### Erreur d'Authentification
|
| 420 |
+
|
| 421 |
+
```json
|
| 422 |
+
{
|
| 423 |
+
"type": "authentication_error",
|
| 424 |
+
"message": "Token invalide ou expiré"
|
| 425 |
+
}
|
| 426 |
+
```
|
| 427 |
+
|
| 428 |
+
### Erreur de Permission
|
| 429 |
+
|
| 430 |
+
```json
|
| 431 |
+
{
|
| 432 |
+
"type": "permission_error",
|
| 433 |
+
"message": "Vous n'avez pas la permission d'effectuer cette action"
|
| 434 |
+
}
|
| 435 |
+
```
|
| 436 |
+
|
| 437 |
+
## 📝 Notes Importantes
|
| 438 |
+
|
| 439 |
+
1. **Toujours inclure le token** dans l'en-tête `Authorization: Bearer YOUR_TOKEN`
|
| 440 |
+
2. **Les dates** doivent être au format ISO 8601 : `2024-11-27T10:30:00Z`
|
| 441 |
+
3. **Les montants** sont en format décimal avec 2 décimales : `"1000.00"`
|
| 442 |
+
4. **Pagination** : Paramètres `?page=2&page_size=20`
|
| 443 |
+
5. **Upload de fichiers** : Utiliser `multipart/form-data` avec `-F`
|
| 444 |
+
|
| 445 |
+
## 🔄 Exemple Complet de Workflow
|
| 446 |
+
|
| 447 |
+
```bash
|
| 448 |
+
# 1. S'inscrire
|
| 449 |
+
TOKEN=$(curl -X POST http://localhost:8000/api/auth/register/ \
|
| 450 |
+
-H "Content-Type: application/json" \
|
| 451 |
+
-d '{"email":"test@example.com","password":"Test123!","password2":"Test123!","first_name":"Test","last_name":"User","account_type":"personal","agreed":true}' \
|
| 452 |
+
| jq -r '.tokens.access')
|
| 453 |
+
|
| 454 |
+
# 2. Créer un produit
|
| 455 |
+
curl -X POST http://localhost:8000/api/products/ \
|
| 456 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 457 |
+
-H "Content-Type: application/json" \
|
| 458 |
+
-d '{"name":"Tomates","price":"800.00","unit":"Kg","category":"vente","stock_status":"ok"}'
|
| 459 |
+
|
| 460 |
+
# 3. Ajouter une transaction
|
| 461 |
+
curl -X POST http://localhost:8000/api/transactions/ \
|
| 462 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 463 |
+
-H "Content-Type: application/json" \
|
| 464 |
+
-d '{"name":"Vente tomates","amount":"5000.00","type":"income","category":"Ventes","date":"2024-11-27T10:00:00Z"}'
|
| 465 |
+
|
| 466 |
+
# 4. Voir le résumé
|
| 467 |
+
curl -X GET http://localhost:8000/api/transactions/summary/ \
|
| 468 |
+
-H "Authorization: Bearer $TOKEN"
|
| 469 |
+
```
|
api/__init__.py
ADDED
|
File without changes
|
api/admin.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
| 3 |
+
from django.utils.html import format_html
|
| 4 |
+
from .models import User, Product, Transaction, Budget, Ad
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@admin.register(User)
|
| 8 |
+
class UserAdmin(BaseUserAdmin):
|
| 9 |
+
"""Administration des utilisateurs"""
|
| 10 |
+
|
| 11 |
+
list_display = [
|
| 12 |
+
'email', 'first_name', 'last_name', 'account_type',
|
| 13 |
+
'is_premium', 'is_staff', 'date_joined'
|
| 14 |
+
]
|
| 15 |
+
list_filter = ['account_type', 'is_premium', 'is_staff', 'is_active']
|
| 16 |
+
search_fields = ['email', 'first_name', 'last_name', 'business_name']
|
| 17 |
+
ordering = ['-date_joined']
|
| 18 |
+
|
| 19 |
+
fieldsets = (
|
| 20 |
+
('Informations de connexion', {
|
| 21 |
+
'fields': ('email', 'password')
|
| 22 |
+
}),
|
| 23 |
+
('Informations personnelles', {
|
| 24 |
+
'fields': ('first_name', 'last_name', 'phone_number', 'avatar')
|
| 25 |
+
}),
|
| 26 |
+
('Type de compte', {
|
| 27 |
+
'fields': ('account_type', 'is_premium')
|
| 28 |
+
}),
|
| 29 |
+
('Informations Business', {
|
| 30 |
+
'fields': ('business_name', 'sector', 'location', 'ifu', 'business_logo'),
|
| 31 |
+
'classes': ('collapse',)
|
| 32 |
+
}),
|
| 33 |
+
('Paramètres', {
|
| 34 |
+
'fields': ('currency', 'language', 'dark_mode')
|
| 35 |
+
}),
|
| 36 |
+
('Permissions', {
|
| 37 |
+
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
|
| 38 |
+
'classes': ('collapse',)
|
| 39 |
+
}),
|
| 40 |
+
('Dates importantes', {
|
| 41 |
+
'fields': ('last_login', 'date_joined', 'business_agreed_at'),
|
| 42 |
+
'classes': ('collapse',)
|
| 43 |
+
}),
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
add_fieldsets = (
|
| 47 |
+
(None, {
|
| 48 |
+
'classes': ('wide',),
|
| 49 |
+
'fields': ('email', 'password1', 'password2', 'first_name', 'last_name', 'account_type'),
|
| 50 |
+
}),
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@admin.register(Product)
|
| 55 |
+
class ProductAdmin(admin.ModelAdmin):
|
| 56 |
+
"""Administration des produits"""
|
| 57 |
+
|
| 58 |
+
list_display = [
|
| 59 |
+
'name', 'user', 'price', 'unit', 'category',
|
| 60 |
+
'stock_status', 'image_preview', 'created_at'
|
| 61 |
+
]
|
| 62 |
+
list_filter = ['category', 'stock_status', 'created_at']
|
| 63 |
+
search_fields = ['name', 'description', 'user__email']
|
| 64 |
+
ordering = ['-created_at']
|
| 65 |
+
readonly_fields = ['created_at', 'updated_at', 'image_preview']
|
| 66 |
+
|
| 67 |
+
fieldsets = (
|
| 68 |
+
('Informations de base', {
|
| 69 |
+
'fields': ('user', 'name', 'description')
|
| 70 |
+
}),
|
| 71 |
+
('Tarification', {
|
| 72 |
+
'fields': ('price', 'unit')
|
| 73 |
+
}),
|
| 74 |
+
('Catégorisation', {
|
| 75 |
+
'fields': ('category', 'stock_status')
|
| 76 |
+
}),
|
| 77 |
+
('Image', {
|
| 78 |
+
'fields': ('image', 'image_preview')
|
| 79 |
+
}),
|
| 80 |
+
('Dates', {
|
| 81 |
+
'fields': ('created_at', 'updated_at'),
|
| 82 |
+
'classes': ('collapse',)
|
| 83 |
+
}),
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def image_preview(self, obj):
|
| 87 |
+
if obj.image:
|
| 88 |
+
return format_html(
|
| 89 |
+
'<img src="{}" style="max-height: 100px; max-width: 100px;" />',
|
| 90 |
+
obj.image.url
|
| 91 |
+
)
|
| 92 |
+
return "Pas d'image"
|
| 93 |
+
image_preview.short_description = "Aperçu"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@admin.register(Transaction)
|
| 97 |
+
class TransactionAdmin(admin.ModelAdmin):
|
| 98 |
+
"""Administration des transactions"""
|
| 99 |
+
|
| 100 |
+
list_display = [
|
| 101 |
+
'name', 'user', 'amount', 'currency', 'type',
|
| 102 |
+
'category', 'date', 'created_at'
|
| 103 |
+
]
|
| 104 |
+
list_filter = ['type', 'category', 'date', 'created_at']
|
| 105 |
+
search_fields = ['name', 'user__email', 'category']
|
| 106 |
+
ordering = ['-date']
|
| 107 |
+
readonly_fields = ['created_at', 'updated_at']
|
| 108 |
+
date_hierarchy = 'date'
|
| 109 |
+
|
| 110 |
+
fieldsets = (
|
| 111 |
+
('Utilisateur', {
|
| 112 |
+
'fields': ('user',)
|
| 113 |
+
}),
|
| 114 |
+
('Transaction', {
|
| 115 |
+
'fields': ('name', 'amount', 'currency', 'type', 'category', 'date')
|
| 116 |
+
}),
|
| 117 |
+
('Dates système', {
|
| 118 |
+
'fields': ('created_at', 'updated_at'),
|
| 119 |
+
'classes': ('collapse',)
|
| 120 |
+
}),
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
def get_queryset(self, request):
|
| 124 |
+
qs = super().get_queryset(request)
|
| 125 |
+
return qs.select_related('user')
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@admin.register(Budget)
|
| 129 |
+
class BudgetAdmin(admin.ModelAdmin):
|
| 130 |
+
"""Administration des budgets"""
|
| 131 |
+
|
| 132 |
+
list_display = [
|
| 133 |
+
'category', 'user', 'limit', 'spent_display',
|
| 134 |
+
'percentage_display', 'color_preview', 'created_at'
|
| 135 |
+
]
|
| 136 |
+
list_filter = ['created_at']
|
| 137 |
+
search_fields = ['category', 'user__email']
|
| 138 |
+
ordering = ['-created_at']
|
| 139 |
+
readonly_fields = ['created_at', 'updated_at', 'spent_display', 'percentage_display']
|
| 140 |
+
|
| 141 |
+
fieldsets = (
|
| 142 |
+
('Utilisateur', {
|
| 143 |
+
'fields': ('user',)
|
| 144 |
+
}),
|
| 145 |
+
('Budget', {
|
| 146 |
+
'fields': ('category', 'limit', 'color')
|
| 147 |
+
}),
|
| 148 |
+
('Statistiques', {
|
| 149 |
+
'fields': ('spent_display', 'percentage_display'),
|
| 150 |
+
'classes': ('collapse',)
|
| 151 |
+
}),
|
| 152 |
+
('Dates', {
|
| 153 |
+
'fields': ('created_at', 'updated_at'),
|
| 154 |
+
'classes': ('collapse',)
|
| 155 |
+
}),
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
def spent_display(self, obj):
|
| 159 |
+
return f"{obj.get_spent_amount()} FCFA"
|
| 160 |
+
spent_display.short_description = "Montant dépensé"
|
| 161 |
+
|
| 162 |
+
def percentage_display(self, obj):
|
| 163 |
+
spent = obj.get_spent_amount()
|
| 164 |
+
if obj.limit > 0:
|
| 165 |
+
percentage = (spent / obj.limit) * 100
|
| 166 |
+
color = 'green' if percentage < 80 else 'orange' if percentage < 100 else 'red'
|
| 167 |
+
return format_html(
|
| 168 |
+
'<span style="color: {};">{}</span>',
|
| 169 |
+
color, f'{percentage:.1f}%'
|
| 170 |
+
)
|
| 171 |
+
return "0%"
|
| 172 |
+
percentage_display.short_description = "Pourcentage"
|
| 173 |
+
|
| 174 |
+
def color_preview(self, obj):
|
| 175 |
+
return format_html(
|
| 176 |
+
'<div style="width: 30px; height: 30px; background-color: {}; border: 1px solid #ccc;"></div>',
|
| 177 |
+
obj.color
|
| 178 |
+
)
|
| 179 |
+
color_preview.short_description = "Couleur"
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@admin.register(Ad)
|
| 183 |
+
class AdAdmin(admin.ModelAdmin):
|
| 184 |
+
"""Administration des annonces"""
|
| 185 |
+
|
| 186 |
+
list_display = [
|
| 187 |
+
'product_name', 'owner_name', 'location',
|
| 188 |
+
'is_verified', 'image_preview', 'created_at'
|
| 189 |
+
]
|
| 190 |
+
list_filter = ['is_verified', 'created_at']
|
| 191 |
+
search_fields = ['product_name', 'owner_name', 'description', 'location']
|
| 192 |
+
ordering = ['-created_at']
|
| 193 |
+
readonly_fields = ['created_at', 'updated_at', 'image_preview']
|
| 194 |
+
|
| 195 |
+
fieldsets = (
|
| 196 |
+
('Informations de base', {
|
| 197 |
+
'fields': ('user', 'product_name', 'owner_name', 'description')
|
| 198 |
+
}),
|
| 199 |
+
('Localisation & Contact', {
|
| 200 |
+
'fields': ('location', 'whatsapp', 'website')
|
| 201 |
+
}),
|
| 202 |
+
('Image', {
|
| 203 |
+
'fields': ('image', 'image_preview')
|
| 204 |
+
}),
|
| 205 |
+
('Modération', {
|
| 206 |
+
'fields': ('is_verified',)
|
| 207 |
+
}),
|
| 208 |
+
('Dates', {
|
| 209 |
+
'fields': ('created_at', 'updated_at'),
|
| 210 |
+
'classes': ('collapse',)
|
| 211 |
+
}),
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
actions = ['verify_ads', 'unverify_ads']
|
| 215 |
+
|
| 216 |
+
def verify_ads(self, request, queryset):
|
| 217 |
+
count = queryset.update(is_verified=True)
|
| 218 |
+
self.message_user(request, f"{count} annonce(s) vérifiée(s).")
|
| 219 |
+
verify_ads.short_description = "Vérifier les annonces sélectionnées"
|
| 220 |
+
|
| 221 |
+
def unverify_ads(self, request, queryset):
|
| 222 |
+
count = queryset.update(is_verified=False)
|
| 223 |
+
self.message_user(request, f"{count} annonce(s) dé-vérifiée(s).")
|
| 224 |
+
unverify_ads.short_description = "Retirer la vérification"
|
| 225 |
+
|
| 226 |
+
def image_preview(self, obj):
|
| 227 |
+
if obj.image:
|
| 228 |
+
return format_html(
|
| 229 |
+
'<img src="{}" style="max-height: 100px; max-width: 100px;" />',
|
| 230 |
+
obj.image.url
|
| 231 |
+
)
|
| 232 |
+
return "Pas d'image"
|
| 233 |
+
image_preview.short_description = "Aperçu"
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# Personnalisation du site admin
|
| 237 |
+
admin.site.site_header = "Akompta AI Administration"
|
| 238 |
+
admin.site.site_title = "Akompta Admin"
|
| 239 |
+
admin.site.index_title = "Bienvenue sur l'administration Akompta"
|
api/apps.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ApiConfig(AppConfig):
|
| 5 |
+
default_auto_field = 'django.db.models.BigAutoField'
|
| 6 |
+
name = 'api'
|
api/exceptions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework.views import exception_handler
|
| 2 |
+
from rest_framework.exceptions import ValidationError
|
| 3 |
+
from rest_framework.response import Response
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def custom_exception_handler(exc, context):
|
| 7 |
+
"""
|
| 8 |
+
Handler personnalisé pour formater les erreurs selon la spec du frontend
|
| 9 |
+
Format attendu:
|
| 10 |
+
{
|
| 11 |
+
"type": "validation_error",
|
| 12 |
+
"errors": {
|
| 13 |
+
"field_name": ["Error message"]
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
"""
|
| 17 |
+
# Appeler le handler par défaut de DRF
|
| 18 |
+
response = exception_handler(exc, context)
|
| 19 |
+
|
| 20 |
+
if response is not None:
|
| 21 |
+
# Formater les erreurs de validation
|
| 22 |
+
if isinstance(exc, ValidationError):
|
| 23 |
+
custom_response = {
|
| 24 |
+
'type': 'validation_error',
|
| 25 |
+
'errors': response.data
|
| 26 |
+
}
|
| 27 |
+
response.data = custom_response
|
| 28 |
+
|
| 29 |
+
# Pour les autres erreurs, ajouter un type
|
| 30 |
+
else:
|
| 31 |
+
error_type = 'error'
|
| 32 |
+
if response.status_code == 401:
|
| 33 |
+
error_type = 'authentication_error'
|
| 34 |
+
elif response.status_code == 403:
|
| 35 |
+
error_type = 'permission_error'
|
| 36 |
+
elif response.status_code == 404:
|
| 37 |
+
error_type = 'not_found_error'
|
| 38 |
+
elif response.status_code >= 500:
|
| 39 |
+
error_type = 'server_error'
|
| 40 |
+
|
| 41 |
+
custom_response = {
|
| 42 |
+
'type': error_type,
|
| 43 |
+
'message': response.data.get('detail', str(exc))
|
| 44 |
+
}
|
| 45 |
+
response.data = custom_response
|
| 46 |
+
|
| 47 |
+
return response
|
api/gemini_service.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from google import genai
|
| 5 |
+
from google.genai import types
|
| 6 |
+
from django.conf import settings
|
| 7 |
+
|
| 8 |
+
class GeminiService:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
# Try to load from environment first
|
| 11 |
+
self.api_key = os.environ.get("GEMINI_API_KEY")
|
| 12 |
+
|
| 13 |
+
# If not in environment, try loading from .env file directly
|
| 14 |
+
if not self.api_key:
|
| 15 |
+
env_path = Path(__file__).resolve().parent.parent / '.env'
|
| 16 |
+
if env_path.exists():
|
| 17 |
+
with open(env_path, 'r') as f:
|
| 18 |
+
for line in f:
|
| 19 |
+
line = line.strip()
|
| 20 |
+
if line.startswith('GEMINI_API_KEY='):
|
| 21 |
+
self.api_key = line.split('=', 1)[1].strip()
|
| 22 |
+
break
|
| 23 |
+
|
| 24 |
+
if not self.api_key or self.api_key == 'your-gemini-api-key-here':
|
| 25 |
+
raise ValueError("GEMINI_API_KEY not found or invalid. Please set it in backend/.env file")
|
| 26 |
+
|
| 27 |
+
self.client = genai.Client(api_key=self.api_key)
|
| 28 |
+
# Use free tier model with generous quotas
|
| 29 |
+
self.model = "gemini-1.5-flash" # Free tier: 15 RPM, 1M tokens/day
|
| 30 |
+
|
| 31 |
+
def process_voice_command(self, audio_bytes, mime_type="audio/mp3"):
|
| 32 |
+
"""
|
| 33 |
+
Process audio bytes to extract transaction details.
|
| 34 |
+
Returns a dictionary with transcription and structured data.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
prompt = """
|
| 38 |
+
You are an AI assistant for a financial app called Akompta.
|
| 39 |
+
Your task is to listen to the user's voice command and extract transaction details.
|
| 40 |
+
|
| 41 |
+
The user might say things like:
|
| 42 |
+
- "J'ai vendu la tomate pour 500FCFA le Kilo" (Income)
|
| 43 |
+
- "J'ai payé un ordinateur à 300000FCFA" (Expense)
|
| 44 |
+
|
| 45 |
+
Please perform the following:
|
| 46 |
+
1. Transcribe the audio exactly as spoken (in French).
|
| 47 |
+
2. Analyze the intent and extract structured data.
|
| 48 |
+
|
| 49 |
+
Return ONLY a JSON object with the following structure:
|
| 50 |
+
{
|
| 51 |
+
"transcription": "The exact transcription",
|
| 52 |
+
"intent": "create_transaction",
|
| 53 |
+
"data": {
|
| 54 |
+
"type": "income" or "expense",
|
| 55 |
+
"amount": number (e.g. 500),
|
| 56 |
+
"currency": "FCFA" or other,
|
| 57 |
+
"category": "Category name (e.g. Vente, Alimentation, Transport, Technologie)",
|
| 58 |
+
"name": "Description of the item or service",
|
| 59 |
+
"date": "YYYY-MM-DD" or null if not specified (assume today if null)
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
If the audio is not clear or not related to a transaction, return:
|
| 64 |
+
{
|
| 65 |
+
"transcription": "...",
|
| 66 |
+
"intent": "unknown",
|
| 67 |
+
"error": "Reason"
|
| 68 |
+
}
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
response = self.client.models.generate_content(
|
| 73 |
+
model=self.model,
|
| 74 |
+
contents=[
|
| 75 |
+
types.Content(
|
| 76 |
+
parts=[
|
| 77 |
+
types.Part.from_bytes(data=audio_bytes, mime_type=mime_type),
|
| 78 |
+
types.Part.from_text(text=prompt)
|
| 79 |
+
]
|
| 80 |
+
)
|
| 81 |
+
],
|
| 82 |
+
config=types.GenerateContentConfig(
|
| 83 |
+
response_mime_type="application/json"
|
| 84 |
+
)
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
result = json.loads(response.text)
|
| 88 |
+
return result
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
print(f"Error calling Gemini: {e}")
|
| 92 |
+
return {
|
| 93 |
+
"transcription": "",
|
| 94 |
+
"intent": "error",
|
| 95 |
+
"error": str(e)
|
| 96 |
+
}
|
api/management/__init__.py
ADDED
|
File without changes
|
api/management/commands/__init__.py
ADDED
|
File without changes
|
api/management/commands/seed_data.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Commande Django pour créer des données de test
|
| 3 |
+
Usage: python manage.py seed_data
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from django.core.management.base import BaseCommand
|
| 7 |
+
from django.contrib.auth import get_user_model
|
| 8 |
+
from django.utils import timezone
|
| 9 |
+
from decimal import Decimal
|
| 10 |
+
from datetime import timedelta
|
| 11 |
+
import random
|
| 12 |
+
|
| 13 |
+
from api.models import Product, Transaction, Budget, Ad
|
| 14 |
+
|
| 15 |
+
User = get_user_model()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Command(BaseCommand):
|
| 19 |
+
help = 'Crée des données de test pour Akompta AI'
|
| 20 |
+
|
| 21 |
+
def add_arguments(self, parser):
|
| 22 |
+
parser.add_argument(
|
| 23 |
+
'--clear',
|
| 24 |
+
action='store_true',
|
| 25 |
+
help='Supprimer toutes les données existantes avant de créer de nouvelles données',
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def handle(self, *args, **options):
|
| 29 |
+
if options['clear']:
|
| 30 |
+
self.stdout.write(self.style.WARNING('Suppression des données existantes...'))
|
| 31 |
+
Product.objects.all().delete()
|
| 32 |
+
Transaction.objects.all().delete()
|
| 33 |
+
Budget.objects.all().delete()
|
| 34 |
+
Ad.objects.all().delete()
|
| 35 |
+
User.objects.filter(is_superuser=False).delete()
|
| 36 |
+
self.stdout.write(self.style.SUCCESS('✓ Données supprimées'))
|
| 37 |
+
|
| 38 |
+
# Créer des utilisateurs de test
|
| 39 |
+
self.stdout.write('Création des utilisateurs...')
|
| 40 |
+
|
| 41 |
+
# Utilisateur personnel
|
| 42 |
+
personal_user, created = User.objects.get_or_create(
|
| 43 |
+
email='demo@akompta.com',
|
| 44 |
+
defaults={
|
| 45 |
+
'first_name': 'Demo',
|
| 46 |
+
'last_name': 'User',
|
| 47 |
+
'account_type': 'personal',
|
| 48 |
+
'phone_number': '+22890123456',
|
| 49 |
+
}
|
| 50 |
+
)
|
| 51 |
+
if created:
|
| 52 |
+
personal_user.set_password('demo123')
|
| 53 |
+
personal_user.save()
|
| 54 |
+
self.stdout.write(self.style.SUCCESS(f'✓ Utilisateur personnel créé: {personal_user.email}'))
|
| 55 |
+
|
| 56 |
+
# Utilisateur business
|
| 57 |
+
business_user, created = User.objects.get_or_create(
|
| 58 |
+
email='business@akompta.com',
|
| 59 |
+
defaults={
|
| 60 |
+
'first_name': 'Business',
|
| 61 |
+
'last_name': 'Owner',
|
| 62 |
+
'account_type': 'business',
|
| 63 |
+
'business_name': 'AgriTech Solutions',
|
| 64 |
+
'sector': 'Agriculture',
|
| 65 |
+
'location': 'Lomé, Togo',
|
| 66 |
+
'ifu': '1234567890123',
|
| 67 |
+
'phone_number': '+22890987654',
|
| 68 |
+
}
|
| 69 |
+
)
|
| 70 |
+
if created:
|
| 71 |
+
business_user.set_password('business123')
|
| 72 |
+
business_user.business_agreed = True
|
| 73 |
+
business_user.business_agreed_at = timezone.now()
|
| 74 |
+
business_user.save()
|
| 75 |
+
self.stdout.write(self.style.SUCCESS(f'✓ Utilisateur business créé: {business_user.email}'))
|
| 76 |
+
|
| 77 |
+
# Créer des produits
|
| 78 |
+
self.stdout.write('Création des produits...')
|
| 79 |
+
products_data = [
|
| 80 |
+
{'name': 'Tomates', 'price': '800', 'unit': 'Kg', 'category': 'vente', 'stock_status': 'ok'},
|
| 81 |
+
{'name': 'Oignons', 'price': '600', 'unit': 'Kg', 'category': 'vente', 'stock_status': 'low'},
|
| 82 |
+
{'name': 'Riz', 'price': '450', 'unit': 'Kg', 'category': 'stock', 'stock_status': 'ok'},
|
| 83 |
+
{'name': 'Huile', 'price': '2500', 'unit': 'Litre', 'category': 'stock', 'stock_status': 'rupture'},
|
| 84 |
+
{'name': 'Maïs', 'price': '350', 'unit': 'Kg', 'category': 'vente', 'stock_status': 'ok'},
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
for user in [personal_user, business_user]:
|
| 88 |
+
for prod_data in products_data:
|
| 89 |
+
Product.objects.get_or_create(
|
| 90 |
+
user=user,
|
| 91 |
+
name=prod_data['name'],
|
| 92 |
+
defaults={
|
| 93 |
+
'description': f'{prod_data["name"]} de qualité premium',
|
| 94 |
+
'price': Decimal(prod_data['price']),
|
| 95 |
+
'unit': prod_data['unit'],
|
| 96 |
+
'category': prod_data['category'],
|
| 97 |
+
'stock_status': prod_data['stock_status'],
|
| 98 |
+
}
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
self.stdout.write(self.style.SUCCESS(f'✓ {len(products_data) * 2} produits créés'))
|
| 102 |
+
|
| 103 |
+
# Créer des transactions
|
| 104 |
+
self.stdout.write('Création des transactions...')
|
| 105 |
+
|
| 106 |
+
categories_income = ['Ventes', 'Services', 'Consultation']
|
| 107 |
+
categories_expense = ['Transport', 'Loyer', 'Achats', 'Marketing', 'Salaires']
|
| 108 |
+
|
| 109 |
+
now = timezone.now()
|
| 110 |
+
transaction_count = 0
|
| 111 |
+
|
| 112 |
+
for user in [personal_user, business_user]:
|
| 113 |
+
# Transactions des 30 derniers jours
|
| 114 |
+
for i in range(50):
|
| 115 |
+
days_ago = random.randint(0, 30)
|
| 116 |
+
trans_date = now - timedelta(days=days_ago)
|
| 117 |
+
|
| 118 |
+
trans_type = random.choice(['income', 'expense'])
|
| 119 |
+
|
| 120 |
+
if trans_type == 'income':
|
| 121 |
+
category = random.choice(categories_income)
|
| 122 |
+
amount = Decimal(random.randint(5000, 50000))
|
| 123 |
+
name = f'Vente {category}'
|
| 124 |
+
else:
|
| 125 |
+
category = random.choice(categories_expense)
|
| 126 |
+
amount = Decimal(random.randint(1000, 30000))
|
| 127 |
+
name = f'Dépense {category}'
|
| 128 |
+
|
| 129 |
+
Transaction.objects.create(
|
| 130 |
+
user=user,
|
| 131 |
+
name=name,
|
| 132 |
+
amount=amount,
|
| 133 |
+
type=trans_type,
|
| 134 |
+
category=category,
|
| 135 |
+
date=trans_date,
|
| 136 |
+
currency='FCFA'
|
| 137 |
+
)
|
| 138 |
+
transaction_count += 1
|
| 139 |
+
|
| 140 |
+
self.stdout.write(self.style.SUCCESS(f'✓ {transaction_count} transactions créées'))
|
| 141 |
+
|
| 142 |
+
# Créer des budgets
|
| 143 |
+
self.stdout.write('Création des budgets...')
|
| 144 |
+
|
| 145 |
+
budgets_data = [
|
| 146 |
+
{'category': 'Transport', 'limit': '50000', 'color': '#3B82F6'},
|
| 147 |
+
{'category': 'Marketing', 'limit': '100000', 'color': '#EF4444'},
|
| 148 |
+
{'category': 'Achats', 'limit': '200000', 'color': '#10B981'},
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
budget_count = 0
|
| 152 |
+
for user in [personal_user, business_user]:
|
| 153 |
+
for budget_data in budgets_data:
|
| 154 |
+
Budget.objects.get_or_create(
|
| 155 |
+
user=user,
|
| 156 |
+
category=budget_data['category'],
|
| 157 |
+
defaults={
|
| 158 |
+
'limit': Decimal(budget_data['limit']),
|
| 159 |
+
'color': budget_data['color'],
|
| 160 |
+
}
|
| 161 |
+
)
|
| 162 |
+
budget_count += 1
|
| 163 |
+
|
| 164 |
+
self.stdout.write(self.style.SUCCESS(f'✓ {budget_count} budgets créés'))
|
| 165 |
+
|
| 166 |
+
# Créer des annonces
|
| 167 |
+
self.stdout.write('Création des annonces...')
|
| 168 |
+
|
| 169 |
+
ads_data = [
|
| 170 |
+
{
|
| 171 |
+
'product_name': 'Engrais Bio Premium',
|
| 172 |
+
'owner_name': 'FertiTogo',
|
| 173 |
+
'description': 'Engrais biologique de haute qualité pour toutes cultures. Augmentez vos rendements naturellement.',
|
| 174 |
+
'whatsapp': '+22890111222',
|
| 175 |
+
'location': 'Lomé, Togo',
|
| 176 |
+
'is_verified': True,
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
'product_name': 'Système d\'irrigation automatique',
|
| 180 |
+
'owner_name': 'AgroTech Solutions',
|
| 181 |
+
'description': 'Solutions d\'irrigation modernes pour optimiser votre consommation d\'eau.',
|
| 182 |
+
'whatsapp': '+22890333444',
|
| 183 |
+
'location': 'Kara, Togo',
|
| 184 |
+
'is_verified': True,
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
'product_name': 'Semences certifiées',
|
| 188 |
+
'owner_name': 'SeedCorp Afrique',
|
| 189 |
+
'description': 'Semences de maïs, riz et soja certifiées et adaptées au climat ouest-africain.',
|
| 190 |
+
'whatsapp': '+22890555666',
|
| 191 |
+
'location': 'Sokodé, Togo',
|
| 192 |
+
'is_verified': True,
|
| 193 |
+
},
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
for ad_data in ads_data:
|
| 197 |
+
Ad.objects.get_or_create(
|
| 198 |
+
user=business_user,
|
| 199 |
+
product_name=ad_data['product_name'],
|
| 200 |
+
defaults=ad_data
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
self.stdout.write(self.style.SUCCESS(f'✓ {len(ads_data)} annonces créées'))
|
| 204 |
+
|
| 205 |
+
# Résumé
|
| 206 |
+
self.stdout.write(self.style.SUCCESS('\n' + '='*50))
|
| 207 |
+
self.stdout.write(self.style.SUCCESS('DONNÉES DE TEST CRÉÉES AVEC SUCCÈS'))
|
| 208 |
+
self.stdout.write(self.style.SUCCESS('='*50))
|
| 209 |
+
self.stdout.write(self.style.SUCCESS('\nComptes de test :'))
|
| 210 |
+
self.stdout.write(f' • Personnel: demo@akompta.com / demo123')
|
| 211 |
+
self.stdout.write(f' • Business: business@akompta.com / business123')
|
| 212 |
+
self.stdout.write(self.style.SUCCESS('\nVous pouvez maintenant tester l\'API !'))
|
| 213 |
+
|
api/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.8 on 2025-11-28 00:35
|
| 2 |
+
|
| 3 |
+
import django.contrib.auth.models
|
| 4 |
+
import django.db.models.deletion
|
| 5 |
+
import django.utils.timezone
|
| 6 |
+
from django.conf import settings
|
| 7 |
+
from django.db import migrations, models
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Migration(migrations.Migration):
|
| 11 |
+
|
| 12 |
+
initial = True
|
| 13 |
+
|
| 14 |
+
dependencies = [
|
| 15 |
+
('auth', '0012_alter_user_first_name_max_length'),
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
operations = [
|
| 19 |
+
migrations.CreateModel(
|
| 20 |
+
name='User',
|
| 21 |
+
fields=[
|
| 22 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 23 |
+
('password', models.CharField(max_length=128, verbose_name='password')),
|
| 24 |
+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
| 25 |
+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
| 26 |
+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
| 27 |
+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
| 28 |
+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
| 29 |
+
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
| 30 |
+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
| 31 |
+
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
| 32 |
+
('phone_number', models.CharField(blank=True, max_length=20)),
|
| 33 |
+
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
| 34 |
+
('account_type', models.CharField(choices=[('personal', 'Personnel'), ('business', 'Professionnel')], default='personal', max_length=10)),
|
| 35 |
+
('is_premium', models.BooleanField(default=False)),
|
| 36 |
+
('business_name', models.CharField(blank=True, max_length=255)),
|
| 37 |
+
('sector', models.CharField(blank=True, max_length=100)),
|
| 38 |
+
('location', models.CharField(blank=True, max_length=255)),
|
| 39 |
+
('ifu', models.CharField(blank=True, max_length=50, verbose_name='Identifiant Fiscal Unique')),
|
| 40 |
+
('business_logo', models.ImageField(blank=True, null=True, upload_to='business_logos/')),
|
| 41 |
+
('currency', models.CharField(default='XOF', max_length=10)),
|
| 42 |
+
('language', models.CharField(default='FR', max_length=5)),
|
| 43 |
+
('dark_mode', models.BooleanField(default=False)),
|
| 44 |
+
('agreed_terms', models.BooleanField(default=False)),
|
| 45 |
+
('business_agreed', models.BooleanField(default=False)),
|
| 46 |
+
('business_agreed_at', models.DateTimeField(blank=True, null=True)),
|
| 47 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 48 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 49 |
+
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
| 50 |
+
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
| 51 |
+
],
|
| 52 |
+
options={
|
| 53 |
+
'verbose_name': 'Utilisateur',
|
| 54 |
+
'verbose_name_plural': 'Utilisateurs',
|
| 55 |
+
},
|
| 56 |
+
managers=[
|
| 57 |
+
('objects', django.contrib.auth.models.UserManager()),
|
| 58 |
+
],
|
| 59 |
+
),
|
| 60 |
+
migrations.CreateModel(
|
| 61 |
+
name='Ad',
|
| 62 |
+
fields=[
|
| 63 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 64 |
+
('product_name', models.CharField(max_length=255)),
|
| 65 |
+
('owner_name', models.CharField(max_length=255)),
|
| 66 |
+
('description', models.TextField()),
|
| 67 |
+
('image', models.ImageField(upload_to='ads/')),
|
| 68 |
+
('whatsapp', models.CharField(max_length=20)),
|
| 69 |
+
('website', models.URLField(blank=True, null=True)),
|
| 70 |
+
('location', models.CharField(max_length=255)),
|
| 71 |
+
('is_verified', models.BooleanField(default=False)),
|
| 72 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 73 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 74 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ads', to=settings.AUTH_USER_MODEL)),
|
| 75 |
+
],
|
| 76 |
+
options={
|
| 77 |
+
'verbose_name': 'Annonce',
|
| 78 |
+
'verbose_name_plural': 'Annonces',
|
| 79 |
+
'ordering': ['-created_at'],
|
| 80 |
+
},
|
| 81 |
+
),
|
| 82 |
+
migrations.CreateModel(
|
| 83 |
+
name='Product',
|
| 84 |
+
fields=[
|
| 85 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 86 |
+
('name', models.CharField(max_length=255)),
|
| 87 |
+
('description', models.TextField(blank=True)),
|
| 88 |
+
('price', models.DecimalField(decimal_places=2, max_digits=15)),
|
| 89 |
+
('unit', models.CharField(default='Unité', max_length=50)),
|
| 90 |
+
('image', models.ImageField(blank=True, null=True, upload_to='products/')),
|
| 91 |
+
('category', models.CharField(choices=[('vente', 'Vente'), ('depense', 'Dépense'), ('stock', 'Stock')], max_length=20)),
|
| 92 |
+
('stock_status', models.CharField(choices=[('ok', 'OK'), ('low', 'Faible'), ('rupture', 'Rupture')], default='ok', max_length=10)),
|
| 93 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 94 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 95 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to=settings.AUTH_USER_MODEL)),
|
| 96 |
+
],
|
| 97 |
+
options={
|
| 98 |
+
'verbose_name': 'Produit',
|
| 99 |
+
'verbose_name_plural': 'Produits',
|
| 100 |
+
'ordering': ['-created_at'],
|
| 101 |
+
},
|
| 102 |
+
),
|
| 103 |
+
migrations.CreateModel(
|
| 104 |
+
name='Budget',
|
| 105 |
+
fields=[
|
| 106 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 107 |
+
('category', models.CharField(max_length=100)),
|
| 108 |
+
('limit', models.DecimalField(decimal_places=2, max_digits=15)),
|
| 109 |
+
('color', models.CharField(default='#4F46E5', max_length=7)),
|
| 110 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 111 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 112 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='budgets', to=settings.AUTH_USER_MODEL)),
|
| 113 |
+
],
|
| 114 |
+
options={
|
| 115 |
+
'verbose_name': 'Budget',
|
| 116 |
+
'verbose_name_plural': 'Budgets',
|
| 117 |
+
'unique_together': {('user', 'category')},
|
| 118 |
+
},
|
| 119 |
+
),
|
| 120 |
+
migrations.CreateModel(
|
| 121 |
+
name='Transaction',
|
| 122 |
+
fields=[
|
| 123 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 124 |
+
('name', models.CharField(max_length=255)),
|
| 125 |
+
('amount', models.DecimalField(decimal_places=2, max_digits=15)),
|
| 126 |
+
('type', models.CharField(choices=[('income', 'Revenu'), ('expense', 'Dépense')], max_length=10)),
|
| 127 |
+
('category', models.CharField(max_length=100)),
|
| 128 |
+
('date', models.DateTimeField()),
|
| 129 |
+
('currency', models.CharField(default='FCFA', max_length=10)),
|
| 130 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 131 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 132 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to=settings.AUTH_USER_MODEL)),
|
| 133 |
+
],
|
| 134 |
+
options={
|
| 135 |
+
'verbose_name': 'Transaction',
|
| 136 |
+
'verbose_name_plural': 'Transactions',
|
| 137 |
+
'ordering': ['-date'],
|
| 138 |
+
'indexes': [models.Index(fields=['user', 'type'], name='api_transac_user_id_687bb9_idx'), models.Index(fields=['user', 'date'], name='api_transac_user_id_aacb53_idx'), models.Index(fields=['user', 'category'], name='api_transac_user_id_a594d0_idx')],
|
| 139 |
+
},
|
| 140 |
+
),
|
| 141 |
+
]
|
api/migrations/0002_alter_user_managers.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.8 on 2025-11-28 00:40
|
| 2 |
+
|
| 3 |
+
from django.db import migrations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('api', '0001_initial'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AlterModelManagers(
|
| 14 |
+
name='user',
|
| 15 |
+
managers=[
|
| 16 |
+
],
|
| 17 |
+
),
|
| 18 |
+
]
|
api/migrations/0003_notification_supportticket.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.8 on 2025-11-28 08:49
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.conf import settings
|
| 5 |
+
from django.db import migrations, models
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Migration(migrations.Migration):
|
| 9 |
+
|
| 10 |
+
dependencies = [
|
| 11 |
+
('api', '0002_alter_user_managers'),
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
operations = [
|
| 15 |
+
migrations.CreateModel(
|
| 16 |
+
name='Notification',
|
| 17 |
+
fields=[
|
| 18 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 19 |
+
('type', models.CharField(choices=[('reminder', 'Rappel'), ('profit', 'Profit'), ('promo', 'Promo'), ('system', 'Système')], default='system', max_length=20)),
|
| 20 |
+
('title', models.CharField(max_length=255)),
|
| 21 |
+
('message', models.TextField()),
|
| 22 |
+
('is_read', models.BooleanField(default=False)),
|
| 23 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 24 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
| 25 |
+
],
|
| 26 |
+
options={
|
| 27 |
+
'verbose_name': 'Notification',
|
| 28 |
+
'verbose_name_plural': 'Notifications',
|
| 29 |
+
'ordering': ['-created_at'],
|
| 30 |
+
},
|
| 31 |
+
),
|
| 32 |
+
migrations.CreateModel(
|
| 33 |
+
name='SupportTicket',
|
| 34 |
+
fields=[
|
| 35 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 36 |
+
('subject', models.CharField(max_length=255)),
|
| 37 |
+
('message', models.TextField()),
|
| 38 |
+
('status', models.CharField(choices=[('open', 'Ouvert'), ('in_progress', 'En cours'), ('closed', 'Fermé')], default='open', max_length=20)),
|
| 39 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 40 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 41 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='support_tickets', to=settings.AUTH_USER_MODEL)),
|
| 42 |
+
],
|
| 43 |
+
options={
|
| 44 |
+
'verbose_name': 'Ticket Support',
|
| 45 |
+
'verbose_name_plural': 'Tickets Support',
|
| 46 |
+
'ordering': ['-created_at'],
|
| 47 |
+
},
|
| 48 |
+
),
|
| 49 |
+
]
|
api/migrations/__init__.py
ADDED
|
File without changes
|
api/models.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
from django.contrib.auth.models import AbstractUser, BaseUserManager
|
| 3 |
+
from django.core.validators import RegexValidator
|
| 4 |
+
from decimal import Decimal
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class UserManager(BaseUserManager):
|
| 8 |
+
"""Custom user manager for email-based authentication"""
|
| 9 |
+
|
| 10 |
+
def create_user(self, email, password=None, **extra_fields):
|
| 11 |
+
"""Create and save a regular user with the given email and password"""
|
| 12 |
+
if not email:
|
| 13 |
+
raise ValueError('L\'adresse email est obligatoire')
|
| 14 |
+
|
| 15 |
+
email = self.normalize_email(email)
|
| 16 |
+
user = self.model(email=email, **extra_fields)
|
| 17 |
+
user.set_password(password)
|
| 18 |
+
user.save(using=self._db)
|
| 19 |
+
return user
|
| 20 |
+
|
| 21 |
+
def create_superuser(self, email, password=None, **extra_fields):
|
| 22 |
+
"""Create and save a superuser with the given email and password"""
|
| 23 |
+
extra_fields.setdefault('is_staff', True)
|
| 24 |
+
extra_fields.setdefault('is_superuser', True)
|
| 25 |
+
extra_fields.setdefault('is_active', True)
|
| 26 |
+
|
| 27 |
+
if extra_fields.get('is_staff') is not True:
|
| 28 |
+
raise ValueError('Le superutilisateur doit avoir is_staff=True.')
|
| 29 |
+
if extra_fields.get('is_superuser') is not True:
|
| 30 |
+
raise ValueError('Le superutilisateur doit avoir is_superuser=True.')
|
| 31 |
+
|
| 32 |
+
return self.create_user(email, password, **extra_fields)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class User(AbstractUser):
|
| 36 |
+
"""Modèle utilisateur étendu pour Akompta"""
|
| 37 |
+
|
| 38 |
+
ACCOUNT_TYPE_CHOICES = [
|
| 39 |
+
('personal', 'Personnel'),
|
| 40 |
+
('business', 'Professionnel'),
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
# Utiliser email comme identifiant
|
| 44 |
+
username = None
|
| 45 |
+
email = models.EmailField(unique=True, verbose_name="Email")
|
| 46 |
+
|
| 47 |
+
# Champs communs
|
| 48 |
+
phone_number = models.CharField(max_length=20, blank=True)
|
| 49 |
+
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
| 50 |
+
account_type = models.CharField(
|
| 51 |
+
max_length=10,
|
| 52 |
+
choices=ACCOUNT_TYPE_CHOICES,
|
| 53 |
+
default='personal'
|
| 54 |
+
)
|
| 55 |
+
is_premium = models.BooleanField(default=False)
|
| 56 |
+
|
| 57 |
+
# Champs Business
|
| 58 |
+
business_name = models.CharField(max_length=255, blank=True)
|
| 59 |
+
sector = models.CharField(max_length=100, blank=True)
|
| 60 |
+
location = models.CharField(max_length=255, blank=True)
|
| 61 |
+
ifu = models.CharField(
|
| 62 |
+
max_length=50,
|
| 63 |
+
blank=True,
|
| 64 |
+
verbose_name="Identifiant Fiscal Unique"
|
| 65 |
+
)
|
| 66 |
+
business_logo = models.ImageField(upload_to='business_logos/', blank=True, null=True)
|
| 67 |
+
|
| 68 |
+
# Settings (Stockés en JSON)
|
| 69 |
+
currency = models.CharField(max_length=10, default='XOF')
|
| 70 |
+
language = models.CharField(max_length=5, default='FR')
|
| 71 |
+
dark_mode = models.BooleanField(default=False)
|
| 72 |
+
|
| 73 |
+
# Acceptation des conditions
|
| 74 |
+
agreed_terms = models.BooleanField(default=False)
|
| 75 |
+
business_agreed = models.BooleanField(default=False)
|
| 76 |
+
business_agreed_at = models.DateTimeField(blank=True, null=True)
|
| 77 |
+
|
| 78 |
+
# Timestamps
|
| 79 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 80 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 81 |
+
|
| 82 |
+
# Custom manager
|
| 83 |
+
objects = UserManager()
|
| 84 |
+
|
| 85 |
+
USERNAME_FIELD = 'email'
|
| 86 |
+
REQUIRED_FIELDS = ['first_name', 'last_name']
|
| 87 |
+
|
| 88 |
+
class Meta:
|
| 89 |
+
verbose_name = "Utilisateur"
|
| 90 |
+
verbose_name_plural = "Utilisateurs"
|
| 91 |
+
|
| 92 |
+
def __str__(self):
|
| 93 |
+
return self.email
|
| 94 |
+
|
| 95 |
+
def save(self, *args, **kwargs):
|
| 96 |
+
# Validation IFU pour les comptes business
|
| 97 |
+
if self.account_type == 'business' and not self.ifu:
|
| 98 |
+
from django.core.exceptions import ValidationError
|
| 99 |
+
raise ValidationError(
|
| 100 |
+
"Le champ IFU est obligatoire pour les comptes professionnels."
|
| 101 |
+
)
|
| 102 |
+
super().save(*args, **kwargs)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class Product(models.Model):
|
| 106 |
+
"""Modèle pour les produits/inventaire"""
|
| 107 |
+
|
| 108 |
+
CATEGORY_CHOICES = [
|
| 109 |
+
('vente', 'Vente'),
|
| 110 |
+
('depense', 'Dépense'),
|
| 111 |
+
('stock', 'Stock'),
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
STOCK_STATUS_CHOICES = [
|
| 115 |
+
('ok', 'OK'),
|
| 116 |
+
('low', 'Faible'),
|
| 117 |
+
('rupture', 'Rupture'),
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products')
|
| 121 |
+
name = models.CharField(max_length=255)
|
| 122 |
+
description = models.TextField(blank=True)
|
| 123 |
+
price = models.DecimalField(max_digits=15, decimal_places=2)
|
| 124 |
+
unit = models.CharField(max_length=50, default='Unité')
|
| 125 |
+
image = models.ImageField(upload_to='products/', blank=True, null=True)
|
| 126 |
+
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
|
| 127 |
+
stock_status = models.CharField(
|
| 128 |
+
max_length=10,
|
| 129 |
+
choices=STOCK_STATUS_CHOICES,
|
| 130 |
+
default='ok'
|
| 131 |
+
)
|
| 132 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 133 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 134 |
+
|
| 135 |
+
class Meta:
|
| 136 |
+
verbose_name = "Produit"
|
| 137 |
+
verbose_name_plural = "Produits"
|
| 138 |
+
ordering = ['-created_at']
|
| 139 |
+
|
| 140 |
+
def __str__(self):
|
| 141 |
+
return f"{self.name} - {self.user.email}"
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class Transaction(models.Model):
|
| 145 |
+
"""Modèle pour les transactions financières"""
|
| 146 |
+
|
| 147 |
+
TYPE_CHOICES = [
|
| 148 |
+
('income', 'Revenu'),
|
| 149 |
+
('expense', 'Dépense'),
|
| 150 |
+
]
|
| 151 |
+
|
| 152 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions')
|
| 153 |
+
name = models.CharField(max_length=255)
|
| 154 |
+
amount = models.DecimalField(max_digits=15, decimal_places=2)
|
| 155 |
+
type = models.CharField(max_length=10, choices=TYPE_CHOICES)
|
| 156 |
+
category = models.CharField(max_length=100)
|
| 157 |
+
date = models.DateTimeField()
|
| 158 |
+
currency = models.CharField(max_length=10, default='FCFA')
|
| 159 |
+
|
| 160 |
+
# Support pour la synchro hors-ligne
|
| 161 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 162 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 163 |
+
|
| 164 |
+
class Meta:
|
| 165 |
+
verbose_name = "Transaction"
|
| 166 |
+
verbose_name_plural = "Transactions"
|
| 167 |
+
ordering = ['-date']
|
| 168 |
+
indexes = [
|
| 169 |
+
models.Index(fields=['user', 'type']),
|
| 170 |
+
models.Index(fields=['user', 'date']),
|
| 171 |
+
models.Index(fields=['user', 'category']),
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
def __str__(self):
|
| 175 |
+
return f"{self.name} - {self.amount} {self.currency}"
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class Budget(models.Model):
|
| 179 |
+
"""Modèle pour les budgets suivis"""
|
| 180 |
+
|
| 181 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='budgets')
|
| 182 |
+
category = models.CharField(max_length=100)
|
| 183 |
+
limit = models.DecimalField(max_digits=15, decimal_places=2)
|
| 184 |
+
color = models.CharField(max_length=7, default='#4F46E5') # Hex color
|
| 185 |
+
|
| 186 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 187 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 188 |
+
|
| 189 |
+
class Meta:
|
| 190 |
+
verbose_name = "Budget"
|
| 191 |
+
verbose_name_plural = "Budgets"
|
| 192 |
+
unique_together = ['user', 'category']
|
| 193 |
+
|
| 194 |
+
def __str__(self):
|
| 195 |
+
return f"{self.category} - {self.limit}"
|
| 196 |
+
|
| 197 |
+
def get_spent_amount(self):
|
| 198 |
+
"""Calcule le montant dépensé pour cette catégorie"""
|
| 199 |
+
from django.db.models import Sum
|
| 200 |
+
result = self.user.transactions.filter(
|
| 201 |
+
type='expense',
|
| 202 |
+
category=self.category
|
| 203 |
+
).aggregate(total=Sum('amount'))
|
| 204 |
+
return result['total'] or Decimal('0.00')
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
class Ad(models.Model):
|
| 208 |
+
"""Modèle pour les annonces partenaires"""
|
| 209 |
+
|
| 210 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ads')
|
| 211 |
+
product_name = models.CharField(max_length=255)
|
| 212 |
+
owner_name = models.CharField(max_length=255)
|
| 213 |
+
description = models.TextField()
|
| 214 |
+
image = models.ImageField(upload_to='ads/')
|
| 215 |
+
whatsapp = models.CharField(max_length=20)
|
| 216 |
+
website = models.URLField(blank=True, null=True)
|
| 217 |
+
location = models.CharField(max_length=255)
|
| 218 |
+
is_verified = models.BooleanField(default=False)
|
| 219 |
+
|
| 220 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 221 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 222 |
+
|
| 223 |
+
class Meta:
|
| 224 |
+
verbose_name = "Annonce"
|
| 225 |
+
verbose_name_plural = "Annonces"
|
| 226 |
+
ordering = ['-created_at']
|
| 227 |
+
|
| 228 |
+
def __str__(self):
|
| 229 |
+
return f"{self.product_name} - {self.owner_name}"
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class Notification(models.Model):
|
| 233 |
+
"""Modèle pour les notifications"""
|
| 234 |
+
|
| 235 |
+
TYPE_CHOICES = [
|
| 236 |
+
('reminder', 'Rappel'),
|
| 237 |
+
('profit', 'Profit'),
|
| 238 |
+
('promo', 'Promo'),
|
| 239 |
+
('system', 'Système'),
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
| 243 |
+
type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='system')
|
| 244 |
+
title = models.CharField(max_length=255)
|
| 245 |
+
message = models.TextField()
|
| 246 |
+
is_read = models.BooleanField(default=False)
|
| 247 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 248 |
+
|
| 249 |
+
class Meta:
|
| 250 |
+
verbose_name = "Notification"
|
| 251 |
+
verbose_name_plural = "Notifications"
|
| 252 |
+
ordering = ['-created_at']
|
| 253 |
+
|
| 254 |
+
def __str__(self):
|
| 255 |
+
return f"{self.title} - {self.user.email}"
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class SupportTicket(models.Model):
|
| 259 |
+
"""Modèle pour le support client"""
|
| 260 |
+
|
| 261 |
+
STATUS_CHOICES = [
|
| 262 |
+
('open', 'Ouvert'),
|
| 263 |
+
('in_progress', 'En cours'),
|
| 264 |
+
('closed', 'Fermé'),
|
| 265 |
+
]
|
| 266 |
+
|
| 267 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='support_tickets')
|
| 268 |
+
subject = models.CharField(max_length=255)
|
| 269 |
+
message = models.TextField()
|
| 270 |
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
|
| 271 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 272 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 273 |
+
|
| 274 |
+
class Meta:
|
| 275 |
+
verbose_name = "Ticket Support"
|
| 276 |
+
verbose_name_plural = "Tickets Support"
|
| 277 |
+
ordering = ['-created_at']
|
| 278 |
+
|
| 279 |
+
def __str__(self):
|
| 280 |
+
return f"{self.subject} - {self.status}"
|
| 281 |
+
|
api/serializers.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import serializers
|
| 2 |
+
from django.contrib.auth import get_user_model
|
| 3 |
+
from django.contrib.auth.password_validation import validate_password
|
| 4 |
+
from django.utils import timezone
|
| 5 |
+
from .models import Product, Transaction, Budget, Ad, Notification, SupportTicket
|
| 6 |
+
|
| 7 |
+
User = get_user_model()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class UserSerializer(serializers.ModelSerializer):
|
| 11 |
+
"""Serializer pour le profil utilisateur"""
|
| 12 |
+
|
| 13 |
+
class Meta:
|
| 14 |
+
model = User
|
| 15 |
+
fields = [
|
| 16 |
+
'id', 'email', 'first_name', 'last_name', 'phone_number',
|
| 17 |
+
'avatar', 'account_type', 'is_premium',
|
| 18 |
+
'business_name', 'sector', 'location', 'ifu', 'business_logo',
|
| 19 |
+
'currency', 'language', 'dark_mode',
|
| 20 |
+
'created_at', 'updated_at'
|
| 21 |
+
]
|
| 22 |
+
read_only_fields = ['id', 'created_at', 'updated_at']
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class RegisterSerializer(serializers.ModelSerializer):
|
| 26 |
+
"""Serializer pour l'inscription"""
|
| 27 |
+
|
| 28 |
+
password = serializers.CharField(
|
| 29 |
+
write_only=True,
|
| 30 |
+
required=True,
|
| 31 |
+
validators=[validate_password]
|
| 32 |
+
)
|
| 33 |
+
password2 = serializers.CharField(write_only=True, required=True)
|
| 34 |
+
agreed = serializers.BooleanField(write_only=True, required=True)
|
| 35 |
+
businessAgreed = serializers.BooleanField(write_only=True, required=False)
|
| 36 |
+
|
| 37 |
+
class Meta:
|
| 38 |
+
model = User
|
| 39 |
+
fields = [
|
| 40 |
+
'email', 'password', 'password2', 'first_name', 'last_name',
|
| 41 |
+
'phone_number', 'account_type', 'business_name', 'sector',
|
| 42 |
+
'location', 'ifu', 'agreed', 'businessAgreed'
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
def validate(self, attrs):
|
| 46 |
+
if attrs['password'] != attrs['password2']:
|
| 47 |
+
raise serializers.ValidationError({
|
| 48 |
+
"password": "Les mots de passe ne correspondent pas."
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
# Validation IFU pour les comptes business
|
| 52 |
+
if attrs.get('account_type') == 'business':
|
| 53 |
+
if not attrs.get('ifu'):
|
| 54 |
+
raise serializers.ValidationError({
|
| 55 |
+
"ifu": "Ce champ est obligatoire pour les comptes professionnels."
|
| 56 |
+
})
|
| 57 |
+
if not attrs.get('businessAgreed'):
|
| 58 |
+
raise serializers.ValidationError({
|
| 59 |
+
"businessAgreed": "Vous devez accepter les conditions professionnelles."
|
| 60 |
+
})
|
| 61 |
+
|
| 62 |
+
if not attrs.get('agreed'):
|
| 63 |
+
raise serializers.ValidationError({
|
| 64 |
+
"agreed": "Vous devez accepter les conditions générales."
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
return attrs
|
| 68 |
+
|
| 69 |
+
def create(self, validated_data):
|
| 70 |
+
validated_data.pop('password2')
|
| 71 |
+
validated_data.pop('agreed')
|
| 72 |
+
business_agreed = validated_data.pop('businessAgreed', False)
|
| 73 |
+
|
| 74 |
+
password = validated_data.pop('password')
|
| 75 |
+
user = User.objects.create_user(password=password, **validated_data)
|
| 76 |
+
|
| 77 |
+
user.agreed_terms = True
|
| 78 |
+
if business_agreed:
|
| 79 |
+
user.business_agreed = True
|
| 80 |
+
user.business_agreed_at = timezone.now()
|
| 81 |
+
user.save()
|
| 82 |
+
|
| 83 |
+
return user
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class ChangePasswordSerializer(serializers.Serializer):
|
| 87 |
+
"""Serializer pour le changement de mot de passe"""
|
| 88 |
+
|
| 89 |
+
old_password = serializers.CharField(required=True)
|
| 90 |
+
new_password = serializers.CharField(
|
| 91 |
+
required=True,
|
| 92 |
+
validators=[validate_password]
|
| 93 |
+
)
|
| 94 |
+
new_password2 = serializers.CharField(required=True)
|
| 95 |
+
|
| 96 |
+
def validate(self, attrs):
|
| 97 |
+
if attrs['new_password'] != attrs['new_password2']:
|
| 98 |
+
raise serializers.ValidationError({
|
| 99 |
+
"new_password": "Les mots de passe ne correspondent pas."
|
| 100 |
+
})
|
| 101 |
+
return attrs
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class ProductSerializer(serializers.ModelSerializer):
|
| 105 |
+
"""Serializer pour les produits"""
|
| 106 |
+
|
| 107 |
+
class Meta:
|
| 108 |
+
model = Product
|
| 109 |
+
fields = [
|
| 110 |
+
'id', 'name', 'description', 'price', 'unit', 'image',
|
| 111 |
+
'category', 'stock_status', 'created_at', 'updated_at'
|
| 112 |
+
]
|
| 113 |
+
read_only_fields = ['id', 'created_at', 'updated_at']
|
| 114 |
+
|
| 115 |
+
def create(self, validated_data):
|
| 116 |
+
validated_data['user'] = self.context['request'].user
|
| 117 |
+
return super().create(validated_data)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class TransactionSerializer(serializers.ModelSerializer):
|
| 121 |
+
"""Serializer pour les transactions"""
|
| 122 |
+
|
| 123 |
+
class Meta:
|
| 124 |
+
model = Transaction
|
| 125 |
+
fields = [
|
| 126 |
+
'id', 'name', 'amount', 'type', 'category', 'date',
|
| 127 |
+
'currency', 'created_at', 'updated_at'
|
| 128 |
+
]
|
| 129 |
+
read_only_fields = ['id', 'created_at', 'updated_at']
|
| 130 |
+
|
| 131 |
+
def create(self, validated_data):
|
| 132 |
+
validated_data['user'] = self.context['request'].user
|
| 133 |
+
return super().create(validated_data)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class TransactionSummarySerializer(serializers.Serializer):
|
| 137 |
+
"""Serializer pour le résumé des transactions (dashboard)"""
|
| 138 |
+
|
| 139 |
+
balance = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 140 |
+
income_24h = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 141 |
+
expenses_24h = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 142 |
+
income_variation = serializers.FloatField()
|
| 143 |
+
expenses_variation = serializers.FloatField()
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class BudgetSerializer(serializers.ModelSerializer):
|
| 147 |
+
"""Serializer pour les budgets"""
|
| 148 |
+
|
| 149 |
+
spent_amount = serializers.SerializerMethodField()
|
| 150 |
+
percentage = serializers.SerializerMethodField()
|
| 151 |
+
|
| 152 |
+
class Meta:
|
| 153 |
+
model = Budget
|
| 154 |
+
fields = [
|
| 155 |
+
'id', 'category', 'limit', 'color', 'spent_amount',
|
| 156 |
+
'percentage', 'created_at', 'updated_at'
|
| 157 |
+
]
|
| 158 |
+
read_only_fields = ['id', 'spent_amount', 'percentage', 'created_at', 'updated_at']
|
| 159 |
+
|
| 160 |
+
def get_spent_amount(self, obj):
|
| 161 |
+
return obj.get_spent_amount()
|
| 162 |
+
|
| 163 |
+
def get_percentage(self, obj):
|
| 164 |
+
spent = obj.get_spent_amount()
|
| 165 |
+
if obj.limit > 0:
|
| 166 |
+
return float((spent / obj.limit) * 100)
|
| 167 |
+
return 0.0
|
| 168 |
+
|
| 169 |
+
def create(self, validated_data):
|
| 170 |
+
validated_data['user'] = self.context['request'].user
|
| 171 |
+
return super().create(validated_data)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class AdSerializer(serializers.ModelSerializer):
|
| 175 |
+
"""Serializer pour les annonces"""
|
| 176 |
+
|
| 177 |
+
class Meta:
|
| 178 |
+
model = Ad
|
| 179 |
+
fields = [
|
| 180 |
+
'id', 'product_name', 'owner_name', 'description', 'image',
|
| 181 |
+
'whatsapp', 'website', 'location', 'is_verified',
|
| 182 |
+
'created_at', 'updated_at'
|
| 183 |
+
]
|
| 184 |
+
read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at']
|
| 185 |
+
|
| 186 |
+
def create(self, validated_data):
|
| 187 |
+
validated_data['user'] = self.context['request'].user
|
| 188 |
+
return super().create(validated_data)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class OverviewAnalyticsSerializer(serializers.Serializer):
|
| 192 |
+
"""Serializer pour les analytics overview (graphique barres)"""
|
| 193 |
+
|
| 194 |
+
month = serializers.CharField()
|
| 195 |
+
income = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 196 |
+
expenses = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class BreakdownAnalyticsSerializer(serializers.Serializer):
|
| 200 |
+
"""Serializer pour le breakdown des dépenses (camembert)"""
|
| 201 |
+
|
| 202 |
+
category = serializers.CharField()
|
| 203 |
+
amount = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 204 |
+
percentage = serializers.FloatField()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
class KPISerializer(serializers.Serializer):
|
| 208 |
+
"""Serializer pour les KPIs"""
|
| 209 |
+
|
| 210 |
+
average_basket = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 211 |
+
estimated_mrr = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 212 |
+
cac = serializers.DecimalField(max_digits=15, decimal_places=2)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class NotificationSerializer(serializers.ModelSerializer):
|
| 216 |
+
"""Serializer pour les notifications"""
|
| 217 |
+
|
| 218 |
+
class Meta:
|
| 219 |
+
model = Notification
|
| 220 |
+
fields = ['id', 'type', 'title', 'message', 'is_read', 'created_at']
|
| 221 |
+
read_only_fields = ['id', 'created_at']
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
class SupportTicketSerializer(serializers.ModelSerializer):
|
| 225 |
+
"""Serializer pour les tickets support"""
|
| 226 |
+
|
| 227 |
+
class Meta:
|
| 228 |
+
model = SupportTicket
|
| 229 |
+
fields = ['id', 'subject', 'message', 'status', 'created_at', 'updated_at']
|
| 230 |
+
read_only_fields = ['id', 'status', 'created_at', 'updated_at']
|
| 231 |
+
|
| 232 |
+
def create(self, validated_data):
|
| 233 |
+
validated_data['user'] = self.context['request'].user
|
| 234 |
+
return super().create(validated_data)
|
api/tests.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
from django.urls import reverse
|
| 3 |
+
from rest_framework.test import APITestCase, APIClient
|
| 4 |
+
from rest_framework import status
|
| 5 |
+
from django.contrib.auth import get_user_model
|
| 6 |
+
from decimal import Decimal
|
| 7 |
+
from django.utils import timezone
|
| 8 |
+
|
| 9 |
+
from .models import Product, Transaction, Budget, Ad
|
| 10 |
+
|
| 11 |
+
User = get_user_model()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AuthenticationTests(APITestCase):
|
| 15 |
+
"""Tests pour l'authentification"""
|
| 16 |
+
|
| 17 |
+
def setUp(self):
|
| 18 |
+
self.client = APIClient()
|
| 19 |
+
self.register_url = reverse('register')
|
| 20 |
+
self.login_url = reverse('login')
|
| 21 |
+
|
| 22 |
+
def test_register_personal_account(self):
|
| 23 |
+
"""Test inscription compte personnel"""
|
| 24 |
+
data = {
|
| 25 |
+
'email': 'test@example.com',
|
| 26 |
+
'password': 'TestPass123!',
|
| 27 |
+
'password2': 'TestPass123!',
|
| 28 |
+
'first_name': 'John',
|
| 29 |
+
'last_name': 'Doe',
|
| 30 |
+
'account_type': 'personal',
|
| 31 |
+
'agreed': True
|
| 32 |
+
}
|
| 33 |
+
response = self.client.post(self.register_url, data)
|
| 34 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 35 |
+
self.assertIn('tokens', response.data)
|
| 36 |
+
self.assertIn('user', response.data)
|
| 37 |
+
|
| 38 |
+
def test_register_business_without_ifu_fails(self):
|
| 39 |
+
"""Test que l'inscription business sans IFU échoue"""
|
| 40 |
+
data = {
|
| 41 |
+
'email': 'business@example.com',
|
| 42 |
+
'password': 'TestPass123!',
|
| 43 |
+
'password2': 'TestPass123!',
|
| 44 |
+
'first_name': 'Jane',
|
| 45 |
+
'last_name': 'Smith',
|
| 46 |
+
'account_type': 'business',
|
| 47 |
+
'business_name': 'Test Corp',
|
| 48 |
+
'agreed': True,
|
| 49 |
+
'businessAgreed': True
|
| 50 |
+
}
|
| 51 |
+
response = self.client.post(self.register_url, data)
|
| 52 |
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
| 53 |
+
self.assertIn('ifu', response.data['errors'])
|
| 54 |
+
|
| 55 |
+
def test_register_business_with_ifu_succeeds(self):
|
| 56 |
+
"""Test inscription business avec IFU réussit"""
|
| 57 |
+
data = {
|
| 58 |
+
'email': 'business@example.com',
|
| 59 |
+
'password': 'TestPass123!',
|
| 60 |
+
'password2': 'TestPass123!',
|
| 61 |
+
'first_name': 'Jane',
|
| 62 |
+
'last_name': 'Smith',
|
| 63 |
+
'account_type': 'business',
|
| 64 |
+
'business_name': 'Test Corp',
|
| 65 |
+
'ifu': '123456789',
|
| 66 |
+
'agreed': True,
|
| 67 |
+
'businessAgreed': True
|
| 68 |
+
}
|
| 69 |
+
response = self.client.post(self.register_url, data)
|
| 70 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 71 |
+
|
| 72 |
+
def test_login_with_email(self):
|
| 73 |
+
"""Test connexion avec email"""
|
| 74 |
+
# Créer un utilisateur
|
| 75 |
+
user = User.objects.create_user(
|
| 76 |
+
email='test@example.com',
|
| 77 |
+
password='TestPass123!',
|
| 78 |
+
first_name='John',
|
| 79 |
+
last_name='Doe'
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# Tenter la connexion
|
| 83 |
+
data = {
|
| 84 |
+
'email': 'test@example.com',
|
| 85 |
+
'password': 'TestPass123!'
|
| 86 |
+
}
|
| 87 |
+
response = self.client.post(self.login_url, data)
|
| 88 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 89 |
+
self.assertIn('tokens', response.data)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ProductTests(APITestCase):
|
| 93 |
+
"""Tests pour les produits"""
|
| 94 |
+
|
| 95 |
+
def setUp(self):
|
| 96 |
+
self.user = User.objects.create_user(
|
| 97 |
+
email='test@example.com',
|
| 98 |
+
password='TestPass123!',
|
| 99 |
+
first_name='John',
|
| 100 |
+
last_name='Doe'
|
| 101 |
+
)
|
| 102 |
+
self.client = APIClient()
|
| 103 |
+
self.client.force_authenticate(user=self.user)
|
| 104 |
+
self.products_url = reverse('product-list')
|
| 105 |
+
|
| 106 |
+
def test_create_product(self):
|
| 107 |
+
"""Test création de produit"""
|
| 108 |
+
data = {
|
| 109 |
+
'name': 'Tomates',
|
| 110 |
+
'description': 'Tomates fraîches',
|
| 111 |
+
'price': '500.00',
|
| 112 |
+
'unit': 'Kg',
|
| 113 |
+
'category': 'vente',
|
| 114 |
+
'stock_status': 'ok'
|
| 115 |
+
}
|
| 116 |
+
response = self.client.post(self.products_url, data)
|
| 117 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 118 |
+
self.assertEqual(Product.objects.count(), 1)
|
| 119 |
+
self.assertEqual(Product.objects.first().user, self.user)
|
| 120 |
+
|
| 121 |
+
def test_list_products(self):
|
| 122 |
+
"""Test récupération de la liste des produits"""
|
| 123 |
+
Product.objects.create(
|
| 124 |
+
user=self.user,
|
| 125 |
+
name='Tomates',
|
| 126 |
+
price=Decimal('500.00'),
|
| 127 |
+
category='vente'
|
| 128 |
+
)
|
| 129 |
+
response = self.client.get(self.products_url)
|
| 130 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 131 |
+
self.assertEqual(len(response.data['results']), 1)
|
| 132 |
+
|
| 133 |
+
def test_filter_products_by_category(self):
|
| 134 |
+
"""Test filtrage des produits par catégorie"""
|
| 135 |
+
Product.objects.create(
|
| 136 |
+
user=self.user,
|
| 137 |
+
name='Tomates',
|
| 138 |
+
price=Decimal('500.00'),
|
| 139 |
+
category='vente'
|
| 140 |
+
)
|
| 141 |
+
Product.objects.create(
|
| 142 |
+
user=self.user,
|
| 143 |
+
name='Essence',
|
| 144 |
+
price=Decimal('1000.00'),
|
| 145 |
+
category='depense'
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
response = self.client.get(self.products_url + '?category=vente')
|
| 149 |
+
self.assertEqual(len(response.data['results']), 1)
|
| 150 |
+
self.assertEqual(response.data['results'][0]['name'], 'Tomates')
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class TransactionTests(APITestCase):
|
| 154 |
+
"""Tests pour les transactions"""
|
| 155 |
+
|
| 156 |
+
def setUp(self):
|
| 157 |
+
self.user = User.objects.create_user(
|
| 158 |
+
email='test@example.com',
|
| 159 |
+
password='TestPass123!',
|
| 160 |
+
first_name='John',
|
| 161 |
+
last_name='Doe'
|
| 162 |
+
)
|
| 163 |
+
self.client = APIClient()
|
| 164 |
+
self.client.force_authenticate(user=self.user)
|
| 165 |
+
self.transactions_url = reverse('transaction-list')
|
| 166 |
+
|
| 167 |
+
def test_create_transaction(self):
|
| 168 |
+
"""Test création de transaction"""
|
| 169 |
+
data = {
|
| 170 |
+
'name': 'Vente tomates',
|
| 171 |
+
'amount': '5000.00',
|
| 172 |
+
'type': 'income',
|
| 173 |
+
'category': 'Ventes',
|
| 174 |
+
'date': timezone.now().isoformat()
|
| 175 |
+
}
|
| 176 |
+
response = self.client.post(self.transactions_url, data)
|
| 177 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 178 |
+
self.assertEqual(Transaction.objects.count(), 1)
|
| 179 |
+
|
| 180 |
+
def test_transaction_summary(self):
|
| 181 |
+
"""Test résumé des transactions"""
|
| 182 |
+
now = timezone.now()
|
| 183 |
+
|
| 184 |
+
# Créer des transactions
|
| 185 |
+
Transaction.objects.create(
|
| 186 |
+
user=self.user,
|
| 187 |
+
name='Vente',
|
| 188 |
+
amount=Decimal('10000.00'),
|
| 189 |
+
type='income',
|
| 190 |
+
category='Ventes',
|
| 191 |
+
date=now
|
| 192 |
+
)
|
| 193 |
+
Transaction.objects.create(
|
| 194 |
+
user=self.user,
|
| 195 |
+
name='Achat',
|
| 196 |
+
amount=Decimal('3000.00'),
|
| 197 |
+
type='expense',
|
| 198 |
+
category='Achats',
|
| 199 |
+
date=now
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
summary_url = reverse('transaction-summary')
|
| 203 |
+
response = self.client.get(summary_url)
|
| 204 |
+
|
| 205 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 206 |
+
self.assertEqual(Decimal(response.data['balance']), Decimal('7000.00'))
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class BudgetTests(APITestCase):
|
| 210 |
+
"""Tests pour les budgets"""
|
| 211 |
+
|
| 212 |
+
def setUp(self):
|
| 213 |
+
self.user = User.objects.create_user(
|
| 214 |
+
email='test@example.com',
|
| 215 |
+
password='TestPass123!',
|
| 216 |
+
first_name='John',
|
| 217 |
+
last_name='Doe'
|
| 218 |
+
)
|
| 219 |
+
self.client = APIClient()
|
| 220 |
+
self.client.force_authenticate(user=self.user)
|
| 221 |
+
self.budgets_url = reverse('budget-list')
|
| 222 |
+
|
| 223 |
+
def test_create_budget(self):
|
| 224 |
+
"""Test création de budget"""
|
| 225 |
+
data = {
|
| 226 |
+
'category': 'Transport',
|
| 227 |
+
'limit': '50000.00',
|
| 228 |
+
'color': '#FF5733'
|
| 229 |
+
}
|
| 230 |
+
response = self.client.post(self.budgets_url, data)
|
| 231 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 232 |
+
self.assertEqual(Budget.objects.count(), 1)
|
| 233 |
+
|
| 234 |
+
def test_budget_spent_amount_calculation(self):
|
| 235 |
+
"""Test calcul du montant dépensé"""
|
| 236 |
+
# Créer un budget
|
| 237 |
+
budget = Budget.objects.create(
|
| 238 |
+
user=self.user,
|
| 239 |
+
category='Transport',
|
| 240 |
+
limit=Decimal('50000.00')
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Créer des dépenses dans cette catégorie
|
| 244 |
+
Transaction.objects.create(
|
| 245 |
+
user=self.user,
|
| 246 |
+
name='Taxi',
|
| 247 |
+
amount=Decimal('5000.00'),
|
| 248 |
+
type='expense',
|
| 249 |
+
category='Transport',
|
| 250 |
+
date=timezone.now()
|
| 251 |
+
)
|
| 252 |
+
Transaction.objects.create(
|
| 253 |
+
user=self.user,
|
| 254 |
+
name='Essence',
|
| 255 |
+
amount=Decimal('10000.00'),
|
| 256 |
+
type='expense',
|
| 257 |
+
category='Transport',
|
| 258 |
+
date=timezone.now()
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Vérifier le calcul
|
| 262 |
+
response = self.client.get(self.budgets_url)
|
| 263 |
+
budget_data = response.data['results'][0]
|
| 264 |
+
|
| 265 |
+
self.assertEqual(Decimal(budget_data['spent_amount']), Decimal('15000.00'))
|
| 266 |
+
self.assertEqual(budget_data['percentage'], 30.0)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class AdTests(APITestCase):
|
| 270 |
+
"""Tests pour les annonces"""
|
| 271 |
+
|
| 272 |
+
def setUp(self):
|
| 273 |
+
self.user = User.objects.create_user(
|
| 274 |
+
email='test@example.com',
|
| 275 |
+
password='TestPass123!',
|
| 276 |
+
first_name='John',
|
| 277 |
+
last_name='Doe'
|
| 278 |
+
)
|
| 279 |
+
self.client = APIClient()
|
| 280 |
+
self.ads_url = reverse('ad-list')
|
| 281 |
+
|
| 282 |
+
def test_list_ads_without_auth(self):
|
| 283 |
+
"""Test que les annonces sont accessibles sans authentification"""
|
| 284 |
+
Ad.objects.create(
|
| 285 |
+
user=self.user,
|
| 286 |
+
product_name='Engrais bio',
|
| 287 |
+
owner_name='AgriCorp',
|
| 288 |
+
description='Engrais de qualité',
|
| 289 |
+
whatsapp='+22890123456',
|
| 290 |
+
location='Lomé',
|
| 291 |
+
is_verified=True
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
response = self.client.get(self.ads_url)
|
| 295 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 296 |
+
self.assertEqual(len(response.data['results']), 1)
|
| 297 |
+
|
| 298 |
+
def test_create_ad_requires_auth(self):
|
| 299 |
+
"""Test que la création d'annonce requiert l'authentification"""
|
| 300 |
+
data = {
|
| 301 |
+
'product_name': 'Test Product',
|
| 302 |
+
'owner_name': 'Test Owner',
|
| 303 |
+
'description': 'Test description',
|
| 304 |
+
'whatsapp': '+22890123456',
|
| 305 |
+
'location': 'Lomé'
|
| 306 |
+
}
|
| 307 |
+
response = self.client.post(self.ads_url, data)
|
| 308 |
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
class ModelTests(TestCase):
|
| 312 |
+
"""Tests pour les modèles"""
|
| 313 |
+
|
| 314 |
+
def test_user_creation_personal(self):
|
| 315 |
+
"""Test création utilisateur personnel"""
|
| 316 |
+
user = User.objects.create_user(
|
| 317 |
+
email='test@example.com',
|
| 318 |
+
password='TestPass123!',
|
| 319 |
+
first_name='John',
|
| 320 |
+
last_name='Doe',
|
| 321 |
+
account_type='personal'
|
| 322 |
+
)
|
| 323 |
+
self.assertEqual(user.email, 'test@example.com')
|
| 324 |
+
self.assertEqual(user.account_type, 'personal')
|
| 325 |
+
self.assertFalse(user.is_premium)
|
| 326 |
+
|
| 327 |
+
def test_user_business_requires_ifu(self):
|
| 328 |
+
"""Test que les comptes business nécessitent un IFU"""
|
| 329 |
+
from django.core.exceptions import ValidationError
|
| 330 |
+
|
| 331 |
+
user = User(
|
| 332 |
+
email='business@example.com',
|
| 333 |
+
account_type='business',
|
| 334 |
+
business_name='Test Corp',
|
| 335 |
+
first_name='Jane',
|
| 336 |
+
last_name='Smith'
|
| 337 |
+
)
|
| 338 |
+
user.set_password('TestPass123!')
|
| 339 |
+
|
| 340 |
+
with self.assertRaises(ValidationError):
|
| 341 |
+
user.save()
|
| 342 |
+
|
| 343 |
+
def test_budget_spent_amount_method(self):
|
| 344 |
+
"""Test méthode get_spent_amount du Budget"""
|
| 345 |
+
user = User.objects.create_user(
|
| 346 |
+
email='test@example.com',
|
| 347 |
+
password='TestPass123!',
|
| 348 |
+
first_name='John',
|
| 349 |
+
last_name='Doe'
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
budget = Budget.objects.create(
|
| 353 |
+
user=user,
|
| 354 |
+
category='Transport',
|
| 355 |
+
limit=Decimal('50000.00')
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
Transaction.objects.create(
|
| 359 |
+
user=user,
|
| 360 |
+
name='Taxi',
|
| 361 |
+
amount=Decimal('5000.00'),
|
| 362 |
+
type='expense',
|
| 363 |
+
category='Transport',
|
| 364 |
+
date=timezone.now()
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
self.assertEqual(budget.get_spent_amount(), Decimal('5000.00'))
|
api/tests_new.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import reverse
|
| 2 |
+
from rest_framework.test import APITestCase, APIClient
|
| 3 |
+
from rest_framework import status
|
| 4 |
+
from django.contrib.auth import get_user_model
|
| 5 |
+
from .models import Notification, SupportTicket
|
| 6 |
+
|
| 7 |
+
User = get_user_model()
|
| 8 |
+
|
| 9 |
+
class NotificationTests(APITestCase):
|
| 10 |
+
"""Tests pour les notifications"""
|
| 11 |
+
|
| 12 |
+
def setUp(self):
|
| 13 |
+
self.user = User.objects.create_user(
|
| 14 |
+
email='test@example.com',
|
| 15 |
+
password='TestPass123!',
|
| 16 |
+
first_name='John',
|
| 17 |
+
last_name='Doe'
|
| 18 |
+
)
|
| 19 |
+
self.client = APIClient()
|
| 20 |
+
self.client.force_authenticate(user=self.user)
|
| 21 |
+
self.notifications_url = reverse('notification-list')
|
| 22 |
+
|
| 23 |
+
def test_create_notification(self):
|
| 24 |
+
"""Test création de notification"""
|
| 25 |
+
data = {
|
| 26 |
+
'title': 'Test Notification',
|
| 27 |
+
'message': 'This is a test message',
|
| 28 |
+
'type': 'system'
|
| 29 |
+
}
|
| 30 |
+
response = self.client.post(self.notifications_url, data)
|
| 31 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 32 |
+
self.assertEqual(Notification.objects.count(), 1)
|
| 33 |
+
self.assertEqual(Notification.objects.first().user, self.user)
|
| 34 |
+
|
| 35 |
+
def test_list_notifications(self):
|
| 36 |
+
"""Test récupération de la liste des notifications"""
|
| 37 |
+
Notification.objects.create(
|
| 38 |
+
user=self.user,
|
| 39 |
+
title='Test Notification',
|
| 40 |
+
message='This is a test message',
|
| 41 |
+
type='system'
|
| 42 |
+
)
|
| 43 |
+
response = self.client.get(self.notifications_url)
|
| 44 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 45 |
+
self.assertEqual(len(response.data['results']), 1)
|
| 46 |
+
|
| 47 |
+
def test_mark_read(self):
|
| 48 |
+
"""Test marquer une notification comme lue"""
|
| 49 |
+
notification = Notification.objects.create(
|
| 50 |
+
user=self.user,
|
| 51 |
+
title='Test Notification',
|
| 52 |
+
message='This is a test message',
|
| 53 |
+
type='system'
|
| 54 |
+
)
|
| 55 |
+
url = reverse('notification-mark-read', args=[notification.id])
|
| 56 |
+
response = self.client.patch(url)
|
| 57 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 58 |
+
notification.refresh_from_db()
|
| 59 |
+
self.assertTrue(notification.is_read)
|
| 60 |
+
|
| 61 |
+
def test_mark_all_read(self):
|
| 62 |
+
"""Test marquer toutes les notifications comme lues"""
|
| 63 |
+
Notification.objects.create(
|
| 64 |
+
user=self.user,
|
| 65 |
+
title='Test Notification 1',
|
| 66 |
+
message='Message 1',
|
| 67 |
+
type='system'
|
| 68 |
+
)
|
| 69 |
+
Notification.objects.create(
|
| 70 |
+
user=self.user,
|
| 71 |
+
title='Test Notification 2',
|
| 72 |
+
message='Message 2',
|
| 73 |
+
type='system'
|
| 74 |
+
)
|
| 75 |
+
url = reverse('notification-mark-all-read')
|
| 76 |
+
response = self.client.patch(url)
|
| 77 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 78 |
+
self.assertEqual(Notification.objects.filter(is_read=True).count(), 2)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class SupportTicketTests(APITestCase):
|
| 82 |
+
"""Tests pour les tickets support"""
|
| 83 |
+
|
| 84 |
+
def setUp(self):
|
| 85 |
+
self.user = User.objects.create_user(
|
| 86 |
+
email='test@example.com',
|
| 87 |
+
password='TestPass123!',
|
| 88 |
+
first_name='John',
|
| 89 |
+
last_name='Doe'
|
| 90 |
+
)
|
| 91 |
+
self.client = APIClient()
|
| 92 |
+
self.client.force_authenticate(user=self.user)
|
| 93 |
+
self.support_url = reverse('support-list')
|
| 94 |
+
|
| 95 |
+
def test_create_ticket(self):
|
| 96 |
+
"""Test création de ticket"""
|
| 97 |
+
data = {
|
| 98 |
+
'subject': 'Help me',
|
| 99 |
+
'message': 'I need help'
|
| 100 |
+
}
|
| 101 |
+
response = self.client.post(self.support_url, data)
|
| 102 |
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
| 103 |
+
self.assertEqual(SupportTicket.objects.count(), 1)
|
| 104 |
+
self.assertEqual(SupportTicket.objects.first().user, self.user)
|
| 105 |
+
|
| 106 |
+
def test_list_tickets(self):
|
| 107 |
+
"""Test récupération de la liste des tickets"""
|
| 108 |
+
SupportTicket.objects.create(
|
| 109 |
+
user=self.user,
|
| 110 |
+
subject='Help me',
|
| 111 |
+
message='I need help'
|
| 112 |
+
)
|
| 113 |
+
response = self.client.get(self.support_url)
|
| 114 |
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
| 115 |
+
self.assertEqual(len(response.data['results']), 1)
|
api/urls.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path, include
|
| 2 |
+
from rest_framework.routers import DefaultRouter
|
| 3 |
+
from rest_framework_simplejwt.views import TokenRefreshView
|
| 4 |
+
|
| 5 |
+
from .views import (
|
| 6 |
+
RegisterView, LoginView, ProfileView, ChangePasswordView,
|
| 7 |
+
ProductViewSet, TransactionViewSet, BudgetViewSet, AdViewSet,
|
| 8 |
+
NotificationViewSet, SupportTicketViewSet, VoiceCommandView,
|
| 9 |
+
analytics_overview, analytics_breakdown, analytics_kpi
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
# Router pour les ViewSets
|
| 13 |
+
router = DefaultRouter()
|
| 14 |
+
router.register(r'products', ProductViewSet, basename='product')
|
| 15 |
+
router.register(r'transactions', TransactionViewSet, basename='transaction')
|
| 16 |
+
router.register(r'budgets', BudgetViewSet, basename='budget')
|
| 17 |
+
router.register(r'ads', AdViewSet, basename='ad')
|
| 18 |
+
router.register(r'notifications', NotificationViewSet, basename='notification')
|
| 19 |
+
router.register(r'support', SupportTicketViewSet, basename='support')
|
| 20 |
+
|
| 21 |
+
urlpatterns = [
|
| 22 |
+
# ===== AUTH =====
|
| 23 |
+
path('auth/register/', RegisterView.as_view(), name='register'),
|
| 24 |
+
path('auth/login/', LoginView.as_view(), name='login'),
|
| 25 |
+
path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
| 26 |
+
path('auth/me/', ProfileView.as_view(), name='profile'),
|
| 27 |
+
path('auth/change-password/', ChangePasswordView.as_view(), name='change-password'),
|
| 28 |
+
|
| 29 |
+
# ===== ANALYTICS =====
|
| 30 |
+
path('analytics/overview/', analytics_overview, name='analytics-overview'),
|
| 31 |
+
path('analytics/breakdown/', analytics_breakdown, name='analytics-breakdown'),
|
| 32 |
+
path('analytics/kpi/', analytics_kpi, name='analytics-kpi'),
|
| 33 |
+
|
| 34 |
+
# ===== ROUTER (Products, Transactions, Budgets, Ads) =====
|
| 35 |
+
path('', include(router.urls)),
|
| 36 |
+
|
| 37 |
+
# ===== VOICE AI =====
|
| 38 |
+
path('voice-command/', VoiceCommandView.as_view(), name='voice-command'),
|
| 39 |
+
]
|
api/views.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import viewsets, status, filters
|
| 2 |
+
from rest_framework.decorators import action, api_view, permission_classes
|
| 3 |
+
from rest_framework.response import Response
|
| 4 |
+
from rest_framework.permissions import IsAuthenticated, AllowAny
|
| 5 |
+
from rest_framework.views import APIView
|
| 6 |
+
from rest_framework_simplejwt.tokens import RefreshToken
|
| 7 |
+
from django.contrib.auth import get_user_model, authenticate
|
| 8 |
+
from django.db.models import Sum, Q, Count
|
| 9 |
+
from django.utils import timezone
|
| 10 |
+
from datetime import timedelta, datetime
|
| 11 |
+
from decimal import Decimal
|
| 12 |
+
from django_filters.rest_framework import DjangoFilterBackend
|
| 13 |
+
import csv
|
| 14 |
+
from django.http import HttpResponse
|
| 15 |
+
|
| 16 |
+
from .models import Product, Transaction, Budget, Ad, Notification, SupportTicket
|
| 17 |
+
from .serializers import (
|
| 18 |
+
UserSerializer, RegisterSerializer, ChangePasswordSerializer,
|
| 19 |
+
ProductSerializer, TransactionSerializer, TransactionSummarySerializer,
|
| 20 |
+
BudgetSerializer, AdSerializer, OverviewAnalyticsSerializer,
|
| 21 |
+
BreakdownAnalyticsSerializer, KPISerializer, NotificationSerializer,
|
| 22 |
+
SupportTicketSerializer
|
| 23 |
+
)
|
| 24 |
+
from .gemini_service import GeminiService
|
| 25 |
+
import tempfile
|
| 26 |
+
import os
|
| 27 |
+
|
| 28 |
+
User = get_user_model()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ========== AUTHENTIFICATION ==========
|
| 32 |
+
|
| 33 |
+
class RegisterView(APIView):
|
| 34 |
+
"""Inscription d'un nouvel utilisateur"""
|
| 35 |
+
permission_classes = [AllowAny]
|
| 36 |
+
|
| 37 |
+
def post(self, request):
|
| 38 |
+
serializer = RegisterSerializer(data=request.data)
|
| 39 |
+
if serializer.is_valid():
|
| 40 |
+
user = serializer.save()
|
| 41 |
+
refresh = RefreshToken.for_user(user)
|
| 42 |
+
|
| 43 |
+
return Response({
|
| 44 |
+
'user': UserSerializer(user).data,
|
| 45 |
+
'tokens': {
|
| 46 |
+
'refresh': str(refresh),
|
| 47 |
+
'access': str(refresh.access_token),
|
| 48 |
+
}
|
| 49 |
+
}, status=status.HTTP_201_CREATED)
|
| 50 |
+
|
| 51 |
+
return Response({
|
| 52 |
+
'type': 'validation_error',
|
| 53 |
+
'errors': serializer.errors
|
| 54 |
+
}, status=status.HTTP_400_BAD_REQUEST)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class LoginView(APIView):
|
| 58 |
+
"""Connexion via email et mot de passe"""
|
| 59 |
+
permission_classes = [AllowAny]
|
| 60 |
+
|
| 61 |
+
def post(self, request):
|
| 62 |
+
email = request.data.get('email')
|
| 63 |
+
password = request.data.get('password')
|
| 64 |
+
|
| 65 |
+
if not email or not password:
|
| 66 |
+
return Response({
|
| 67 |
+
'type': 'validation_error',
|
| 68 |
+
'errors': {
|
| 69 |
+
'email': ['Email et mot de passe requis.']
|
| 70 |
+
}
|
| 71 |
+
}, status=status.HTTP_400_BAD_REQUEST)
|
| 72 |
+
|
| 73 |
+
# Authenticate avec email
|
| 74 |
+
try:
|
| 75 |
+
user = User.objects.get(email=email)
|
| 76 |
+
except User.DoesNotExist:
|
| 77 |
+
return Response({
|
| 78 |
+
'type': 'validation_error',
|
| 79 |
+
'errors': {
|
| 80 |
+
'email': ['Email ou mot de passe incorrect.']
|
| 81 |
+
}
|
| 82 |
+
}, status=status.HTTP_401_UNAUTHORIZED)
|
| 83 |
+
|
| 84 |
+
if not user.check_password(password):
|
| 85 |
+
return Response({
|
| 86 |
+
'type': 'validation_error',
|
| 87 |
+
'errors': {
|
| 88 |
+
'password': ['Email ou mot de passe incorrect.']
|
| 89 |
+
}
|
| 90 |
+
}, status=status.HTTP_401_UNAUTHORIZED)
|
| 91 |
+
|
| 92 |
+
if not user.is_active:
|
| 93 |
+
return Response({
|
| 94 |
+
'type': 'validation_error',
|
| 95 |
+
'errors': {
|
| 96 |
+
'email': ['Ce compte est désactivé.']
|
| 97 |
+
}
|
| 98 |
+
}, status=status.HTTP_403_FORBIDDEN)
|
| 99 |
+
|
| 100 |
+
refresh = RefreshToken.for_user(user)
|
| 101 |
+
|
| 102 |
+
return Response({
|
| 103 |
+
'user': UserSerializer(user).data,
|
| 104 |
+
'tokens': {
|
| 105 |
+
'refresh': str(refresh),
|
| 106 |
+
'access': str(refresh.access_token),
|
| 107 |
+
}
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class ProfileView(APIView):
|
| 112 |
+
"""Récupération et mise à jour du profil"""
|
| 113 |
+
permission_classes = [IsAuthenticated]
|
| 114 |
+
|
| 115 |
+
def get(self, request):
|
| 116 |
+
serializer = UserSerializer(request.user)
|
| 117 |
+
return Response(serializer.data)
|
| 118 |
+
|
| 119 |
+
def patch(self, request):
|
| 120 |
+
serializer = UserSerializer(
|
| 121 |
+
request.user,
|
| 122 |
+
data=request.data,
|
| 123 |
+
partial=True
|
| 124 |
+
)
|
| 125 |
+
if serializer.is_valid():
|
| 126 |
+
serializer.save()
|
| 127 |
+
return Response(serializer.data)
|
| 128 |
+
|
| 129 |
+
return Response({
|
| 130 |
+
'type': 'validation_error',
|
| 131 |
+
'errors': serializer.errors
|
| 132 |
+
}, status=status.HTTP_400_BAD_REQUEST)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class ChangePasswordView(APIView):
|
| 136 |
+
"""Changement de mot de passe"""
|
| 137 |
+
permission_classes = [IsAuthenticated]
|
| 138 |
+
|
| 139 |
+
def post(self, request):
|
| 140 |
+
serializer = ChangePasswordSerializer(data=request.data)
|
| 141 |
+
|
| 142 |
+
if serializer.is_valid():
|
| 143 |
+
user = request.user
|
| 144 |
+
|
| 145 |
+
if not user.check_password(serializer.validated_data['old_password']):
|
| 146 |
+
return Response({
|
| 147 |
+
'type': 'validation_error',
|
| 148 |
+
'errors': {
|
| 149 |
+
'old_password': ['Mot de passe actuel incorrect.']
|
| 150 |
+
}
|
| 151 |
+
}, status=status.HTTP_400_BAD_REQUEST)
|
| 152 |
+
|
| 153 |
+
user.set_password(serializer.validated_data['new_password'])
|
| 154 |
+
user.save()
|
| 155 |
+
|
| 156 |
+
return Response({
|
| 157 |
+
'message': 'Mot de passe modifié avec succès.'
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
return Response({
|
| 161 |
+
'type': 'validation_error',
|
| 162 |
+
'errors': serializer.errors
|
| 163 |
+
}, status=status.HTTP_400_BAD_REQUEST)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ========== PRODUITS ==========
|
| 167 |
+
|
| 168 |
+
class ProductViewSet(viewsets.ModelViewSet):
|
| 169 |
+
"""CRUD pour les produits"""
|
| 170 |
+
serializer_class = ProductSerializer
|
| 171 |
+
permission_classes = [IsAuthenticated]
|
| 172 |
+
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
| 173 |
+
filterset_fields = ['category', 'stock_status']
|
| 174 |
+
search_fields = ['name', 'description']
|
| 175 |
+
|
| 176 |
+
def get_queryset(self):
|
| 177 |
+
return Product.objects.filter(user=self.request.user)
|
| 178 |
+
|
| 179 |
+
@action(detail=False, methods=['get'])
|
| 180 |
+
def export(self, request):
|
| 181 |
+
"""Export CSV des produits"""
|
| 182 |
+
products = self.get_queryset()
|
| 183 |
+
|
| 184 |
+
response = HttpResponse(content_type='text/csv')
|
| 185 |
+
response['Content-Disposition'] = 'attachment; filename="products.csv"'
|
| 186 |
+
|
| 187 |
+
writer = csv.writer(response)
|
| 188 |
+
writer.writerow(['Nom', 'Description', 'Prix', 'Unité', 'Catégorie', 'Stock'])
|
| 189 |
+
|
| 190 |
+
for product in products:
|
| 191 |
+
writer.writerow([
|
| 192 |
+
product.name,
|
| 193 |
+
product.description,
|
| 194 |
+
product.price,
|
| 195 |
+
product.unit,
|
| 196 |
+
product.get_category_display(),
|
| 197 |
+
product.get_stock_status_display()
|
| 198 |
+
])
|
| 199 |
+
|
| 200 |
+
return response
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
# ========== TRANSACTIONS ==========
|
| 204 |
+
|
| 205 |
+
class TransactionViewSet(viewsets.ModelViewSet):
|
| 206 |
+
"""CRUD pour les transactions"""
|
| 207 |
+
serializer_class = TransactionSerializer
|
| 208 |
+
permission_classes = [IsAuthenticated]
|
| 209 |
+
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
| 210 |
+
filterset_fields = ['type', 'category']
|
| 211 |
+
search_fields = ['name', 'category']
|
| 212 |
+
ordering_fields = ['date', 'amount']
|
| 213 |
+
ordering = ['-date']
|
| 214 |
+
|
| 215 |
+
def get_queryset(self):
|
| 216 |
+
queryset = Transaction.objects.filter(user=self.request.user)
|
| 217 |
+
|
| 218 |
+
# Filtre par date range
|
| 219 |
+
date_range = self.request.query_params.get('date_range')
|
| 220 |
+
if date_range:
|
| 221 |
+
now = timezone.now()
|
| 222 |
+
if date_range == 'today':
|
| 223 |
+
start_date = now.replace(hour=0, minute=0, second=0)
|
| 224 |
+
elif date_range == 'week':
|
| 225 |
+
start_date = now - timedelta(days=7)
|
| 226 |
+
elif date_range == 'month':
|
| 227 |
+
start_date = now - timedelta(days=30)
|
| 228 |
+
elif date_range == 'year':
|
| 229 |
+
start_date = now - timedelta(days=365)
|
| 230 |
+
else:
|
| 231 |
+
start_date = None
|
| 232 |
+
|
| 233 |
+
if start_date:
|
| 234 |
+
queryset = queryset.filter(date__gte=start_date)
|
| 235 |
+
|
| 236 |
+
return queryset
|
| 237 |
+
|
| 238 |
+
@action(detail=False, methods=['get'])
|
| 239 |
+
def summary(self, request):
|
| 240 |
+
"""Résumé pour le dashboard"""
|
| 241 |
+
user = request.user
|
| 242 |
+
now = timezone.now()
|
| 243 |
+
yesterday = now - timedelta(days=1)
|
| 244 |
+
day_before = now - timedelta(days=2)
|
| 245 |
+
|
| 246 |
+
# Transactions des dernières 24h
|
| 247 |
+
recent = Transaction.objects.filter(
|
| 248 |
+
user=user,
|
| 249 |
+
date__gte=yesterday
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Transactions des 24h précédentes
|
| 253 |
+
previous = Transaction.objects.filter(
|
| 254 |
+
user=user,
|
| 255 |
+
date__gte=day_before,
|
| 256 |
+
date__lt=yesterday
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
# Calculs
|
| 260 |
+
income_24h = recent.filter(type='income').aggregate(
|
| 261 |
+
total=Sum('amount')
|
| 262 |
+
)['total'] or Decimal('0.00')
|
| 263 |
+
|
| 264 |
+
expenses_24h = recent.filter(type='expense').aggregate(
|
| 265 |
+
total=Sum('amount')
|
| 266 |
+
)['total'] or Decimal('0.00')
|
| 267 |
+
|
| 268 |
+
prev_income = previous.filter(type='income').aggregate(
|
| 269 |
+
total=Sum('amount')
|
| 270 |
+
)['total'] or Decimal('0.00')
|
| 271 |
+
|
| 272 |
+
prev_expenses = previous.filter(type='expense').aggregate(
|
| 273 |
+
total=Sum('amount')
|
| 274 |
+
)['total'] or Decimal('0.00')
|
| 275 |
+
|
| 276 |
+
# Balance totale
|
| 277 |
+
total_income = Transaction.objects.filter(
|
| 278 |
+
user=user, type='income'
|
| 279 |
+
).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
|
| 280 |
+
|
| 281 |
+
total_expenses = Transaction.objects.filter(
|
| 282 |
+
user=user, type='expense'
|
| 283 |
+
).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
|
| 284 |
+
|
| 285 |
+
balance = total_income - total_expenses
|
| 286 |
+
|
| 287 |
+
# Variations en %
|
| 288 |
+
def calc_variation(current, previous):
|
| 289 |
+
if previous > 0:
|
| 290 |
+
return float(((current - previous) / previous) * 100)
|
| 291 |
+
return 0.0
|
| 292 |
+
|
| 293 |
+
data = {
|
| 294 |
+
'balance': balance,
|
| 295 |
+
'income_24h': income_24h,
|
| 296 |
+
'expenses_24h': expenses_24h,
|
| 297 |
+
'income_variation': calc_variation(income_24h, prev_income),
|
| 298 |
+
'expenses_variation': calc_variation(expenses_24h, prev_expenses)
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
serializer = TransactionSummarySerializer(data)
|
| 302 |
+
return Response(serializer.data)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# ========== ANALYTICS ==========
|
| 306 |
+
|
| 307 |
+
class AnalyticsView(APIView):
|
| 308 |
+
"""Analytics pour le dashboard"""
|
| 309 |
+
permission_classes = [IsAuthenticated]
|
| 310 |
+
|
| 311 |
+
def get_overview(self, request):
|
| 312 |
+
"""Graphique barres: Revenus vs Dépenses par mois"""
|
| 313 |
+
user = request.user
|
| 314 |
+
now = timezone.now()
|
| 315 |
+
six_months_ago = now - timedelta(days=180)
|
| 316 |
+
|
| 317 |
+
transactions = Transaction.objects.filter(
|
| 318 |
+
user=user,
|
| 319 |
+
date__gte=six_months_ago
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# Grouper par mois
|
| 323 |
+
monthly_data = {}
|
| 324 |
+
for t in transactions:
|
| 325 |
+
month_key = t.date.strftime('%Y-%m')
|
| 326 |
+
if month_key not in monthly_data:
|
| 327 |
+
monthly_data[month_key] = {'income': Decimal('0.00'), 'expenses': Decimal('0.00')}
|
| 328 |
+
|
| 329 |
+
if t.type == 'income':
|
| 330 |
+
monthly_data[month_key]['income'] += t.amount
|
| 331 |
+
else:
|
| 332 |
+
monthly_data[month_key]['expenses'] += t.amount
|
| 333 |
+
|
| 334 |
+
# Formater pour le serializer
|
| 335 |
+
result = []
|
| 336 |
+
for month, data in sorted(monthly_data.items()):
|
| 337 |
+
result.append({
|
| 338 |
+
'month': datetime.strptime(month, '%Y-%m').strftime('%b %Y'),
|
| 339 |
+
'income': data['income'],
|
| 340 |
+
'expenses': data['expenses']
|
| 341 |
+
})
|
| 342 |
+
|
| 343 |
+
serializer = OverviewAnalyticsSerializer(result, many=True)
|
| 344 |
+
return Response(serializer.data)
|
| 345 |
+
|
| 346 |
+
def get_breakdown(self, request):
|
| 347 |
+
"""Graphique camembert: Dépenses par catégorie"""
|
| 348 |
+
user = request.user
|
| 349 |
+
|
| 350 |
+
expenses = Transaction.objects.filter(
|
| 351 |
+
user=user,
|
| 352 |
+
type='expense'
|
| 353 |
+
).values('category').annotate(
|
| 354 |
+
total=Sum('amount')
|
| 355 |
+
).order_by('-total')
|
| 356 |
+
|
| 357 |
+
total_expenses = sum(item['total'] for item in expenses)
|
| 358 |
+
|
| 359 |
+
result = []
|
| 360 |
+
for item in expenses:
|
| 361 |
+
percentage = float((item['total'] / total_expenses) * 100) if total_expenses > 0 else 0
|
| 362 |
+
result.append({
|
| 363 |
+
'category': item['category'],
|
| 364 |
+
'amount': item['total'],
|
| 365 |
+
'percentage': percentage
|
| 366 |
+
})
|
| 367 |
+
|
| 368 |
+
serializer = BreakdownAnalyticsSerializer(result, many=True)
|
| 369 |
+
return Response(serializer.data)
|
| 370 |
+
|
| 371 |
+
def get_kpi(self, request):
|
| 372 |
+
"""KPIs clés"""
|
| 373 |
+
user = request.user
|
| 374 |
+
now = timezone.now()
|
| 375 |
+
month_ago = now - timedelta(days=30)
|
| 376 |
+
|
| 377 |
+
# Panier moyen (revenus / nombre de transactions de revenus)
|
| 378 |
+
income_transactions = Transaction.objects.filter(
|
| 379 |
+
user=user,
|
| 380 |
+
type='income',
|
| 381 |
+
date__gte=month_ago
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
total_income = income_transactions.aggregate(
|
| 385 |
+
total=Sum('amount')
|
| 386 |
+
)['total'] or Decimal('0.00')
|
| 387 |
+
|
| 388 |
+
count_income = income_transactions.count()
|
| 389 |
+
average_basket = total_income / count_income if count_income > 0 else Decimal('0.00')
|
| 390 |
+
|
| 391 |
+
# MRR estimé (revenus du dernier mois)
|
| 392 |
+
estimated_mrr = total_income
|
| 393 |
+
|
| 394 |
+
# CAC (estimation simplifiée: dépenses marketing / nouveaux clients)
|
| 395 |
+
# Pour simplifier, on utilise les dépenses de catégorie "Marketing"
|
| 396 |
+
marketing_expenses = Transaction.objects.filter(
|
| 397 |
+
user=user,
|
| 398 |
+
type='expense',
|
| 399 |
+
category__icontains='marketing',
|
| 400 |
+
date__gte=month_ago
|
| 401 |
+
).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
|
| 402 |
+
|
| 403 |
+
# Estimation simplifiée du CAC
|
| 404 |
+
cac = marketing_expenses
|
| 405 |
+
|
| 406 |
+
data = {
|
| 407 |
+
'average_basket': average_basket,
|
| 408 |
+
'estimated_mrr': estimated_mrr,
|
| 409 |
+
'cac': cac
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
serializer = KPISerializer(data)
|
| 413 |
+
return Response(serializer.data)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@api_view(['GET'])
|
| 417 |
+
@permission_classes([IsAuthenticated])
|
| 418 |
+
def analytics_overview(request):
|
| 419 |
+
view = AnalyticsView()
|
| 420 |
+
return view.get_overview(request)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
@api_view(['GET'])
|
| 424 |
+
@permission_classes([IsAuthenticated])
|
| 425 |
+
def analytics_breakdown(request):
|
| 426 |
+
view = AnalyticsView()
|
| 427 |
+
return view.get_breakdown(request)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
@api_view(['GET'])
|
| 431 |
+
@permission_classes([IsAuthenticated])
|
| 432 |
+
def analytics_kpi(request):
|
| 433 |
+
view = AnalyticsView()
|
| 434 |
+
return view.get_kpi(request)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
# ========== BUDGETS ==========
|
| 438 |
+
|
| 439 |
+
class BudgetViewSet(viewsets.ModelViewSet):
|
| 440 |
+
"""CRUD pour les budgets"""
|
| 441 |
+
serializer_class = BudgetSerializer
|
| 442 |
+
permission_classes = [IsAuthenticated]
|
| 443 |
+
|
| 444 |
+
def get_queryset(self):
|
| 445 |
+
return Budget.objects.filter(user=self.request.user)
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
# ========== ANNONCES ==========
|
| 449 |
+
|
| 450 |
+
class AdViewSet(viewsets.ModelViewSet):
|
| 451 |
+
"""CRUD pour les annonces"""
|
| 452 |
+
serializer_class = AdSerializer
|
| 453 |
+
permission_classes = [IsAuthenticated]
|
| 454 |
+
filter_backends = [filters.SearchFilter]
|
| 455 |
+
search_fields = ['product_name', 'owner_name', 'description', 'location']
|
| 456 |
+
|
| 457 |
+
def get_queryset(self):
|
| 458 |
+
# Les annonces sont publiques mais filtrées par vérification
|
| 459 |
+
return Ad.objects.filter(is_verified=True)
|
| 460 |
+
|
| 461 |
+
def get_permissions(self):
|
| 462 |
+
# Lecture publique, écriture authentifiée
|
| 463 |
+
if self.action in ['list', 'retrieve']:
|
| 464 |
+
return [AllowAny()]
|
| 465 |
+
return [IsAuthenticated()]
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
# ========== NOTIFICATIONS ==========
|
| 469 |
+
|
| 470 |
+
class NotificationViewSet(viewsets.ModelViewSet):
|
| 471 |
+
"""CRUD pour les notifications"""
|
| 472 |
+
serializer_class = NotificationSerializer
|
| 473 |
+
permission_classes = [IsAuthenticated]
|
| 474 |
+
|
| 475 |
+
def get_queryset(self):
|
| 476 |
+
return Notification.objects.filter(user=self.request.user)
|
| 477 |
+
|
| 478 |
+
@action(detail=True, methods=['patch'])
|
| 479 |
+
def mark_read(self, request, pk=None):
|
| 480 |
+
notification = self.get_object()
|
| 481 |
+
notification.is_read = True
|
| 482 |
+
notification.save()
|
| 483 |
+
return Response({'status': 'marked as read'})
|
| 484 |
+
|
| 485 |
+
@action(detail=False, methods=['patch'])
|
| 486 |
+
def mark_all_read(self, request):
|
| 487 |
+
self.get_queryset().update(is_read=True)
|
| 488 |
+
return Response({'status': 'all marked as read'})
|
| 489 |
+
|
| 490 |
+
def perform_create(self, serializer):
|
| 491 |
+
serializer.save(user=self.request.user)
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
# ========== SUPPORT ==========
|
| 495 |
+
|
| 496 |
+
class SupportTicketViewSet(viewsets.ModelViewSet):
|
| 497 |
+
"""CRUD pour les tickets support"""
|
| 498 |
+
serializer_class = SupportTicketSerializer
|
| 499 |
+
permission_classes = [IsAuthenticated]
|
| 500 |
+
|
| 501 |
+
def get_queryset(self):
|
| 502 |
+
return SupportTicket.objects.filter(user=self.request.user)
|
| 503 |
+
|
| 504 |
+
def perform_create(self, serializer):
|
| 505 |
+
serializer.save(user=self.request.user)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
# ========== VOICE AI ==========
|
| 509 |
+
|
| 510 |
+
class VoiceCommandView(APIView):
|
| 511 |
+
"""Traitement des commandes vocales via Gemini"""
|
| 512 |
+
permission_classes = [IsAuthenticated]
|
| 513 |
+
|
| 514 |
+
def post(self, request):
|
| 515 |
+
if 'audio' not in request.FILES:
|
| 516 |
+
return Response({'error': 'No audio file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
| 517 |
+
|
| 518 |
+
audio_file = request.FILES['audio']
|
| 519 |
+
|
| 520 |
+
try:
|
| 521 |
+
audio_bytes = audio_file.read()
|
| 522 |
+
mime_type = audio_file.content_type or 'audio/mp3'
|
| 523 |
+
|
| 524 |
+
service = GeminiService()
|
| 525 |
+
result = service.process_voice_command(audio_bytes, mime_type)
|
| 526 |
+
|
| 527 |
+
if result.get('intent') == 'create_transaction':
|
| 528 |
+
data = result.get('data', {})
|
| 529 |
+
|
| 530 |
+
# Prepare data for serializer
|
| 531 |
+
transaction_data = {
|
| 532 |
+
'name': data.get('name', 'Transaction Vocale'),
|
| 533 |
+
'amount': data.get('amount'),
|
| 534 |
+
'type': data.get('type'),
|
| 535 |
+
'category': data.get('category', 'Divers'),
|
| 536 |
+
'currency': data.get('currency', 'FCFA'),
|
| 537 |
+
'date': data.get('date') or timezone.now().date()
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
# Use serializer to validate and save
|
| 541 |
+
# We need to pass context={'request': request} so that create() method can access user
|
| 542 |
+
serializer = TransactionSerializer(data=transaction_data, context={'request': request})
|
| 543 |
+
|
| 544 |
+
if serializer.is_valid():
|
| 545 |
+
serializer.save()
|
| 546 |
+
return Response({
|
| 547 |
+
'status': 'success',
|
| 548 |
+
'transcription': result.get('transcription'),
|
| 549 |
+
'transaction': serializer.data
|
| 550 |
+
})
|
| 551 |
+
else:
|
| 552 |
+
return Response({
|
| 553 |
+
'status': 'error',
|
| 554 |
+
'transcription': result.get('transcription'),
|
| 555 |
+
'message': 'Validation failed',
|
| 556 |
+
'errors': serializer.errors
|
| 557 |
+
}, status=status.HTTP_400_BAD_REQUEST)
|
| 558 |
+
|
| 559 |
+
return Response({
|
| 560 |
+
'status': 'processed',
|
| 561 |
+
'transcription': result.get('transcription'),
|
| 562 |
+
'intent': result.get('intent'),
|
| 563 |
+
'data': result.get('data'),
|
| 564 |
+
'error': result.get('error')
|
| 565 |
+
})
|
| 566 |
+
|
| 567 |
+
except Exception as e:
|
| 568 |
+
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
manage.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Django's command-line utility for administrative tasks."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
"""Run administrative tasks."""
|
| 9 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Akompta.settings')
|
| 10 |
+
try:
|
| 11 |
+
from django.core.management import execute_from_command_line
|
| 12 |
+
except ImportError as exc:
|
| 13 |
+
raise ImportError(
|
| 14 |
+
"Couldn't import Django. Are you sure it's installed and "
|
| 15 |
+
"available on your PYTHONPATH environment variable? Did you "
|
| 16 |
+
"forget to activate a virtual environment?"
|
| 17 |
+
) from exc
|
| 18 |
+
execute_from_command_line(sys.argv)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == '__main__':
|
| 22 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Django & DRF
|
| 2 |
+
Django==5.0.1
|
| 3 |
+
djangorestframework==3.14.0
|
| 4 |
+
django-filter==23.5
|
| 5 |
+
|
| 6 |
+
# Authentication
|
| 7 |
+
djangorestframework-simplejwt==5.3.1
|
| 8 |
+
|
| 9 |
+
# CORS
|
| 10 |
+
django-cors-headers==4.3.1
|
| 11 |
+
|
| 12 |
+
# Database
|
| 13 |
+
psycopg2-binary==2.9.9
|
| 14 |
+
|
| 15 |
+
# Image handling
|
| 16 |
+
Pillow==10.2.0
|
| 17 |
+
|
| 18 |
+
# Environment variables
|
| 19 |
+
python-decouple==3.8
|
| 20 |
+
|
| 21 |
+
# Production server
|
| 22 |
+
gunicorn==21.2.0
|
| 23 |
+
|
| 24 |
+
# Gemini AI Integration
|
| 25 |
+
google-genai>=1.52.0
|
| 26 |
+
|
| 27 |
+
# Storage (optional - pour AWS S3 si besoin)
|
| 28 |
+
# django-storages==1.14.2
|
| 29 |
+
# boto3==1.34.22
|
static/admin/css/autocomplete.css
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
select.admin-autocomplete {
|
| 2 |
+
width: 20em;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
.select2-container--admin-autocomplete.select2-container {
|
| 6 |
+
min-height: 30px;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.select2-container--admin-autocomplete .select2-selection--single,
|
| 10 |
+
.select2-container--admin-autocomplete .select2-selection--multiple {
|
| 11 |
+
min-height: 30px;
|
| 12 |
+
padding: 0;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
| 16 |
+
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
| 17 |
+
border-color: var(--body-quiet-color);
|
| 18 |
+
min-height: 30px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
| 22 |
+
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
| 23 |
+
padding: 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
| 27 |
+
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
| 28 |
+
padding: 0;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.select2-container--admin-autocomplete .select2-selection--single {
|
| 32 |
+
background-color: var(--body-bg);
|
| 33 |
+
border: 1px solid var(--border-color);
|
| 34 |
+
border-radius: 4px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
| 38 |
+
color: var(--body-fg);
|
| 39 |
+
line-height: 30px;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
| 43 |
+
cursor: pointer;
|
| 44 |
+
float: right;
|
| 45 |
+
font-weight: bold;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
| 49 |
+
color: var(--body-quiet-color);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
| 53 |
+
height: 26px;
|
| 54 |
+
position: absolute;
|
| 55 |
+
top: 1px;
|
| 56 |
+
right: 1px;
|
| 57 |
+
width: 20px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
| 61 |
+
border-color: #888 transparent transparent transparent;
|
| 62 |
+
border-style: solid;
|
| 63 |
+
border-width: 5px 4px 0 4px;
|
| 64 |
+
height: 0;
|
| 65 |
+
left: 50%;
|
| 66 |
+
margin-left: -4px;
|
| 67 |
+
margin-top: -2px;
|
| 68 |
+
position: absolute;
|
| 69 |
+
top: 50%;
|
| 70 |
+
width: 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
| 74 |
+
float: left;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
| 78 |
+
left: 1px;
|
| 79 |
+
right: auto;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
| 83 |
+
background-color: var(--darkened-bg);
|
| 84 |
+
cursor: default;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
| 88 |
+
display: none;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
| 92 |
+
border-color: transparent transparent #888 transparent;
|
| 93 |
+
border-width: 0 4px 5px 4px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.select2-container--admin-autocomplete .select2-selection--multiple {
|
| 97 |
+
background-color: var(--body-bg);
|
| 98 |
+
border: 1px solid var(--border-color);
|
| 99 |
+
border-radius: 4px;
|
| 100 |
+
cursor: text;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
| 104 |
+
box-sizing: border-box;
|
| 105 |
+
list-style: none;
|
| 106 |
+
margin: 0;
|
| 107 |
+
padding: 0 10px 5px 5px;
|
| 108 |
+
width: 100%;
|
| 109 |
+
display: flex;
|
| 110 |
+
flex-wrap: wrap;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
| 114 |
+
list-style: none;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
| 118 |
+
color: var(--body-quiet-color);
|
| 119 |
+
margin-top: 5px;
|
| 120 |
+
float: left;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
| 124 |
+
cursor: pointer;
|
| 125 |
+
float: right;
|
| 126 |
+
font-weight: bold;
|
| 127 |
+
margin: 5px;
|
| 128 |
+
position: absolute;
|
| 129 |
+
right: 0;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
| 133 |
+
background-color: var(--darkened-bg);
|
| 134 |
+
border: 1px solid var(--border-color);
|
| 135 |
+
border-radius: 4px;
|
| 136 |
+
cursor: default;
|
| 137 |
+
float: left;
|
| 138 |
+
margin-right: 5px;
|
| 139 |
+
margin-top: 5px;
|
| 140 |
+
padding: 0 5px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
| 144 |
+
color: var(--body-quiet-color);
|
| 145 |
+
cursor: pointer;
|
| 146 |
+
display: inline-block;
|
| 147 |
+
font-weight: bold;
|
| 148 |
+
margin-right: 2px;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
| 152 |
+
color: var(--body-fg);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.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 {
|
| 156 |
+
float: right;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
| 160 |
+
margin-left: 5px;
|
| 161 |
+
margin-right: auto;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
| 165 |
+
margin-left: 2px;
|
| 166 |
+
margin-right: auto;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
| 170 |
+
border: solid var(--body-quiet-color) 1px;
|
| 171 |
+
outline: 0;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
| 175 |
+
background-color: var(--darkened-bg);
|
| 176 |
+
cursor: default;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
| 180 |
+
display: none;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.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 {
|
| 184 |
+
border-top-left-radius: 0;
|
| 185 |
+
border-top-right-radius: 0;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.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 {
|
| 189 |
+
border-bottom-left-radius: 0;
|
| 190 |
+
border-bottom-right-radius: 0;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.select2-container--admin-autocomplete .select2-search--dropdown {
|
| 194 |
+
background: var(--darkened-bg);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
| 198 |
+
background: var(--body-bg);
|
| 199 |
+
color: var(--body-fg);
|
| 200 |
+
border: 1px solid var(--border-color);
|
| 201 |
+
border-radius: 4px;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
| 205 |
+
background: transparent;
|
| 206 |
+
color: var(--body-fg);
|
| 207 |
+
border: none;
|
| 208 |
+
outline: 0;
|
| 209 |
+
box-shadow: none;
|
| 210 |
+
-webkit-appearance: textfield;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
| 214 |
+
max-height: 200px;
|
| 215 |
+
overflow-y: auto;
|
| 216 |
+
color: var(--body-fg);
|
| 217 |
+
background: var(--body-bg);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
| 221 |
+
padding: 0;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
| 225 |
+
color: var(--body-quiet-color);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
| 229 |
+
background-color: var(--selected-bg);
|
| 230 |
+
color: var(--body-fg);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
| 234 |
+
padding-left: 1em;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
| 238 |
+
padding-left: 0;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
| 242 |
+
margin-left: -1em;
|
| 243 |
+
padding-left: 2em;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 247 |
+
margin-left: -2em;
|
| 248 |
+
padding-left: 3em;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 252 |
+
margin-left: -3em;
|
| 253 |
+
padding-left: 4em;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 257 |
+
margin-left: -4em;
|
| 258 |
+
padding-left: 5em;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.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 {
|
| 262 |
+
margin-left: -5em;
|
| 263 |
+
padding-left: 6em;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
| 267 |
+
background-color: var(--primary);
|
| 268 |
+
color: var(--primary-fg);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.select2-container--admin-autocomplete .select2-results__group {
|
| 272 |
+
cursor: default;
|
| 273 |
+
display: block;
|
| 274 |
+
padding: 6px;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.errors .select2-selection {
|
| 278 |
+
border: 1px solid var(--error-fg);
|
| 279 |
+
}
|
static/admin/css/base.css
ADDED
|
@@ -0,0 +1,1180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
DJANGO Admin styles
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
/* VARIABLE DEFINITIONS */
|
| 6 |
+
html[data-theme="light"],
|
| 7 |
+
:root {
|
| 8 |
+
--primary: #79aec8;
|
| 9 |
+
--secondary: #417690;
|
| 10 |
+
--accent: #f5dd5d;
|
| 11 |
+
--primary-fg: #fff;
|
| 12 |
+
|
| 13 |
+
--body-fg: #333;
|
| 14 |
+
--body-bg: #fff;
|
| 15 |
+
--body-quiet-color: #666;
|
| 16 |
+
--body-medium-color: #444;
|
| 17 |
+
--body-loud-color: #000;
|
| 18 |
+
|
| 19 |
+
--header-color: #ffc;
|
| 20 |
+
--header-branding-color: var(--accent);
|
| 21 |
+
--header-bg: var(--secondary);
|
| 22 |
+
--header-link-color: var(--primary-fg);
|
| 23 |
+
|
| 24 |
+
--breadcrumbs-fg: #c4dce8;
|
| 25 |
+
--breadcrumbs-link-fg: var(--body-bg);
|
| 26 |
+
--breadcrumbs-bg: #264b5d;
|
| 27 |
+
|
| 28 |
+
--link-fg: #417893;
|
| 29 |
+
--link-hover-color: #036;
|
| 30 |
+
--link-selected-fg: var(--secondary);
|
| 31 |
+
|
| 32 |
+
--hairline-color: #e8e8e8;
|
| 33 |
+
--border-color: #ccc;
|
| 34 |
+
|
| 35 |
+
--error-fg: #ba2121;
|
| 36 |
+
|
| 37 |
+
--message-success-bg: #dfd;
|
| 38 |
+
--message-warning-bg: #ffc;
|
| 39 |
+
--message-error-bg: #ffefef;
|
| 40 |
+
|
| 41 |
+
--darkened-bg: #f8f8f8; /* A bit darker than --body-bg */
|
| 42 |
+
--selected-bg: #e4e4e4; /* E.g. selected table cells */
|
| 43 |
+
--selected-row: #ffc;
|
| 44 |
+
|
| 45 |
+
--button-fg: #fff;
|
| 46 |
+
--button-bg: var(--secondary);
|
| 47 |
+
--button-hover-bg: #205067;
|
| 48 |
+
--default-button-bg: #205067;
|
| 49 |
+
--default-button-hover-bg: var(--secondary);
|
| 50 |
+
--close-button-bg: #747474;
|
| 51 |
+
--close-button-hover-bg: #333;
|
| 52 |
+
--delete-button-bg: #ba2121;
|
| 53 |
+
--delete-button-hover-bg: #a41515;
|
| 54 |
+
|
| 55 |
+
--object-tools-fg: var(--button-fg);
|
| 56 |
+
--object-tools-bg: var(--close-button-bg);
|
| 57 |
+
--object-tools-hover-bg: var(--close-button-hover-bg);
|
| 58 |
+
|
| 59 |
+
--font-family-primary:
|
| 60 |
+
"Segoe UI",
|
| 61 |
+
system-ui,
|
| 62 |
+
Roboto,
|
| 63 |
+
"Helvetica Neue",
|
| 64 |
+
Arial,
|
| 65 |
+
sans-serif,
|
| 66 |
+
"Apple Color Emoji",
|
| 67 |
+
"Segoe UI Emoji",
|
| 68 |
+
"Segoe UI Symbol",
|
| 69 |
+
"Noto Color Emoji";
|
| 70 |
+
--font-family-monospace:
|
| 71 |
+
ui-monospace,
|
| 72 |
+
Menlo,
|
| 73 |
+
Monaco,
|
| 74 |
+
"Cascadia Mono",
|
| 75 |
+
"Segoe UI Mono",
|
| 76 |
+
"Roboto Mono",
|
| 77 |
+
"Oxygen Mono",
|
| 78 |
+
"Ubuntu Monospace",
|
| 79 |
+
"Source Code Pro",
|
| 80 |
+
"Fira Mono",
|
| 81 |
+
"Droid Sans Mono",
|
| 82 |
+
"Courier New",
|
| 83 |
+
monospace,
|
| 84 |
+
"Apple Color Emoji",
|
| 85 |
+
"Segoe UI Emoji",
|
| 86 |
+
"Segoe UI Symbol",
|
| 87 |
+
"Noto Color Emoji";
|
| 88 |
+
|
| 89 |
+
color-scheme: light;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
html, body {
|
| 93 |
+
height: 100%;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
body {
|
| 97 |
+
margin: 0;
|
| 98 |
+
padding: 0;
|
| 99 |
+
font-size: 0.875rem;
|
| 100 |
+
font-family: var(--font-family-primary);
|
| 101 |
+
color: var(--body-fg);
|
| 102 |
+
background: var(--body-bg);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* LINKS */
|
| 106 |
+
|
| 107 |
+
a:link, a:visited {
|
| 108 |
+
color: var(--link-fg);
|
| 109 |
+
text-decoration: none;
|
| 110 |
+
transition: color 0.15s, background 0.15s;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
a:focus, a:hover {
|
| 114 |
+
color: var(--link-hover-color);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
a:focus {
|
| 118 |
+
text-decoration: underline;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
a img {
|
| 122 |
+
border: none;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
a.section:link, a.section:visited {
|
| 126 |
+
color: var(--header-link-color);
|
| 127 |
+
text-decoration: none;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
a.section:focus, a.section:hover {
|
| 131 |
+
text-decoration: underline;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* GLOBAL DEFAULTS */
|
| 135 |
+
|
| 136 |
+
p, ol, ul, dl {
|
| 137 |
+
margin: .2em 0 .8em 0;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
p {
|
| 141 |
+
padding: 0;
|
| 142 |
+
line-height: 140%;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
h1,h2,h3,h4,h5 {
|
| 146 |
+
font-weight: bold;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
h1 {
|
| 150 |
+
margin: 0 0 20px;
|
| 151 |
+
font-weight: 300;
|
| 152 |
+
font-size: 1.25rem;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
h2 {
|
| 156 |
+
font-size: 1rem;
|
| 157 |
+
margin: 1em 0 .5em 0;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
h2.subhead {
|
| 161 |
+
font-weight: normal;
|
| 162 |
+
margin-top: 0;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
h3 {
|
| 166 |
+
font-size: 0.875rem;
|
| 167 |
+
margin: .8em 0 .3em 0;
|
| 168 |
+
color: var(--body-medium-color);
|
| 169 |
+
font-weight: bold;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
h4 {
|
| 173 |
+
font-size: 0.75rem;
|
| 174 |
+
margin: 1em 0 .8em 0;
|
| 175 |
+
padding-bottom: 3px;
|
| 176 |
+
color: var(--body-medium-color);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
h5 {
|
| 180 |
+
font-size: 0.625rem;
|
| 181 |
+
margin: 1.5em 0 .5em 0;
|
| 182 |
+
color: var(--body-quiet-color);
|
| 183 |
+
text-transform: uppercase;
|
| 184 |
+
letter-spacing: 1px;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
ul > li {
|
| 188 |
+
list-style-type: square;
|
| 189 |
+
padding: 1px 0;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
li ul {
|
| 193 |
+
margin-bottom: 0;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
li, dt, dd {
|
| 197 |
+
font-size: 0.8125rem;
|
| 198 |
+
line-height: 1.25rem;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
dt {
|
| 202 |
+
font-weight: bold;
|
| 203 |
+
margin-top: 4px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
dd {
|
| 207 |
+
margin-left: 0;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
form {
|
| 211 |
+
margin: 0;
|
| 212 |
+
padding: 0;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
fieldset {
|
| 216 |
+
margin: 0;
|
| 217 |
+
min-width: 0;
|
| 218 |
+
padding: 0;
|
| 219 |
+
border: none;
|
| 220 |
+
border-top: 1px solid var(--hairline-color);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
details summary {
|
| 224 |
+
cursor: pointer;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
blockquote {
|
| 228 |
+
font-size: 0.6875rem;
|
| 229 |
+
color: #777;
|
| 230 |
+
margin-left: 2px;
|
| 231 |
+
padding-left: 10px;
|
| 232 |
+
border-left: 5px solid #ddd;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
code, pre {
|
| 236 |
+
font-family: var(--font-family-monospace);
|
| 237 |
+
color: var(--body-quiet-color);
|
| 238 |
+
font-size: 0.75rem;
|
| 239 |
+
overflow-x: auto;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
pre.literal-block {
|
| 243 |
+
margin: 10px;
|
| 244 |
+
background: var(--darkened-bg);
|
| 245 |
+
padding: 6px 8px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
code strong {
|
| 249 |
+
color: #930;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
hr {
|
| 253 |
+
clear: both;
|
| 254 |
+
color: var(--hairline-color);
|
| 255 |
+
background-color: var(--hairline-color);
|
| 256 |
+
height: 1px;
|
| 257 |
+
border: none;
|
| 258 |
+
margin: 0;
|
| 259 |
+
padding: 0;
|
| 260 |
+
line-height: 1px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* TEXT STYLES & MODIFIERS */
|
| 264 |
+
|
| 265 |
+
.small {
|
| 266 |
+
font-size: 0.6875rem;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.mini {
|
| 270 |
+
font-size: 0.625rem;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.help, p.help, form p.help, div.help, form div.help, div.help li {
|
| 274 |
+
font-size: 0.6875rem;
|
| 275 |
+
color: var(--body-quiet-color);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
div.help ul {
|
| 279 |
+
margin-bottom: 0;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.help-tooltip {
|
| 283 |
+
cursor: help;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
p img, h1 img, h2 img, h3 img, h4 img, td img {
|
| 287 |
+
vertical-align: middle;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.quiet, a.quiet:link, a.quiet:visited {
|
| 291 |
+
color: var(--body-quiet-color);
|
| 292 |
+
font-weight: normal;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.clear {
|
| 296 |
+
clear: both;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.nowrap {
|
| 300 |
+
white-space: nowrap;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.hidden {
|
| 304 |
+
display: none !important;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/* TABLES */
|
| 308 |
+
|
| 309 |
+
table {
|
| 310 |
+
border-collapse: collapse;
|
| 311 |
+
border-color: var(--border-color);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
td, th {
|
| 315 |
+
font-size: 0.8125rem;
|
| 316 |
+
line-height: 1rem;
|
| 317 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 318 |
+
vertical-align: top;
|
| 319 |
+
padding: 8px;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
th {
|
| 323 |
+
font-weight: 500;
|
| 324 |
+
text-align: left;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
thead th,
|
| 328 |
+
tfoot td {
|
| 329 |
+
color: var(--body-quiet-color);
|
| 330 |
+
padding: 5px 10px;
|
| 331 |
+
font-size: 0.6875rem;
|
| 332 |
+
background: var(--body-bg);
|
| 333 |
+
border: none;
|
| 334 |
+
border-top: 1px solid var(--hairline-color);
|
| 335 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
tfoot td {
|
| 339 |
+
border-bottom: none;
|
| 340 |
+
border-top: 1px solid var(--hairline-color);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
thead th.required {
|
| 344 |
+
font-weight: bold;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
tr.alt {
|
| 348 |
+
background: var(--darkened-bg);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
tr:nth-child(odd), .row-form-errors {
|
| 352 |
+
background: var(--body-bg);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
tr:nth-child(even),
|
| 356 |
+
tr:nth-child(even) .errorlist,
|
| 357 |
+
tr:nth-child(odd) + .row-form-errors,
|
| 358 |
+
tr:nth-child(odd) + .row-form-errors .errorlist {
|
| 359 |
+
background: var(--darkened-bg);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
/* SORTABLE TABLES */
|
| 363 |
+
|
| 364 |
+
thead th {
|
| 365 |
+
padding: 5px 10px;
|
| 366 |
+
line-height: normal;
|
| 367 |
+
text-transform: uppercase;
|
| 368 |
+
background: var(--darkened-bg);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
thead th a:link, thead th a:visited {
|
| 372 |
+
color: var(--body-quiet-color);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
thead th.sorted {
|
| 376 |
+
background: var(--selected-bg);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
thead th.sorted .text {
|
| 380 |
+
padding-right: 42px;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
table thead th .text span {
|
| 384 |
+
padding: 8px 10px;
|
| 385 |
+
display: block;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
table thead th .text a {
|
| 389 |
+
display: block;
|
| 390 |
+
cursor: pointer;
|
| 391 |
+
padding: 8px 10px;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
table thead th .text a:focus, table thead th .text a:hover {
|
| 395 |
+
background: var(--selected-bg);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
thead th.sorted a.sortremove {
|
| 399 |
+
visibility: hidden;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
table thead th.sorted:hover a.sortremove {
|
| 403 |
+
visibility: visible;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
table thead th.sorted .sortoptions {
|
| 407 |
+
display: block;
|
| 408 |
+
padding: 9px 5px 0 5px;
|
| 409 |
+
float: right;
|
| 410 |
+
text-align: right;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
table thead th.sorted .sortpriority {
|
| 414 |
+
font-size: .8em;
|
| 415 |
+
min-width: 12px;
|
| 416 |
+
text-align: center;
|
| 417 |
+
vertical-align: 3px;
|
| 418 |
+
margin-left: 2px;
|
| 419 |
+
margin-right: 2px;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
table thead th.sorted .sortoptions a {
|
| 423 |
+
position: relative;
|
| 424 |
+
width: 14px;
|
| 425 |
+
height: 14px;
|
| 426 |
+
display: inline-block;
|
| 427 |
+
background: url(../img/sorting-icons.svg) 0 0 no-repeat;
|
| 428 |
+
background-size: 14px auto;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
table thead th.sorted .sortoptions a.sortremove {
|
| 432 |
+
background-position: 0 0;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
table thead th.sorted .sortoptions a.sortremove:after {
|
| 436 |
+
content: '\\';
|
| 437 |
+
position: absolute;
|
| 438 |
+
top: -6px;
|
| 439 |
+
left: 3px;
|
| 440 |
+
font-weight: 200;
|
| 441 |
+
font-size: 1.125rem;
|
| 442 |
+
color: var(--body-quiet-color);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
table thead th.sorted .sortoptions a.sortremove:focus:after,
|
| 446 |
+
table thead th.sorted .sortoptions a.sortremove:hover:after {
|
| 447 |
+
color: var(--link-fg);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
table thead th.sorted .sortoptions a.sortremove:focus,
|
| 451 |
+
table thead th.sorted .sortoptions a.sortremove:hover {
|
| 452 |
+
background-position: 0 -14px;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
table thead th.sorted .sortoptions a.ascending {
|
| 456 |
+
background-position: 0 -28px;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
table thead th.sorted .sortoptions a.ascending:focus,
|
| 460 |
+
table thead th.sorted .sortoptions a.ascending:hover {
|
| 461 |
+
background-position: 0 -42px;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
table thead th.sorted .sortoptions a.descending {
|
| 465 |
+
top: 1px;
|
| 466 |
+
background-position: 0 -56px;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
table thead th.sorted .sortoptions a.descending:focus,
|
| 470 |
+
table thead th.sorted .sortoptions a.descending:hover {
|
| 471 |
+
background-position: 0 -70px;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/* FORM DEFAULTS */
|
| 475 |
+
|
| 476 |
+
input, textarea, select, .form-row p, form .button {
|
| 477 |
+
margin: 2px 0;
|
| 478 |
+
padding: 2px 3px;
|
| 479 |
+
vertical-align: middle;
|
| 480 |
+
font-family: var(--font-family-primary);
|
| 481 |
+
font-weight: normal;
|
| 482 |
+
font-size: 0.8125rem;
|
| 483 |
+
}
|
| 484 |
+
.form-row div.help {
|
| 485 |
+
padding: 2px 3px;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
textarea {
|
| 489 |
+
vertical-align: top;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
/*
|
| 493 |
+
Minifiers remove the default (text) "type" attribute from "input" HTML tags.
|
| 494 |
+
Add input:not([type]) to make the CSS stylesheet work the same.
|
| 495 |
+
*/
|
| 496 |
+
input:not([type]), input[type=text], input[type=password], input[type=email],
|
| 497 |
+
input[type=url], input[type=number], input[type=tel], textarea, select,
|
| 498 |
+
.vTextField {
|
| 499 |
+
border: 1px solid var(--border-color);
|
| 500 |
+
border-radius: 4px;
|
| 501 |
+
padding: 5px 6px;
|
| 502 |
+
margin-top: 0;
|
| 503 |
+
color: var(--body-fg);
|
| 504 |
+
background-color: var(--body-bg);
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
/*
|
| 508 |
+
Minifiers remove the default (text) "type" attribute from "input" HTML tags.
|
| 509 |
+
Add input:not([type]) to make the CSS stylesheet work the same.
|
| 510 |
+
*/
|
| 511 |
+
input:not([type]):focus, input[type=text]:focus, input[type=password]:focus,
|
| 512 |
+
input[type=email]:focus, input[type=url]:focus, input[type=number]:focus,
|
| 513 |
+
input[type=tel]:focus, textarea:focus, select:focus, .vTextField:focus {
|
| 514 |
+
border-color: var(--body-quiet-color);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
select {
|
| 518 |
+
height: 1.875rem;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
select[multiple] {
|
| 522 |
+
/* Allow HTML size attribute to override the height in the rule above. */
|
| 523 |
+
height: auto;
|
| 524 |
+
min-height: 150px;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* FORM BUTTONS */
|
| 528 |
+
|
| 529 |
+
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
| 530 |
+
background: var(--button-bg);
|
| 531 |
+
padding: 10px 15px;
|
| 532 |
+
border: none;
|
| 533 |
+
border-radius: 4px;
|
| 534 |
+
color: var(--button-fg);
|
| 535 |
+
cursor: pointer;
|
| 536 |
+
transition: background 0.15s;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
a.button {
|
| 540 |
+
padding: 4px 5px;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.button:active, input[type=submit]:active, input[type=button]:active,
|
| 544 |
+
.button:focus, input[type=submit]:focus, input[type=button]:focus,
|
| 545 |
+
.button:hover, input[type=submit]:hover, input[type=button]:hover {
|
| 546 |
+
background: var(--button-hover-bg);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.button[disabled], input[type=submit][disabled], input[type=button][disabled] {
|
| 550 |
+
opacity: 0.4;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.button.default, input[type=submit].default, .submit-row input.default {
|
| 554 |
+
border: none;
|
| 555 |
+
font-weight: 400;
|
| 556 |
+
background: var(--default-button-bg);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.button.default:active, input[type=submit].default:active,
|
| 560 |
+
.button.default:focus, input[type=submit].default:focus,
|
| 561 |
+
.button.default:hover, input[type=submit].default:hover {
|
| 562 |
+
background: var(--default-button-hover-bg);
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.button[disabled].default,
|
| 566 |
+
input[type=submit][disabled].default,
|
| 567 |
+
input[type=button][disabled].default {
|
| 568 |
+
opacity: 0.4;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
/* MODULES */
|
| 573 |
+
|
| 574 |
+
.module {
|
| 575 |
+
border: none;
|
| 576 |
+
margin-bottom: 30px;
|
| 577 |
+
background: var(--body-bg);
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.module p, .module ul, .module h3, .module h4, .module dl, .module pre {
|
| 581 |
+
padding-left: 10px;
|
| 582 |
+
padding-right: 10px;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.module blockquote {
|
| 586 |
+
margin-left: 12px;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.module ul, .module ol {
|
| 590 |
+
margin-left: 1.5em;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.module h3 {
|
| 594 |
+
margin-top: .6em;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.module h2, .module caption, .inline-group h2 {
|
| 598 |
+
margin: 0;
|
| 599 |
+
padding: 8px;
|
| 600 |
+
font-weight: 400;
|
| 601 |
+
font-size: 0.8125rem;
|
| 602 |
+
text-align: left;
|
| 603 |
+
background: var(--header-bg);
|
| 604 |
+
color: var(--header-link-color);
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.module caption,
|
| 608 |
+
.inline-group h2 {
|
| 609 |
+
font-size: 0.75rem;
|
| 610 |
+
letter-spacing: 0.5px;
|
| 611 |
+
text-transform: uppercase;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.module table {
|
| 615 |
+
border-collapse: collapse;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
/* MESSAGES & ERRORS */
|
| 619 |
+
|
| 620 |
+
ul.messagelist {
|
| 621 |
+
padding: 0;
|
| 622 |
+
margin: 0;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
ul.messagelist li {
|
| 626 |
+
display: block;
|
| 627 |
+
font-weight: 400;
|
| 628 |
+
font-size: 0.8125rem;
|
| 629 |
+
padding: 10px 10px 10px 65px;
|
| 630 |
+
margin: 0 0 10px 0;
|
| 631 |
+
background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat;
|
| 632 |
+
background-size: 16px auto;
|
| 633 |
+
color: var(--body-fg);
|
| 634 |
+
word-break: break-word;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
ul.messagelist li.warning {
|
| 638 |
+
background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat;
|
| 639 |
+
background-size: 14px auto;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
ul.messagelist li.error {
|
| 643 |
+
background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat;
|
| 644 |
+
background-size: 16px auto;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.errornote {
|
| 648 |
+
font-size: 0.875rem;
|
| 649 |
+
font-weight: 700;
|
| 650 |
+
display: block;
|
| 651 |
+
padding: 10px 12px;
|
| 652 |
+
margin: 0 0 10px 0;
|
| 653 |
+
color: var(--error-fg);
|
| 654 |
+
border: 1px solid var(--error-fg);
|
| 655 |
+
border-radius: 4px;
|
| 656 |
+
background-color: var(--body-bg);
|
| 657 |
+
background-position: 5px 12px;
|
| 658 |
+
overflow-wrap: break-word;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
ul.errorlist {
|
| 662 |
+
margin: 0 0 4px;
|
| 663 |
+
padding: 0;
|
| 664 |
+
color: var(--error-fg);
|
| 665 |
+
background: var(--body-bg);
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
ul.errorlist li {
|
| 669 |
+
font-size: 0.8125rem;
|
| 670 |
+
display: block;
|
| 671 |
+
margin-bottom: 4px;
|
| 672 |
+
overflow-wrap: break-word;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
ul.errorlist li:first-child {
|
| 676 |
+
margin-top: 0;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
ul.errorlist li a {
|
| 680 |
+
color: inherit;
|
| 681 |
+
text-decoration: underline;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
td ul.errorlist {
|
| 685 |
+
margin: 0;
|
| 686 |
+
padding: 0;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
td ul.errorlist li {
|
| 690 |
+
margin: 0;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.form-row.errors {
|
| 694 |
+
margin: 0;
|
| 695 |
+
border: none;
|
| 696 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 697 |
+
background: none;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.form-row.errors ul.errorlist li {
|
| 701 |
+
padding-left: 0;
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
.errors input, .errors select, .errors textarea,
|
| 705 |
+
td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea {
|
| 706 |
+
border: 1px solid var(--error-fg);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.description {
|
| 710 |
+
font-size: 0.75rem;
|
| 711 |
+
padding: 5px 0 0 12px;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
/* BREADCRUMBS */
|
| 715 |
+
|
| 716 |
+
div.breadcrumbs {
|
| 717 |
+
background: var(--breadcrumbs-bg);
|
| 718 |
+
padding: 10px 40px;
|
| 719 |
+
border: none;
|
| 720 |
+
color: var(--breadcrumbs-fg);
|
| 721 |
+
text-align: left;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
div.breadcrumbs a {
|
| 725 |
+
color: var(--breadcrumbs-link-fg);
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
div.breadcrumbs a:focus, div.breadcrumbs a:hover {
|
| 729 |
+
color: var(--breadcrumbs-fg);
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
/* ACTION ICONS */
|
| 733 |
+
|
| 734 |
+
.viewlink, .inlineviewlink {
|
| 735 |
+
padding-left: 16px;
|
| 736 |
+
background: url(../img/icon-viewlink.svg) 0 1px no-repeat;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.hidelink {
|
| 740 |
+
padding-left: 16px;
|
| 741 |
+
background: url(../img/icon-hidelink.svg) 0 1px no-repeat;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
.addlink {
|
| 745 |
+
padding-left: 16px;
|
| 746 |
+
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.changelink, .inlinechangelink {
|
| 750 |
+
padding-left: 16px;
|
| 751 |
+
background: url(../img/icon-changelink.svg) 0 1px no-repeat;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.deletelink {
|
| 755 |
+
padding-left: 16px;
|
| 756 |
+
background: url(../img/icon-deletelink.svg) 0 1px no-repeat;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
a.deletelink:link, a.deletelink:visited {
|
| 760 |
+
color: #CC3434; /* XXX Probably unused? */
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
a.deletelink:focus, a.deletelink:hover {
|
| 764 |
+
color: #993333; /* XXX Probably unused? */
|
| 765 |
+
text-decoration: none;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
/* OBJECT TOOLS */
|
| 769 |
+
|
| 770 |
+
.object-tools {
|
| 771 |
+
font-size: 0.625rem;
|
| 772 |
+
font-weight: bold;
|
| 773 |
+
padding-left: 0;
|
| 774 |
+
float: right;
|
| 775 |
+
position: relative;
|
| 776 |
+
margin-top: -48px;
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
.object-tools li {
|
| 780 |
+
display: block;
|
| 781 |
+
float: left;
|
| 782 |
+
margin-left: 5px;
|
| 783 |
+
height: 1rem;
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
.object-tools a {
|
| 787 |
+
border-radius: 15px;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.object-tools a:link, .object-tools a:visited {
|
| 791 |
+
display: block;
|
| 792 |
+
float: left;
|
| 793 |
+
padding: 3px 12px;
|
| 794 |
+
background: var(--object-tools-bg);
|
| 795 |
+
color: var(--object-tools-fg);
|
| 796 |
+
font-weight: 400;
|
| 797 |
+
font-size: 0.6875rem;
|
| 798 |
+
text-transform: uppercase;
|
| 799 |
+
letter-spacing: 0.5px;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.object-tools a:focus, .object-tools a:hover {
|
| 803 |
+
background-color: var(--object-tools-hover-bg);
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.object-tools a:focus{
|
| 807 |
+
text-decoration: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.object-tools a.viewsitelink, .object-tools a.addlink {
|
| 811 |
+
background-repeat: no-repeat;
|
| 812 |
+
background-position: right 7px center;
|
| 813 |
+
padding-right: 26px;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
.object-tools a.viewsitelink {
|
| 817 |
+
background-image: url(../img/tooltag-arrowright.svg);
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.object-tools a.addlink {
|
| 821 |
+
background-image: url(../img/tooltag-add.svg);
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
/* OBJECT HISTORY */
|
| 825 |
+
|
| 826 |
+
#change-history table {
|
| 827 |
+
width: 100%;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
#change-history table tbody th {
|
| 831 |
+
width: 16em;
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
#change-history .paginator {
|
| 835 |
+
color: var(--body-quiet-color);
|
| 836 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 837 |
+
background: var(--body-bg);
|
| 838 |
+
overflow: hidden;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
/* PAGE STRUCTURE */
|
| 842 |
+
|
| 843 |
+
#container {
|
| 844 |
+
position: relative;
|
| 845 |
+
width: 100%;
|
| 846 |
+
min-width: 980px;
|
| 847 |
+
padding: 0;
|
| 848 |
+
display: flex;
|
| 849 |
+
flex-direction: column;
|
| 850 |
+
height: 100%;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
#container > .main {
|
| 854 |
+
display: flex;
|
| 855 |
+
flex: 1 0 auto;
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
.main > .content {
|
| 859 |
+
flex: 1 0;
|
| 860 |
+
max-width: 100%;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
.skip-to-content-link {
|
| 864 |
+
position: absolute;
|
| 865 |
+
top: -999px;
|
| 866 |
+
margin: 5px;
|
| 867 |
+
padding: 5px;
|
| 868 |
+
background: var(--body-bg);
|
| 869 |
+
z-index: 1;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.skip-to-content-link:focus {
|
| 873 |
+
left: 0px;
|
| 874 |
+
top: 0px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
#content {
|
| 878 |
+
padding: 20px 40px;
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
.dashboard #content {
|
| 882 |
+
width: 600px;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
#content-main {
|
| 886 |
+
float: left;
|
| 887 |
+
width: 100%;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
#content-related {
|
| 891 |
+
float: right;
|
| 892 |
+
width: 260px;
|
| 893 |
+
position: relative;
|
| 894 |
+
margin-right: -300px;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
@media (forced-colors: active) {
|
| 898 |
+
#content-related {
|
| 899 |
+
border: 1px solid;
|
| 900 |
+
}
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
/* COLUMN TYPES */
|
| 904 |
+
|
| 905 |
+
.colMS {
|
| 906 |
+
margin-right: 300px;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.colSM {
|
| 910 |
+
margin-left: 300px;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.colSM #content-related {
|
| 914 |
+
float: left;
|
| 915 |
+
margin-right: 0;
|
| 916 |
+
margin-left: -300px;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
.colSM #content-main {
|
| 920 |
+
float: right;
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
+
.popup .colM {
|
| 924 |
+
width: auto;
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
/* HEADER */
|
| 928 |
+
|
| 929 |
+
#header {
|
| 930 |
+
width: auto;
|
| 931 |
+
height: auto;
|
| 932 |
+
display: flex;
|
| 933 |
+
justify-content: space-between;
|
| 934 |
+
align-items: center;
|
| 935 |
+
padding: 10px 40px;
|
| 936 |
+
background: var(--header-bg);
|
| 937 |
+
color: var(--header-color);
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
#header a:link, #header a:visited, #logout-form button {
|
| 941 |
+
color: var(--header-link-color);
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
#header a:focus , #header a:hover {
|
| 945 |
+
text-decoration: underline;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
@media (forced-colors: active) {
|
| 949 |
+
#header {
|
| 950 |
+
border-bottom: 1px solid;
|
| 951 |
+
}
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
#branding {
|
| 955 |
+
display: flex;
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
#site-name {
|
| 959 |
+
padding: 0;
|
| 960 |
+
margin: 0;
|
| 961 |
+
margin-inline-end: 20px;
|
| 962 |
+
font-weight: 300;
|
| 963 |
+
font-size: 1.5rem;
|
| 964 |
+
color: var(--header-branding-color);
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
#site-name a:link, #site-name a:visited {
|
| 968 |
+
color: var(--accent);
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
#branding h2 {
|
| 972 |
+
padding: 0 10px;
|
| 973 |
+
font-size: 0.875rem;
|
| 974 |
+
margin: -8px 0 8px 0;
|
| 975 |
+
font-weight: normal;
|
| 976 |
+
color: var(--header-color);
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
#branding a:hover {
|
| 980 |
+
text-decoration: none;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
#logout-form {
|
| 984 |
+
display: inline;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
#logout-form button {
|
| 988 |
+
background: none;
|
| 989 |
+
border: 0;
|
| 990 |
+
cursor: pointer;
|
| 991 |
+
font-family: var(--font-family-primary);
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
#user-tools {
|
| 995 |
+
float: right;
|
| 996 |
+
margin: 0 0 0 20px;
|
| 997 |
+
text-align: right;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
#user-tools, #logout-form button{
|
| 1001 |
+
padding: 0;
|
| 1002 |
+
font-weight: 300;
|
| 1003 |
+
font-size: 0.6875rem;
|
| 1004 |
+
letter-spacing: 0.5px;
|
| 1005 |
+
text-transform: uppercase;
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
#user-tools a, #logout-form button {
|
| 1009 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
#user-tools a:focus, #user-tools a:hover,
|
| 1013 |
+
#logout-form button:active, #logout-form button:hover {
|
| 1014 |
+
text-decoration: none;
|
| 1015 |
+
border-bottom: 0;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
#logout-form button:active, #logout-form button:hover {
|
| 1019 |
+
margin-bottom: 1px;
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
/* SIDEBAR */
|
| 1023 |
+
|
| 1024 |
+
#content-related {
|
| 1025 |
+
background: var(--darkened-bg);
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
#content-related .module {
|
| 1029 |
+
background: none;
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
#content-related h3 {
|
| 1033 |
+
color: var(--body-quiet-color);
|
| 1034 |
+
padding: 0 16px;
|
| 1035 |
+
margin: 0 0 16px;
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
#content-related h4 {
|
| 1039 |
+
font-size: 0.8125rem;
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
#content-related p {
|
| 1043 |
+
padding-left: 16px;
|
| 1044 |
+
padding-right: 16px;
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
#content-related .actionlist {
|
| 1048 |
+
padding: 0;
|
| 1049 |
+
margin: 16px;
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
#content-related .actionlist li {
|
| 1053 |
+
line-height: 1.2;
|
| 1054 |
+
margin-bottom: 10px;
|
| 1055 |
+
padding-left: 18px;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
#content-related .module h2 {
|
| 1059 |
+
background: none;
|
| 1060 |
+
padding: 16px;
|
| 1061 |
+
margin-bottom: 16px;
|
| 1062 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 1063 |
+
font-size: 1.125rem;
|
| 1064 |
+
color: var(--body-fg);
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
.delete-confirmation form input[type="submit"] {
|
| 1068 |
+
background: var(--delete-button-bg);
|
| 1069 |
+
border-radius: 4px;
|
| 1070 |
+
padding: 10px 15px;
|
| 1071 |
+
color: var(--button-fg);
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
.delete-confirmation form input[type="submit"]:active,
|
| 1075 |
+
.delete-confirmation form input[type="submit"]:focus,
|
| 1076 |
+
.delete-confirmation form input[type="submit"]:hover {
|
| 1077 |
+
background: var(--delete-button-hover-bg);
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
.delete-confirmation form .cancel-link {
|
| 1081 |
+
display: inline-block;
|
| 1082 |
+
vertical-align: middle;
|
| 1083 |
+
height: 0.9375rem;
|
| 1084 |
+
line-height: 0.9375rem;
|
| 1085 |
+
border-radius: 4px;
|
| 1086 |
+
padding: 10px 15px;
|
| 1087 |
+
color: var(--button-fg);
|
| 1088 |
+
background: var(--close-button-bg);
|
| 1089 |
+
margin: 0 0 0 10px;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
.delete-confirmation form .cancel-link:active,
|
| 1093 |
+
.delete-confirmation form .cancel-link:focus,
|
| 1094 |
+
.delete-confirmation form .cancel-link:hover {
|
| 1095 |
+
background: var(--close-button-hover-bg);
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
/* POPUP */
|
| 1099 |
+
.popup #content {
|
| 1100 |
+
padding: 20px;
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
.popup #container {
|
| 1104 |
+
min-width: 0;
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
.popup #header {
|
| 1108 |
+
padding: 10px 20px;
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
/* PAGINATOR */
|
| 1112 |
+
|
| 1113 |
+
.paginator {
|
| 1114 |
+
display: flex;
|
| 1115 |
+
align-items: center;
|
| 1116 |
+
gap: 4px;
|
| 1117 |
+
font-size: 0.8125rem;
|
| 1118 |
+
padding-top: 10px;
|
| 1119 |
+
padding-bottom: 10px;
|
| 1120 |
+
line-height: 22px;
|
| 1121 |
+
margin: 0;
|
| 1122 |
+
border-top: 1px solid var(--hairline-color);
|
| 1123 |
+
width: 100%;
|
| 1124 |
+
box-sizing: border-box;
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
.paginator a:link, .paginator a:visited {
|
| 1128 |
+
padding: 2px 6px;
|
| 1129 |
+
background: var(--button-bg);
|
| 1130 |
+
text-decoration: none;
|
| 1131 |
+
color: var(--button-fg);
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
.paginator a.showall {
|
| 1135 |
+
border: none;
|
| 1136 |
+
background: none;
|
| 1137 |
+
color: var(--link-fg);
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
.paginator a.showall:focus, .paginator a.showall:hover {
|
| 1141 |
+
background: none;
|
| 1142 |
+
color: var(--link-hover-color);
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
.paginator .end {
|
| 1146 |
+
margin-right: 6px;
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
.paginator .this-page {
|
| 1150 |
+
padding: 2px 6px;
|
| 1151 |
+
font-weight: bold;
|
| 1152 |
+
font-size: 0.8125rem;
|
| 1153 |
+
vertical-align: top;
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
.paginator a:focus, .paginator a:hover {
|
| 1157 |
+
color: white;
|
| 1158 |
+
background: var(--link-hover-color);
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
+
.paginator input {
|
| 1162 |
+
margin-left: auto;
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
.base-svgs {
|
| 1166 |
+
display: none;
|
| 1167 |
+
}
|
| 1168 |
+
|
| 1169 |
+
.visually-hidden {
|
| 1170 |
+
position: absolute;
|
| 1171 |
+
width: 1px;
|
| 1172 |
+
height: 1px;
|
| 1173 |
+
padding: 0;
|
| 1174 |
+
overflow: hidden;
|
| 1175 |
+
clip: rect(0,0,0,0);
|
| 1176 |
+
white-space: nowrap;
|
| 1177 |
+
border: 0;
|
| 1178 |
+
color: var(--body-fg);
|
| 1179 |
+
background-color: var(--body-bg);
|
| 1180 |
+
}
|
static/admin/css/changelists.css
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CHANGELISTS */
|
| 2 |
+
|
| 3 |
+
#changelist {
|
| 4 |
+
display: flex;
|
| 5 |
+
align-items: flex-start;
|
| 6 |
+
justify-content: space-between;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
#changelist .changelist-form-container {
|
| 10 |
+
flex: 1 1 auto;
|
| 11 |
+
min-width: 0;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
#changelist table {
|
| 15 |
+
width: 100%;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.change-list .hiddenfields { display:none; }
|
| 19 |
+
|
| 20 |
+
.change-list .filtered table {
|
| 21 |
+
border-right: none;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.change-list .filtered {
|
| 25 |
+
min-height: 400px;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.change-list .filtered .results, .change-list .filtered .paginator,
|
| 29 |
+
.filtered #toolbar, .filtered div.xfull {
|
| 30 |
+
width: auto;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.change-list .filtered table tbody th {
|
| 34 |
+
padding-right: 1em;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#changelist-form .results {
|
| 38 |
+
overflow-x: auto;
|
| 39 |
+
width: 100%;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
#changelist .toplinks {
|
| 43 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
#changelist .paginator {
|
| 47 |
+
color: var(--body-quiet-color);
|
| 48 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 49 |
+
background: var(--body-bg);
|
| 50 |
+
overflow: hidden;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* CHANGELIST TABLES */
|
| 54 |
+
|
| 55 |
+
#changelist table thead th {
|
| 56 |
+
padding: 0;
|
| 57 |
+
white-space: nowrap;
|
| 58 |
+
vertical-align: middle;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
#changelist table thead th.action-checkbox-column {
|
| 62 |
+
width: 1.5em;
|
| 63 |
+
text-align: center;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
#changelist table tbody td.action-checkbox {
|
| 67 |
+
text-align: center;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
#changelist table tfoot {
|
| 71 |
+
color: var(--body-quiet-color);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* TOOLBAR */
|
| 75 |
+
|
| 76 |
+
#toolbar {
|
| 77 |
+
padding: 8px 10px;
|
| 78 |
+
margin-bottom: 15px;
|
| 79 |
+
border-top: 1px solid var(--hairline-color);
|
| 80 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 81 |
+
background: var(--darkened-bg);
|
| 82 |
+
color: var(--body-quiet-color);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
#toolbar form input {
|
| 86 |
+
border-radius: 4px;
|
| 87 |
+
font-size: 0.875rem;
|
| 88 |
+
padding: 5px;
|
| 89 |
+
color: var(--body-fg);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
#toolbar #searchbar {
|
| 93 |
+
height: 1.1875rem;
|
| 94 |
+
border: 1px solid var(--border-color);
|
| 95 |
+
padding: 2px 5px;
|
| 96 |
+
margin: 0;
|
| 97 |
+
vertical-align: top;
|
| 98 |
+
font-size: 0.8125rem;
|
| 99 |
+
max-width: 100%;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
#toolbar #searchbar:focus {
|
| 103 |
+
border-color: var(--body-quiet-color);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
#toolbar form input[type="submit"] {
|
| 107 |
+
border: 1px solid var(--border-color);
|
| 108 |
+
font-size: 0.8125rem;
|
| 109 |
+
padding: 4px 8px;
|
| 110 |
+
margin: 0;
|
| 111 |
+
vertical-align: middle;
|
| 112 |
+
background: var(--body-bg);
|
| 113 |
+
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
color: var(--body-fg);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
#toolbar form input[type="submit"]:focus,
|
| 119 |
+
#toolbar form input[type="submit"]:hover {
|
| 120 |
+
border-color: var(--body-quiet-color);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
#changelist-search img {
|
| 124 |
+
vertical-align: middle;
|
| 125 |
+
margin-right: 4px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
#changelist-search .help {
|
| 129 |
+
word-break: break-word;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* FILTER COLUMN */
|
| 133 |
+
|
| 134 |
+
#changelist-filter {
|
| 135 |
+
flex: 0 0 240px;
|
| 136 |
+
order: 1;
|
| 137 |
+
background: var(--darkened-bg);
|
| 138 |
+
border-left: none;
|
| 139 |
+
margin: 0 0 0 30px;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
@media (forced-colors: active) {
|
| 143 |
+
#changelist-filter {
|
| 144 |
+
border: 1px solid;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#changelist-filter h2 {
|
| 149 |
+
font-size: 0.875rem;
|
| 150 |
+
text-transform: uppercase;
|
| 151 |
+
letter-spacing: 0.5px;
|
| 152 |
+
padding: 5px 15px;
|
| 153 |
+
margin-bottom: 12px;
|
| 154 |
+
border-bottom: none;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
#changelist-filter h3,
|
| 158 |
+
#changelist-filter details summary {
|
| 159 |
+
font-weight: 400;
|
| 160 |
+
padding: 0 15px;
|
| 161 |
+
margin-bottom: 10px;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
#changelist-filter details summary > * {
|
| 165 |
+
display: inline;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
#changelist-filter details > summary {
|
| 169 |
+
list-style-type: none;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
#changelist-filter details > summary::-webkit-details-marker {
|
| 173 |
+
display: none;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
#changelist-filter details > summary::before {
|
| 177 |
+
content: '→';
|
| 178 |
+
font-weight: bold;
|
| 179 |
+
color: var(--link-hover-color);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
#changelist-filter details[open] > summary::before {
|
| 183 |
+
content: '↓';
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
#changelist-filter ul {
|
| 187 |
+
margin: 5px 0;
|
| 188 |
+
padding: 0 15px 15px;
|
| 189 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
#changelist-filter ul:last-child {
|
| 193 |
+
border-bottom: none;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
#changelist-filter li {
|
| 197 |
+
list-style-type: none;
|
| 198 |
+
margin-left: 0;
|
| 199 |
+
padding-left: 0;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
#changelist-filter a {
|
| 203 |
+
display: block;
|
| 204 |
+
color: var(--body-quiet-color);
|
| 205 |
+
word-break: break-word;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
#changelist-filter li.selected {
|
| 209 |
+
border-left: 5px solid var(--hairline-color);
|
| 210 |
+
padding-left: 10px;
|
| 211 |
+
margin-left: -15px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
#changelist-filter li.selected a {
|
| 215 |
+
color: var(--link-selected-fg);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
#changelist-filter a:focus, #changelist-filter a:hover,
|
| 219 |
+
#changelist-filter li.selected a:focus,
|
| 220 |
+
#changelist-filter li.selected a:hover {
|
| 221 |
+
color: var(--link-hover-color);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
#changelist-filter #changelist-filter-extra-actions {
|
| 225 |
+
font-size: 0.8125rem;
|
| 226 |
+
margin-bottom: 10px;
|
| 227 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* DATE DRILLDOWN */
|
| 231 |
+
|
| 232 |
+
.change-list .toplinks {
|
| 233 |
+
display: flex;
|
| 234 |
+
padding-bottom: 5px;
|
| 235 |
+
flex-wrap: wrap;
|
| 236 |
+
gap: 3px 17px;
|
| 237 |
+
font-weight: bold;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.change-list .toplinks a {
|
| 241 |
+
font-size: 0.8125rem;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.change-list .toplinks .date-back {
|
| 245 |
+
color: var(--body-quiet-color);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.change-list .toplinks .date-back:focus,
|
| 249 |
+
.change-list .toplinks .date-back:hover {
|
| 250 |
+
color: var(--link-hover-color);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* ACTIONS */
|
| 254 |
+
|
| 255 |
+
.filtered .actions {
|
| 256 |
+
border-right: none;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
#changelist table input {
|
| 260 |
+
margin: 0;
|
| 261 |
+
vertical-align: baseline;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
|
| 265 |
+
selector and the JS adding the class can be removed. */
|
| 266 |
+
#changelist tbody tr.selected {
|
| 267 |
+
background-color: var(--selected-row);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
#changelist tbody tr:has(.action-select:checked) {
|
| 271 |
+
background-color: var(--selected-row);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
@media (forced-colors: active) {
|
| 275 |
+
#changelist tbody tr.selected {
|
| 276 |
+
background-color: SelectedItem;
|
| 277 |
+
}
|
| 278 |
+
#changelist tbody tr:has(.action-select:checked) {
|
| 279 |
+
background-color: SelectedItem;
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
#changelist .actions {
|
| 284 |
+
padding: 10px;
|
| 285 |
+
background: var(--body-bg);
|
| 286 |
+
border-top: none;
|
| 287 |
+
border-bottom: none;
|
| 288 |
+
line-height: 1.5rem;
|
| 289 |
+
color: var(--body-quiet-color);
|
| 290 |
+
width: 100%;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
#changelist .actions span.all,
|
| 294 |
+
#changelist .actions span.action-counter,
|
| 295 |
+
#changelist .actions span.clear,
|
| 296 |
+
#changelist .actions span.question {
|
| 297 |
+
font-size: 0.8125rem;
|
| 298 |
+
margin: 0 0.5em;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
#changelist .actions:last-child {
|
| 302 |
+
border-bottom: none;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
#changelist .actions select {
|
| 306 |
+
vertical-align: top;
|
| 307 |
+
height: 1.5rem;
|
| 308 |
+
color: var(--body-fg);
|
| 309 |
+
border: 1px solid var(--border-color);
|
| 310 |
+
border-radius: 4px;
|
| 311 |
+
font-size: 0.875rem;
|
| 312 |
+
padding: 0 0 0 4px;
|
| 313 |
+
margin: 0;
|
| 314 |
+
margin-left: 10px;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
#changelist .actions select:focus {
|
| 318 |
+
border-color: var(--body-quiet-color);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
#changelist .actions label {
|
| 322 |
+
display: inline-block;
|
| 323 |
+
vertical-align: middle;
|
| 324 |
+
font-size: 0.8125rem;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
#changelist .actions .button {
|
| 328 |
+
font-size: 0.8125rem;
|
| 329 |
+
border: 1px solid var(--border-color);
|
| 330 |
+
border-radius: 4px;
|
| 331 |
+
background: var(--body-bg);
|
| 332 |
+
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
| 333 |
+
cursor: pointer;
|
| 334 |
+
height: 1.5rem;
|
| 335 |
+
line-height: 1;
|
| 336 |
+
padding: 4px 8px;
|
| 337 |
+
margin: 0;
|
| 338 |
+
color: var(--body-fg);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
| 342 |
+
border-color: var(--body-quiet-color);
|
| 343 |
+
}
|
static/admin/css/dark_mode.css
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@media (prefers-color-scheme: dark) {
|
| 2 |
+
:root {
|
| 3 |
+
--primary: #264b5d;
|
| 4 |
+
--primary-fg: #f7f7f7;
|
| 5 |
+
|
| 6 |
+
--body-fg: #eeeeee;
|
| 7 |
+
--body-bg: #121212;
|
| 8 |
+
--body-quiet-color: #d0d0d0;
|
| 9 |
+
--body-medium-color: #e0e0e0;
|
| 10 |
+
--body-loud-color: #ffffff;
|
| 11 |
+
|
| 12 |
+
--breadcrumbs-link-fg: #e0e0e0;
|
| 13 |
+
--breadcrumbs-bg: var(--primary);
|
| 14 |
+
|
| 15 |
+
--link-fg: #81d4fa;
|
| 16 |
+
--link-hover-color: #4ac1f7;
|
| 17 |
+
--link-selected-fg: #6f94c6;
|
| 18 |
+
|
| 19 |
+
--hairline-color: #272727;
|
| 20 |
+
--border-color: #353535;
|
| 21 |
+
|
| 22 |
+
--error-fg: #e35f5f;
|
| 23 |
+
--message-success-bg: #006b1b;
|
| 24 |
+
--message-warning-bg: #583305;
|
| 25 |
+
--message-error-bg: #570808;
|
| 26 |
+
|
| 27 |
+
--darkened-bg: #212121;
|
| 28 |
+
--selected-bg: #1b1b1b;
|
| 29 |
+
--selected-row: #00363a;
|
| 30 |
+
|
| 31 |
+
--close-button-bg: #333333;
|
| 32 |
+
--close-button-hover-bg: #666666;
|
| 33 |
+
|
| 34 |
+
color-scheme: dark;
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
html[data-theme="dark"] {
|
| 40 |
+
--primary: #264b5d;
|
| 41 |
+
--primary-fg: #f7f7f7;
|
| 42 |
+
|
| 43 |
+
--body-fg: #eeeeee;
|
| 44 |
+
--body-bg: #121212;
|
| 45 |
+
--body-quiet-color: #d0d0d0;
|
| 46 |
+
--body-medium-color: #e0e0e0;
|
| 47 |
+
--body-loud-color: #ffffff;
|
| 48 |
+
|
| 49 |
+
--breadcrumbs-link-fg: #e0e0e0;
|
| 50 |
+
--breadcrumbs-bg: var(--primary);
|
| 51 |
+
|
| 52 |
+
--link-fg: #81d4fa;
|
| 53 |
+
--link-hover-color: #4ac1f7;
|
| 54 |
+
--link-selected-fg: #6f94c6;
|
| 55 |
+
|
| 56 |
+
--hairline-color: #272727;
|
| 57 |
+
--border-color: #353535;
|
| 58 |
+
|
| 59 |
+
--error-fg: #e35f5f;
|
| 60 |
+
--message-success-bg: #006b1b;
|
| 61 |
+
--message-warning-bg: #583305;
|
| 62 |
+
--message-error-bg: #570808;
|
| 63 |
+
|
| 64 |
+
--darkened-bg: #212121;
|
| 65 |
+
--selected-bg: #1b1b1b;
|
| 66 |
+
--selected-row: #00363a;
|
| 67 |
+
|
| 68 |
+
--close-button-bg: #333333;
|
| 69 |
+
--close-button-hover-bg: #666666;
|
| 70 |
+
|
| 71 |
+
color-scheme: dark;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* THEME SWITCH */
|
| 75 |
+
.theme-toggle {
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
border: none;
|
| 78 |
+
padding: 0;
|
| 79 |
+
background: transparent;
|
| 80 |
+
vertical-align: middle;
|
| 81 |
+
margin-inline-start: 5px;
|
| 82 |
+
margin-top: -1px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.theme-toggle svg {
|
| 86 |
+
vertical-align: middle;
|
| 87 |
+
height: 1.5rem;
|
| 88 |
+
width: 1.5rem;
|
| 89 |
+
display: none;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/*
|
| 93 |
+
Fully hide screen reader text so we only show the one matching the current
|
| 94 |
+
theme.
|
| 95 |
+
*/
|
| 96 |
+
.theme-toggle .visually-hidden {
|
| 97 |
+
display: none;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
|
| 101 |
+
display: block;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
|
| 105 |
+
display: block;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
html[data-theme="light"] .theme-toggle .theme-label-when-light {
|
| 109 |
+
display: block;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* ICONS */
|
| 113 |
+
.theme-toggle svg.theme-icon-when-auto,
|
| 114 |
+
.theme-toggle svg.theme-icon-when-dark,
|
| 115 |
+
.theme-toggle svg.theme-icon-when-light {
|
| 116 |
+
fill: var(--header-link-color);
|
| 117 |
+
color: var(--header-bg);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
|
| 121 |
+
display: block;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
|
| 125 |
+
display: block;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
|
| 129 |
+
display: block;
|
| 130 |
+
}
|
static/admin/css/dashboard.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* DASHBOARD */
|
| 2 |
+
.dashboard td, .dashboard th {
|
| 3 |
+
word-break: break-word;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
.dashboard .module table th {
|
| 7 |
+
width: 100%;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.dashboard .module table td {
|
| 11 |
+
white-space: nowrap;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.dashboard .module table td a {
|
| 15 |
+
display: block;
|
| 16 |
+
padding-right: .6em;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* RECENT ACTIONS MODULE */
|
| 20 |
+
|
| 21 |
+
.module ul.actionlist {
|
| 22 |
+
margin-left: 0;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
ul.actionlist li {
|
| 26 |
+
list-style-type: none;
|
| 27 |
+
overflow: hidden;
|
| 28 |
+
text-overflow: ellipsis;
|
| 29 |
+
}
|
static/admin/css/forms.css
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('widgets.css');
|
| 2 |
+
|
| 3 |
+
/* FORM ROWS */
|
| 4 |
+
|
| 5 |
+
.form-row {
|
| 6 |
+
overflow: hidden;
|
| 7 |
+
padding: 10px;
|
| 8 |
+
font-size: 0.8125rem;
|
| 9 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.form-row img, .form-row input {
|
| 13 |
+
vertical-align: middle;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.form-row label input[type="checkbox"] {
|
| 17 |
+
margin-top: 0;
|
| 18 |
+
vertical-align: 0;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
form .form-row p {
|
| 22 |
+
padding-left: 0;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.flex-container {
|
| 26 |
+
display: flex;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.form-multiline {
|
| 30 |
+
flex-wrap: wrap;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.form-multiline > div {
|
| 34 |
+
padding-bottom: 10px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* FORM LABELS */
|
| 38 |
+
|
| 39 |
+
label {
|
| 40 |
+
font-weight: normal;
|
| 41 |
+
color: var(--body-quiet-color);
|
| 42 |
+
font-size: 0.8125rem;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.required label, label.required {
|
| 46 |
+
font-weight: bold;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* RADIO BUTTONS */
|
| 50 |
+
|
| 51 |
+
form div.radiolist div {
|
| 52 |
+
padding-right: 7px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
form div.radiolist.inline div {
|
| 56 |
+
display: inline-block;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
form div.radiolist label {
|
| 60 |
+
width: auto;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
form div.radiolist input[type="radio"] {
|
| 64 |
+
margin: -2px 4px 0 0;
|
| 65 |
+
padding: 0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
form ul.inline {
|
| 69 |
+
margin-left: 0;
|
| 70 |
+
padding: 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
form ul.inline li {
|
| 74 |
+
float: left;
|
| 75 |
+
padding-right: 7px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* FIELDSETS */
|
| 79 |
+
|
| 80 |
+
fieldset .fieldset-heading,
|
| 81 |
+
fieldset .inline-heading,
|
| 82 |
+
:not(.inline-related) .collapse summary {
|
| 83 |
+
border: 1px solid var(--header-bg);
|
| 84 |
+
margin: 0;
|
| 85 |
+
padding: 8px;
|
| 86 |
+
font-weight: 400;
|
| 87 |
+
font-size: 0.8125rem;
|
| 88 |
+
background: var(--header-bg);
|
| 89 |
+
color: var(--header-link-color);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* ALIGNED FIELDSETS */
|
| 93 |
+
|
| 94 |
+
.aligned label {
|
| 95 |
+
display: block;
|
| 96 |
+
padding: 4px 10px 0 0;
|
| 97 |
+
min-width: 160px;
|
| 98 |
+
width: 160px;
|
| 99 |
+
word-wrap: break-word;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.aligned label:not(.vCheckboxLabel):after {
|
| 103 |
+
content: '';
|
| 104 |
+
display: inline-block;
|
| 105 |
+
vertical-align: middle;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
|
| 109 |
+
padding: 6px 0;
|
| 110 |
+
margin-top: 0;
|
| 111 |
+
margin-bottom: 0;
|
| 112 |
+
margin-left: 0;
|
| 113 |
+
overflow-wrap: break-word;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.aligned ul label {
|
| 117 |
+
display: inline;
|
| 118 |
+
float: none;
|
| 119 |
+
width: auto;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.aligned .form-row input {
|
| 123 |
+
margin-bottom: 0;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
|
| 127 |
+
width: 350px;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
form .aligned ul {
|
| 131 |
+
margin-left: 160px;
|
| 132 |
+
padding-left: 10px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
form .aligned div.radiolist {
|
| 136 |
+
display: inline-block;
|
| 137 |
+
margin: 0;
|
| 138 |
+
padding: 0;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
form .aligned p.help,
|
| 142 |
+
form .aligned div.help {
|
| 143 |
+
margin-top: 0;
|
| 144 |
+
margin-left: 160px;
|
| 145 |
+
padding-left: 10px;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
form .aligned p.date div.help.timezonewarning,
|
| 149 |
+
form .aligned p.datetime div.help.timezonewarning,
|
| 150 |
+
form .aligned p.time div.help.timezonewarning {
|
| 151 |
+
margin-left: 0;
|
| 152 |
+
padding-left: 0;
|
| 153 |
+
font-weight: normal;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
form .aligned p.help:last-child,
|
| 157 |
+
form .aligned div.help:last-child {
|
| 158 |
+
margin-bottom: 0;
|
| 159 |
+
padding-bottom: 0;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
form .aligned input + p.help,
|
| 163 |
+
form .aligned textarea + p.help,
|
| 164 |
+
form .aligned select + p.help,
|
| 165 |
+
form .aligned input + div.help,
|
| 166 |
+
form .aligned textarea + div.help,
|
| 167 |
+
form .aligned select + div.help {
|
| 168 |
+
margin-left: 160px;
|
| 169 |
+
padding-left: 10px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
form .aligned select option:checked {
|
| 173 |
+
background-color: var(--selected-row);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
form .aligned ul li {
|
| 177 |
+
list-style: none;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
form .aligned table p {
|
| 181 |
+
margin-left: 0;
|
| 182 |
+
padding-left: 0;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.aligned .vCheckboxLabel {
|
| 186 |
+
padding: 1px 0 0 5px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.aligned .vCheckboxLabel + p.help,
|
| 190 |
+
.aligned .vCheckboxLabel + div.help {
|
| 191 |
+
margin-top: -4px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
|
| 195 |
+
width: 610px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
fieldset .fieldBox {
|
| 199 |
+
margin-right: 20px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* WIDE FIELDSETS */
|
| 203 |
+
|
| 204 |
+
.wide label {
|
| 205 |
+
width: 200px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
form .wide p.help,
|
| 209 |
+
form .wide ul.errorlist,
|
| 210 |
+
form .wide div.help {
|
| 211 |
+
padding-left: 50px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
form div.help ul {
|
| 215 |
+
padding-left: 0;
|
| 216 |
+
margin-left: 0;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
|
| 220 |
+
width: 450px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/* COLLAPSIBLE FIELDSETS */
|
| 224 |
+
|
| 225 |
+
.collapse summary .fieldset-heading,
|
| 226 |
+
.collapse summary .inline-heading {
|
| 227 |
+
background: transparent;
|
| 228 |
+
border: none;
|
| 229 |
+
color: currentColor;
|
| 230 |
+
display: inline;
|
| 231 |
+
margin: 0;
|
| 232 |
+
padding: 0;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* MONOSPACE TEXTAREAS */
|
| 236 |
+
|
| 237 |
+
fieldset.monospace textarea {
|
| 238 |
+
font-family: var(--font-family-monospace);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* SUBMIT ROW */
|
| 242 |
+
|
| 243 |
+
.submit-row {
|
| 244 |
+
padding: 12px 14px 12px;
|
| 245 |
+
margin: 0 0 20px;
|
| 246 |
+
background: var(--darkened-bg);
|
| 247 |
+
border: 1px solid var(--hairline-color);
|
| 248 |
+
border-radius: 4px;
|
| 249 |
+
overflow: hidden;
|
| 250 |
+
display: flex;
|
| 251 |
+
gap: 10px;
|
| 252 |
+
flex-wrap: wrap;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
body.popup .submit-row {
|
| 256 |
+
overflow: auto;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.submit-row input {
|
| 260 |
+
height: 2.1875rem;
|
| 261 |
+
line-height: 0.9375rem;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.submit-row input, .submit-row a {
|
| 265 |
+
margin: 0;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.submit-row input.default {
|
| 269 |
+
text-transform: uppercase;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.submit-row a.deletelink {
|
| 273 |
+
margin-left: auto;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.submit-row a.deletelink {
|
| 277 |
+
display: block;
|
| 278 |
+
background: var(--delete-button-bg);
|
| 279 |
+
border-radius: 4px;
|
| 280 |
+
padding: 0.625rem 0.9375rem;
|
| 281 |
+
height: 0.9375rem;
|
| 282 |
+
line-height: 0.9375rem;
|
| 283 |
+
color: var(--button-fg);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.submit-row a.closelink {
|
| 287 |
+
display: inline-block;
|
| 288 |
+
background: var(--close-button-bg);
|
| 289 |
+
border-radius: 4px;
|
| 290 |
+
padding: 10px 15px;
|
| 291 |
+
height: 0.9375rem;
|
| 292 |
+
line-height: 0.9375rem;
|
| 293 |
+
color: var(--button-fg);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.submit-row a.deletelink:focus,
|
| 297 |
+
.submit-row a.deletelink:hover,
|
| 298 |
+
.submit-row a.deletelink:active {
|
| 299 |
+
background: var(--delete-button-hover-bg);
|
| 300 |
+
text-decoration: none;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.submit-row a.closelink:focus,
|
| 304 |
+
.submit-row a.closelink:hover,
|
| 305 |
+
.submit-row a.closelink:active {
|
| 306 |
+
background: var(--close-button-hover-bg);
|
| 307 |
+
text-decoration: none;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* CUSTOM FORM FIELDS */
|
| 311 |
+
|
| 312 |
+
.vSelectMultipleField {
|
| 313 |
+
vertical-align: top;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.vCheckboxField {
|
| 317 |
+
border: none;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.vDateField, .vTimeField {
|
| 321 |
+
margin-right: 2px;
|
| 322 |
+
margin-bottom: 4px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.vDateField {
|
| 326 |
+
min-width: 6.85em;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.vTimeField {
|
| 330 |
+
min-width: 4.7em;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.vURLField {
|
| 334 |
+
width: 30em;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.vLargeTextField, .vXMLLargeTextField {
|
| 338 |
+
width: 48em;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.flatpages-flatpage #id_content {
|
| 342 |
+
height: 40.2em;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.module table .vPositiveSmallIntegerField {
|
| 346 |
+
width: 2.2em;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.vIntegerField {
|
| 350 |
+
width: 5em;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.vBigIntegerField {
|
| 354 |
+
width: 10em;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.vForeignKeyRawIdAdminField {
|
| 358 |
+
width: 5em;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.vTextField, .vUUIDField {
|
| 362 |
+
width: 20em;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/* INLINES */
|
| 366 |
+
|
| 367 |
+
.inline-group {
|
| 368 |
+
padding: 0;
|
| 369 |
+
margin: 0 0 30px;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.inline-group thead th {
|
| 373 |
+
padding: 8px 10px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.inline-group .aligned label {
|
| 377 |
+
width: 160px;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.inline-related {
|
| 381 |
+
position: relative;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.inline-related h4,
|
| 385 |
+
.inline-related:not(.tabular) .collapse summary {
|
| 386 |
+
margin: 0;
|
| 387 |
+
color: var(--body-medium-color);
|
| 388 |
+
padding: 5px;
|
| 389 |
+
font-size: 0.8125rem;
|
| 390 |
+
background: var(--darkened-bg);
|
| 391 |
+
border: 1px solid var(--hairline-color);
|
| 392 |
+
border-left-color: var(--darkened-bg);
|
| 393 |
+
border-right-color: var(--darkened-bg);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.inline-related h3 span.delete {
|
| 397 |
+
float: right;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.inline-related h3 span.delete label {
|
| 401 |
+
margin-left: 2px;
|
| 402 |
+
font-size: 0.6875rem;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.inline-related fieldset {
|
| 406 |
+
margin: 0;
|
| 407 |
+
background: var(--body-bg);
|
| 408 |
+
border: none;
|
| 409 |
+
width: 100%;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.inline-group .tabular fieldset.module {
|
| 413 |
+
border: none;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.inline-related.tabular fieldset.module table {
|
| 417 |
+
width: 100%;
|
| 418 |
+
overflow-x: scroll;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.last-related fieldset {
|
| 422 |
+
border: none;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.inline-group .tabular tr.has_original td {
|
| 426 |
+
padding-top: 2em;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.inline-group .tabular tr td.original {
|
| 430 |
+
padding: 2px 0 0 0;
|
| 431 |
+
width: 0;
|
| 432 |
+
_position: relative;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.inline-group .tabular th.original {
|
| 436 |
+
width: 0px;
|
| 437 |
+
padding: 0;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.inline-group .tabular td.original p {
|
| 441 |
+
position: absolute;
|
| 442 |
+
left: 0;
|
| 443 |
+
height: 1.1em;
|
| 444 |
+
padding: 2px 9px;
|
| 445 |
+
overflow: hidden;
|
| 446 |
+
font-size: 0.5625rem;
|
| 447 |
+
font-weight: bold;
|
| 448 |
+
color: var(--body-quiet-color);
|
| 449 |
+
_width: 700px;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.inline-group div.add-row,
|
| 453 |
+
.inline-group .tabular tr.add-row td {
|
| 454 |
+
color: var(--body-quiet-color);
|
| 455 |
+
background: var(--darkened-bg);
|
| 456 |
+
padding: 8px 10px;
|
| 457 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.inline-group .tabular tr.add-row td {
|
| 461 |
+
padding: 8px 10px;
|
| 462 |
+
border-bottom: 1px solid var(--hairline-color);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.inline-group div.add-row a,
|
| 466 |
+
.inline-group .tabular tr.add-row td a {
|
| 467 |
+
font-size: 0.75rem;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.empty-form {
|
| 471 |
+
display: none;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/* RELATED FIELD ADD ONE / LOOKUP */
|
| 475 |
+
|
| 476 |
+
.related-lookup {
|
| 477 |
+
margin-left: 5px;
|
| 478 |
+
display: inline-block;
|
| 479 |
+
vertical-align: middle;
|
| 480 |
+
background-repeat: no-repeat;
|
| 481 |
+
background-size: 14px;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
.related-lookup {
|
| 485 |
+
width: 1rem;
|
| 486 |
+
height: 1rem;
|
| 487 |
+
background-image: url(../img/search.svg);
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
form .related-widget-wrapper ul {
|
| 491 |
+
display: inline-block;
|
| 492 |
+
margin-left: 0;
|
| 493 |
+
padding-left: 0;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.clearable-file-input input {
|
| 497 |
+
margin-top: 0;
|
| 498 |
+
}
|
static/admin/css/login.css
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* LOGIN FORM */
|
| 2 |
+
|
| 3 |
+
.login {
|
| 4 |
+
background: var(--darkened-bg);
|
| 5 |
+
height: auto;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.login #header {
|
| 9 |
+
height: auto;
|
| 10 |
+
padding: 15px 16px;
|
| 11 |
+
justify-content: center;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.login #header h1 {
|
| 15 |
+
font-size: 1.125rem;
|
| 16 |
+
margin: 0;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.login #header h1 a {
|
| 20 |
+
color: var(--header-link-color);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.login #content {
|
| 24 |
+
padding: 20px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.login #container {
|
| 28 |
+
background: var(--body-bg);
|
| 29 |
+
border: 1px solid var(--hairline-color);
|
| 30 |
+
border-radius: 4px;
|
| 31 |
+
overflow: hidden;
|
| 32 |
+
width: 28em;
|
| 33 |
+
min-width: 300px;
|
| 34 |
+
margin: 100px auto;
|
| 35 |
+
height: auto;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.login .form-row {
|
| 39 |
+
padding: 4px 0;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.login .form-row label {
|
| 43 |
+
display: block;
|
| 44 |
+
line-height: 2em;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.login .form-row #id_username, .login .form-row #id_password {
|
| 48 |
+
padding: 8px;
|
| 49 |
+
width: 100%;
|
| 50 |
+
box-sizing: border-box;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.login .submit-row {
|
| 54 |
+
padding: 1em 0 0 0;
|
| 55 |
+
margin: 0;
|
| 56 |
+
text-align: center;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.login .password-reset-link {
|
| 60 |
+
text-align: center;
|
| 61 |
+
}
|
static/admin/css/nav_sidebar.css
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.sticky {
|
| 2 |
+
position: sticky;
|
| 3 |
+
top: 0;
|
| 4 |
+
max-height: 100vh;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
.toggle-nav-sidebar {
|
| 8 |
+
z-index: 20;
|
| 9 |
+
left: 0;
|
| 10 |
+
display: flex;
|
| 11 |
+
align-items: center;
|
| 12 |
+
justify-content: center;
|
| 13 |
+
flex: 0 0 23px;
|
| 14 |
+
width: 23px;
|
| 15 |
+
border: 0;
|
| 16 |
+
border-right: 1px solid var(--hairline-color);
|
| 17 |
+
background-color: var(--body-bg);
|
| 18 |
+
cursor: pointer;
|
| 19 |
+
font-size: 1.25rem;
|
| 20 |
+
color: var(--link-fg);
|
| 21 |
+
padding: 0;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
[dir="rtl"] .toggle-nav-sidebar {
|
| 25 |
+
border-left: 1px solid var(--hairline-color);
|
| 26 |
+
border-right: 0;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.toggle-nav-sidebar:hover,
|
| 30 |
+
.toggle-nav-sidebar:focus {
|
| 31 |
+
background-color: var(--darkened-bg);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
#nav-sidebar {
|
| 35 |
+
z-index: 15;
|
| 36 |
+
flex: 0 0 275px;
|
| 37 |
+
left: -276px;
|
| 38 |
+
margin-left: -276px;
|
| 39 |
+
border-top: 1px solid transparent;
|
| 40 |
+
border-right: 1px solid var(--hairline-color);
|
| 41 |
+
background-color: var(--body-bg);
|
| 42 |
+
overflow: auto;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
[dir="rtl"] #nav-sidebar {
|
| 46 |
+
border-left: 1px solid var(--hairline-color);
|
| 47 |
+
border-right: 0;
|
| 48 |
+
left: 0;
|
| 49 |
+
margin-left: 0;
|
| 50 |
+
right: -276px;
|
| 51 |
+
margin-right: -276px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.toggle-nav-sidebar::before {
|
| 55 |
+
content: '\00BB';
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.main.shifted .toggle-nav-sidebar::before {
|
| 59 |
+
content: '\00AB';
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.main > #nav-sidebar {
|
| 63 |
+
visibility: hidden;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.main.shifted > #nav-sidebar {
|
| 67 |
+
margin-left: 0;
|
| 68 |
+
visibility: visible;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
[dir="rtl"] .main.shifted > #nav-sidebar {
|
| 72 |
+
margin-right: 0;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
#nav-sidebar .module th {
|
| 76 |
+
width: 100%;
|
| 77 |
+
overflow-wrap: anywhere;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#nav-sidebar .module th,
|
| 81 |
+
#nav-sidebar .module caption {
|
| 82 |
+
padding-left: 16px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
#nav-sidebar .module td {
|
| 86 |
+
white-space: nowrap;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
[dir="rtl"] #nav-sidebar .module th,
|
| 90 |
+
[dir="rtl"] #nav-sidebar .module caption {
|
| 91 |
+
padding-left: 8px;
|
| 92 |
+
padding-right: 16px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#nav-sidebar .current-app .section:link,
|
| 96 |
+
#nav-sidebar .current-app .section:visited {
|
| 97 |
+
color: var(--header-color);
|
| 98 |
+
font-weight: bold;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
#nav-sidebar .current-model {
|
| 102 |
+
background: var(--selected-row);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
@media (forced-colors: active) {
|
| 106 |
+
#nav-sidebar .current-model {
|
| 107 |
+
background-color: SelectedItem;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.main > #nav-sidebar + .content {
|
| 112 |
+
max-width: calc(100% - 23px);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.main.shifted > #nav-sidebar + .content {
|
| 116 |
+
max-width: calc(100% - 299px);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
@media (max-width: 767px) {
|
| 120 |
+
#nav-sidebar, #toggle-nav-sidebar {
|
| 121 |
+
display: none;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.main > #nav-sidebar + .content,
|
| 125 |
+
.main.shifted > #nav-sidebar + .content {
|
| 126 |
+
max-width: 100%;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
#nav-filter {
|
| 131 |
+
width: 100%;
|
| 132 |
+
box-sizing: border-box;
|
| 133 |
+
padding: 2px 5px;
|
| 134 |
+
margin: 5px 0;
|
| 135 |
+
border: 1px solid var(--border-color);
|
| 136 |
+
background-color: var(--darkened-bg);
|
| 137 |
+
color: var(--body-fg);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
#nav-filter:focus {
|
| 141 |
+
border-color: var(--body-quiet-color);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
#nav-filter.no-results {
|
| 145 |
+
background: var(--message-error-bg);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#nav-sidebar table {
|
| 149 |
+
width: 100%;
|
| 150 |
+
}
|
static/admin/css/responsive.css
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Tablets */
|
| 2 |
+
|
| 3 |
+
input[type="submit"], button {
|
| 4 |
+
-webkit-appearance: none;
|
| 5 |
+
appearance: none;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@media (max-width: 1024px) {
|
| 9 |
+
/* Basic */
|
| 10 |
+
|
| 11 |
+
html {
|
| 12 |
+
-webkit-text-size-adjust: 100%;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
td, th {
|
| 16 |
+
padding: 10px;
|
| 17 |
+
font-size: 0.875rem;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.small {
|
| 21 |
+
font-size: 0.75rem;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* Layout */
|
| 25 |
+
|
| 26 |
+
#container {
|
| 27 |
+
min-width: 0;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
#content {
|
| 31 |
+
padding: 15px 20px 20px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
div.breadcrumbs {
|
| 35 |
+
padding: 10px 30px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Header */
|
| 39 |
+
|
| 40 |
+
#header {
|
| 41 |
+
flex-direction: column;
|
| 42 |
+
padding: 15px 30px;
|
| 43 |
+
justify-content: flex-start;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
#site-name {
|
| 47 |
+
margin: 0 0 8px;
|
| 48 |
+
line-height: 1.2;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
#user-tools {
|
| 52 |
+
margin: 0;
|
| 53 |
+
font-weight: 400;
|
| 54 |
+
line-height: 1.85;
|
| 55 |
+
text-align: left;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
#user-tools a {
|
| 59 |
+
display: inline-block;
|
| 60 |
+
line-height: 1.4;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* Dashboard */
|
| 64 |
+
|
| 65 |
+
.dashboard #content {
|
| 66 |
+
width: auto;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#content-related {
|
| 70 |
+
margin-right: -290px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.colSM #content-related {
|
| 74 |
+
margin-left: -290px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.colMS {
|
| 78 |
+
margin-right: 290px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.colSM {
|
| 82 |
+
margin-left: 290px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.dashboard .module table td a {
|
| 86 |
+
padding-right: 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
td .changelink, td .addlink {
|
| 90 |
+
font-size: 0.8125rem;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Changelist */
|
| 94 |
+
|
| 95 |
+
#toolbar {
|
| 96 |
+
border: none;
|
| 97 |
+
padding: 15px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
#changelist-search > div {
|
| 101 |
+
display: flex;
|
| 102 |
+
flex-wrap: nowrap;
|
| 103 |
+
max-width: 480px;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
#changelist-search label {
|
| 107 |
+
line-height: 1.375rem;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
#toolbar form #searchbar {
|
| 111 |
+
flex: 1 0 auto;
|
| 112 |
+
width: 0;
|
| 113 |
+
height: 1.375rem;
|
| 114 |
+
margin: 0 10px 0 6px;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
#toolbar form input[type=submit] {
|
| 118 |
+
flex: 0 1 auto;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
#changelist-search .quiet {
|
| 122 |
+
width: 0;
|
| 123 |
+
flex: 1 0 auto;
|
| 124 |
+
margin: 5px 0 0 25px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
#changelist .actions {
|
| 128 |
+
display: flex;
|
| 129 |
+
flex-wrap: wrap;
|
| 130 |
+
padding: 15px 0;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
#changelist .actions label {
|
| 134 |
+
display: flex;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
#changelist .actions select {
|
| 138 |
+
background: var(--body-bg);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#changelist .actions .button {
|
| 142 |
+
min-width: 48px;
|
| 143 |
+
margin: 0 10px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
#changelist .actions span.all,
|
| 147 |
+
#changelist .actions span.clear,
|
| 148 |
+
#changelist .actions span.question,
|
| 149 |
+
#changelist .actions span.action-counter {
|
| 150 |
+
font-size: 0.6875rem;
|
| 151 |
+
margin: 0 10px 0 0;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
#changelist-filter {
|
| 155 |
+
flex-basis: 200px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.change-list .filtered .results,
|
| 159 |
+
.change-list .filtered .paginator,
|
| 160 |
+
.filtered #toolbar,
|
| 161 |
+
.filtered .actions,
|
| 162 |
+
|
| 163 |
+
#changelist .paginator {
|
| 164 |
+
border-top-color: var(--hairline-color); /* XXX Is this used at all? */
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
#changelist .results + .paginator {
|
| 168 |
+
border-top: none;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/* Forms */
|
| 172 |
+
|
| 173 |
+
label {
|
| 174 |
+
font-size: 1rem;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/*
|
| 178 |
+
Minifiers remove the default (text) "type" attribute from "input" HTML
|
| 179 |
+
tags. Add input:not([type]) to make the CSS stylesheet work the same.
|
| 180 |
+
*/
|
| 181 |
+
.form-row input:not([type]),
|
| 182 |
+
.form-row input[type=text],
|
| 183 |
+
.form-row input[type=password],
|
| 184 |
+
.form-row input[type=email],
|
| 185 |
+
.form-row input[type=url],
|
| 186 |
+
.form-row input[type=tel],
|
| 187 |
+
.form-row input[type=number],
|
| 188 |
+
.form-row textarea,
|
| 189 |
+
.form-row select,
|
| 190 |
+
.form-row .vTextField {
|
| 191 |
+
box-sizing: border-box;
|
| 192 |
+
margin: 0;
|
| 193 |
+
padding: 6px 8px;
|
| 194 |
+
min-height: 2.25rem;
|
| 195 |
+
font-size: 1rem;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.form-row select {
|
| 199 |
+
height: 2.25rem;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.form-row select[multiple] {
|
| 203 |
+
height: auto;
|
| 204 |
+
min-height: 0;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
fieldset .fieldBox + .fieldBox {
|
| 208 |
+
margin-top: 10px;
|
| 209 |
+
padding-top: 10px;
|
| 210 |
+
border-top: 1px solid var(--hairline-color);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
textarea {
|
| 214 |
+
max-width: 100%;
|
| 215 |
+
max-height: 120px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.aligned label {
|
| 219 |
+
padding-top: 6px;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.aligned .related-lookup,
|
| 223 |
+
.aligned .datetimeshortcuts,
|
| 224 |
+
.aligned .related-lookup + strong {
|
| 225 |
+
align-self: center;
|
| 226 |
+
margin-left: 15px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
form .aligned div.radiolist {
|
| 230 |
+
margin-left: 2px;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.submit-row {
|
| 234 |
+
padding: 8px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.submit-row a.deletelink {
|
| 238 |
+
padding: 10px 7px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
| 242 |
+
padding: 7px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* Selector */
|
| 246 |
+
|
| 247 |
+
.selector {
|
| 248 |
+
display: flex;
|
| 249 |
+
width: 100%;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.selector .selector-filter {
|
| 253 |
+
display: flex;
|
| 254 |
+
align-items: center;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.selector .selector-filter input {
|
| 258 |
+
width: 100%;
|
| 259 |
+
min-height: 0;
|
| 260 |
+
flex: 1 1;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.selector-available, .selector-chosen {
|
| 264 |
+
width: auto;
|
| 265 |
+
flex: 1 1;
|
| 266 |
+
display: flex;
|
| 267 |
+
flex-direction: column;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.selector select {
|
| 271 |
+
width: 100%;
|
| 272 |
+
flex: 1 0 auto;
|
| 273 |
+
margin-bottom: 5px;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.selector-chooseall, .selector-clearall {
|
| 277 |
+
align-self: center;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.stacked {
|
| 281 |
+
flex-direction: column;
|
| 282 |
+
max-width: 480px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.stacked > * {
|
| 286 |
+
flex: 0 1 auto;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.stacked select {
|
| 290 |
+
margin-bottom: 0;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.stacked .selector-available, .stacked .selector-chosen {
|
| 294 |
+
width: auto;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.stacked ul.selector-chooser {
|
| 298 |
+
padding: 0 2px;
|
| 299 |
+
transform: none;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.stacked .selector-chooser li {
|
| 303 |
+
padding: 3px;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.help-tooltip, .selector .help-icon {
|
| 307 |
+
display: none;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.datetime input {
|
| 311 |
+
width: 50%;
|
| 312 |
+
max-width: 120px;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.datetime span {
|
| 316 |
+
font-size: 0.8125rem;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.datetime .timezonewarning {
|
| 320 |
+
display: block;
|
| 321 |
+
font-size: 0.6875rem;
|
| 322 |
+
color: var(--body-quiet-color);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.datetimeshortcuts {
|
| 326 |
+
color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
| 330 |
+
width: 75%;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.inline-group {
|
| 334 |
+
overflow: auto;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/* Messages */
|
| 338 |
+
|
| 339 |
+
ul.messagelist li {
|
| 340 |
+
padding-left: 55px;
|
| 341 |
+
background-position: 30px 12px;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
ul.messagelist li.error {
|
| 345 |
+
background-position: 30px 12px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
ul.messagelist li.warning {
|
| 349 |
+
background-position: 30px 14px;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* Login */
|
| 353 |
+
|
| 354 |
+
.login #header {
|
| 355 |
+
padding: 15px 20px;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.login #site-name {
|
| 359 |
+
margin: 0;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
/* GIS */
|
| 363 |
+
|
| 364 |
+
div.olMap {
|
| 365 |
+
max-width: calc(100vw - 30px);
|
| 366 |
+
max-height: 300px;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.olMap + .clear_features {
|
| 370 |
+
display: block;
|
| 371 |
+
margin-top: 10px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Docs */
|
| 375 |
+
|
| 376 |
+
.module table.xfull {
|
| 377 |
+
width: 100%;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
pre.literal-block {
|
| 381 |
+
overflow: auto;
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/* Mobile */
|
| 386 |
+
|
| 387 |
+
@media (max-width: 767px) {
|
| 388 |
+
/* Layout */
|
| 389 |
+
|
| 390 |
+
#header, #content {
|
| 391 |
+
padding: 15px;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
div.breadcrumbs {
|
| 395 |
+
padding: 10px 15px;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
/* Dashboard */
|
| 399 |
+
|
| 400 |
+
.colMS, .colSM {
|
| 401 |
+
margin: 0;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
#content-related, .colSM #content-related {
|
| 405 |
+
width: 100%;
|
| 406 |
+
margin: 0;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
#content-related .module {
|
| 410 |
+
margin-bottom: 0;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
#content-related .module h2 {
|
| 414 |
+
padding: 10px 15px;
|
| 415 |
+
font-size: 1rem;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
/* Changelist */
|
| 419 |
+
|
| 420 |
+
#changelist {
|
| 421 |
+
align-items: stretch;
|
| 422 |
+
flex-direction: column;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
#toolbar {
|
| 426 |
+
padding: 10px;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
#changelist-filter {
|
| 430 |
+
margin-left: 0;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
#changelist .actions label {
|
| 434 |
+
flex: 1 1;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
#changelist .actions select {
|
| 438 |
+
flex: 1 0;
|
| 439 |
+
width: 100%;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
#changelist .actions span {
|
| 443 |
+
flex: 1 0 100%;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
#changelist-filter {
|
| 447 |
+
position: static;
|
| 448 |
+
width: auto;
|
| 449 |
+
margin-top: 30px;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.object-tools {
|
| 453 |
+
float: none;
|
| 454 |
+
margin: 0 0 15px;
|
| 455 |
+
padding: 0;
|
| 456 |
+
overflow: hidden;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.object-tools li {
|
| 460 |
+
height: auto;
|
| 461 |
+
margin-left: 0;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.object-tools li + li {
|
| 465 |
+
margin-left: 15px;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
/* Forms */
|
| 469 |
+
|
| 470 |
+
.form-row {
|
| 471 |
+
padding: 15px 0;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.aligned .form-row,
|
| 475 |
+
.aligned .form-row > div {
|
| 476 |
+
max-width: 100vw;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
.aligned .form-row > div {
|
| 480 |
+
width: calc(100vw - 30px);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.flex-container {
|
| 484 |
+
flex-flow: column;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.flex-container.checkbox-row {
|
| 488 |
+
flex-flow: row;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
textarea {
|
| 492 |
+
max-width: none;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.vURLField {
|
| 496 |
+
width: auto;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
fieldset .fieldBox + .fieldBox {
|
| 500 |
+
margin-top: 15px;
|
| 501 |
+
padding-top: 15px;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.aligned label {
|
| 505 |
+
width: 100%;
|
| 506 |
+
min-width: auto;
|
| 507 |
+
padding: 0 0 10px;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.aligned label:after {
|
| 511 |
+
max-height: 0;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.aligned .form-row input,
|
| 515 |
+
.aligned .form-row select,
|
| 516 |
+
.aligned .form-row textarea {
|
| 517 |
+
flex: 1 1 auto;
|
| 518 |
+
max-width: 100%;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.aligned .checkbox-row input {
|
| 522 |
+
flex: 0 1 auto;
|
| 523 |
+
margin: 0;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.aligned .vCheckboxLabel {
|
| 527 |
+
flex: 1 0;
|
| 528 |
+
padding: 1px 0 0 5px;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.aligned label + p,
|
| 532 |
+
.aligned label + div.help,
|
| 533 |
+
.aligned label + div.readonly {
|
| 534 |
+
padding: 0;
|
| 535 |
+
margin-left: 0;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.aligned p.file-upload {
|
| 539 |
+
font-size: 0.8125rem;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
span.clearable-file-input {
|
| 543 |
+
margin-left: 15px;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
span.clearable-file-input label {
|
| 547 |
+
font-size: 0.8125rem;
|
| 548 |
+
padding-bottom: 0;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.aligned .timezonewarning {
|
| 552 |
+
flex: 1 0 100%;
|
| 553 |
+
margin-top: 5px;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
form .aligned .form-row div.help {
|
| 557 |
+
width: 100%;
|
| 558 |
+
margin: 5px 0 0;
|
| 559 |
+
padding: 0;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
form .aligned ul,
|
| 563 |
+
form .aligned ul.errorlist {
|
| 564 |
+
margin-left: 0;
|
| 565 |
+
padding-left: 0;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
form .aligned div.radiolist {
|
| 569 |
+
margin-top: 5px;
|
| 570 |
+
margin-right: 15px;
|
| 571 |
+
margin-bottom: -3px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
form .aligned div.radiolist:not(.inline) div + div {
|
| 575 |
+
margin-top: 5px;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
/* Related widget */
|
| 579 |
+
|
| 580 |
+
.related-widget-wrapper {
|
| 581 |
+
width: 100%;
|
| 582 |
+
display: flex;
|
| 583 |
+
align-items: flex-start;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.related-widget-wrapper .selector {
|
| 587 |
+
order: 1;
|
| 588 |
+
flex: 1 0 auto;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
.related-widget-wrapper > a {
|
| 592 |
+
order: 2;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.related-widget-wrapper .radiolist ~ a {
|
| 596 |
+
align-self: flex-end;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.related-widget-wrapper > select ~ a {
|
| 600 |
+
align-self: center;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
/* Selector */
|
| 604 |
+
|
| 605 |
+
.selector {
|
| 606 |
+
flex-direction: column;
|
| 607 |
+
gap: 10px 0;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.selector-available, .selector-chosen {
|
| 611 |
+
flex: 1 1 auto;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.selector select {
|
| 615 |
+
max-height: 96px;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.selector ul.selector-chooser {
|
| 619 |
+
display: flex;
|
| 620 |
+
width: 60px;
|
| 621 |
+
height: 30px;
|
| 622 |
+
padding: 0 2px;
|
| 623 |
+
transform: none;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.selector ul.selector-chooser li {
|
| 627 |
+
float: left;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.selector-remove {
|
| 631 |
+
background-position: 0 0;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
| 635 |
+
background-position: 0 -24px;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.selector-add {
|
| 639 |
+
background-position: 0 -48px;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
| 643 |
+
background-position: 0 -72px;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
/* Inlines */
|
| 647 |
+
|
| 648 |
+
.inline-group[data-inline-type="stacked"] .inline-related {
|
| 649 |
+
border: 1px solid var(--hairline-color);
|
| 650 |
+
border-radius: 4px;
|
| 651 |
+
margin-top: 15px;
|
| 652 |
+
overflow: auto;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.inline-group[data-inline-type="stacked"] .inline-related > * {
|
| 656 |
+
box-sizing: border-box;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.inline-group[data-inline-type="stacked"] .inline-related .module {
|
| 660 |
+
padding: 0 10px;
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
|
| 664 |
+
border-top: 1px solid var(--hairline-color);
|
| 665 |
+
border-bottom: none;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
|
| 669 |
+
border-top: none;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.inline-group[data-inline-type="stacked"] .inline-related h3 {
|
| 673 |
+
padding: 10px;
|
| 674 |
+
border-top-width: 0;
|
| 675 |
+
border-bottom-width: 2px;
|
| 676 |
+
display: flex;
|
| 677 |
+
flex-wrap: wrap;
|
| 678 |
+
align-items: center;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
|
| 682 |
+
margin-right: auto;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
|
| 686 |
+
float: none;
|
| 687 |
+
flex: 1 1 100%;
|
| 688 |
+
margin-top: 5px;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
|
| 692 |
+
width: 100%;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.inline-group[data-inline-type="stacked"] .aligned label {
|
| 696 |
+
width: 100%;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
.inline-group[data-inline-type="stacked"] div.add-row {
|
| 700 |
+
margin-top: 15px;
|
| 701 |
+
border: 1px solid var(--hairline-color);
|
| 702 |
+
border-radius: 4px;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.inline-group div.add-row,
|
| 706 |
+
.inline-group .tabular tr.add-row td {
|
| 707 |
+
padding: 0;
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
.inline-group div.add-row a,
|
| 711 |
+
.inline-group .tabular tr.add-row td a {
|
| 712 |
+
display: block;
|
| 713 |
+
padding: 8px 10px 8px 26px;
|
| 714 |
+
background-position: 8px 9px;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
/* Submit row */
|
| 718 |
+
|
| 719 |
+
.submit-row {
|
| 720 |
+
padding: 10px;
|
| 721 |
+
margin: 0 0 15px;
|
| 722 |
+
flex-direction: column;
|
| 723 |
+
gap: 8px;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.submit-row input, .submit-row input.default, .submit-row a {
|
| 727 |
+
text-align: center;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.submit-row a.closelink {
|
| 731 |
+
padding: 10px 0;
|
| 732 |
+
text-align: center;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.submit-row a.deletelink {
|
| 736 |
+
margin: 0;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
/* Messages */
|
| 740 |
+
|
| 741 |
+
ul.messagelist li {
|
| 742 |
+
padding-left: 40px;
|
| 743 |
+
background-position: 15px 12px;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
ul.messagelist li.error {
|
| 747 |
+
background-position: 15px 12px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
ul.messagelist li.warning {
|
| 751 |
+
background-position: 15px 14px;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
/* Paginator */
|
| 755 |
+
|
| 756 |
+
.paginator .this-page, .paginator a:link, .paginator a:visited {
|
| 757 |
+
padding: 4px 10px;
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
/* Login */
|
| 761 |
+
|
| 762 |
+
body.login {
|
| 763 |
+
padding: 0 15px;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.login #container {
|
| 767 |
+
width: auto;
|
| 768 |
+
max-width: 480px;
|
| 769 |
+
margin: 50px auto;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.login #header,
|
| 773 |
+
.login #content {
|
| 774 |
+
padding: 15px;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.login #content-main {
|
| 778 |
+
float: none;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
.login .form-row {
|
| 782 |
+
padding: 0;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
.login .form-row + .form-row {
|
| 786 |
+
margin-top: 15px;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.login .form-row label {
|
| 790 |
+
margin: 0 0 5px;
|
| 791 |
+
line-height: 1.2;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.login .submit-row {
|
| 795 |
+
padding: 15px 0 0;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.login br {
|
| 799 |
+
display: none;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.login .submit-row input {
|
| 803 |
+
margin: 0;
|
| 804 |
+
text-transform: uppercase;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.errornote {
|
| 808 |
+
margin: 0 0 20px;
|
| 809 |
+
padding: 8px 12px;
|
| 810 |
+
font-size: 0.8125rem;
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
/* Calendar and clock */
|
| 814 |
+
|
| 815 |
+
.calendarbox, .clockbox {
|
| 816 |
+
position: fixed !important;
|
| 817 |
+
top: 50% !important;
|
| 818 |
+
left: 50% !important;
|
| 819 |
+
transform: translate(-50%, -50%);
|
| 820 |
+
margin: 0;
|
| 821 |
+
border: none;
|
| 822 |
+
overflow: visible;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
.calendarbox:before, .clockbox:before {
|
| 826 |
+
content: '';
|
| 827 |
+
position: fixed;
|
| 828 |
+
top: 50%;
|
| 829 |
+
left: 50%;
|
| 830 |
+
width: 100vw;
|
| 831 |
+
height: 100vh;
|
| 832 |
+
background: rgba(0, 0, 0, 0.75);
|
| 833 |
+
transform: translate(-50%, -50%);
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.calendarbox > *, .clockbox > * {
|
| 837 |
+
position: relative;
|
| 838 |
+
z-index: 1;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
.calendarbox > div:first-child {
|
| 842 |
+
z-index: 2;
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
.calendarbox .calendar, .clockbox h2 {
|
| 846 |
+
border-radius: 4px 4px 0 0;
|
| 847 |
+
overflow: hidden;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
|
| 851 |
+
border-radius: 0 0 4px 4px;
|
| 852 |
+
overflow: hidden;
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.calendar-shortcuts {
|
| 856 |
+
padding: 10px 0;
|
| 857 |
+
font-size: 0.75rem;
|
| 858 |
+
line-height: 0.75rem;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.calendar-shortcuts a {
|
| 862 |
+
margin: 0 4px;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
.timelist a {
|
| 866 |
+
background: var(--body-bg);
|
| 867 |
+
padding: 4px;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
.calendar-cancel {
|
| 871 |
+
padding: 8px 10px;
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
.clockbox h2 {
|
| 875 |
+
padding: 8px 15px;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.calendar caption {
|
| 879 |
+
padding: 10px;
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
| 883 |
+
z-index: 1;
|
| 884 |
+
top: 10px;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
/* History */
|
| 888 |
+
|
| 889 |
+
table#change-history tbody th, table#change-history tbody td {
|
| 890 |
+
font-size: 0.8125rem;
|
| 891 |
+
word-break: break-word;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
table#change-history tbody th {
|
| 895 |
+
width: auto;
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
/* Docs */
|
| 899 |
+
|
| 900 |
+
table.model tbody th, table.model tbody td {
|
| 901 |
+
font-size: 0.8125rem;
|
| 902 |
+
word-break: break-word;
|
| 903 |
+
}
|
| 904 |
+
}
|
static/admin/css/responsive_rtl.css
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* TABLETS */
|
| 2 |
+
|
| 3 |
+
@media (max-width: 1024px) {
|
| 4 |
+
[dir="rtl"] .colMS {
|
| 5 |
+
margin-right: 0;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
[dir="rtl"] #user-tools {
|
| 9 |
+
text-align: right;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
[dir="rtl"] #changelist .actions label {
|
| 13 |
+
padding-left: 10px;
|
| 14 |
+
padding-right: 0;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
[dir="rtl"] #changelist .actions select {
|
| 18 |
+
margin-left: 0;
|
| 19 |
+
margin-right: 15px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
[dir="rtl"] .change-list .filtered .results,
|
| 23 |
+
[dir="rtl"] .change-list .filtered .paginator,
|
| 24 |
+
[dir="rtl"] .filtered #toolbar,
|
| 25 |
+
[dir="rtl"] .filtered div.xfull,
|
| 26 |
+
[dir="rtl"] .filtered .actions,
|
| 27 |
+
[dir="rtl"] #changelist-filter {
|
| 28 |
+
margin-left: 0;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
[dir="rtl"] .inline-group div.add-row a,
|
| 32 |
+
[dir="rtl"] .inline-group .tabular tr.add-row td a {
|
| 33 |
+
padding: 8px 26px 8px 10px;
|
| 34 |
+
background-position: calc(100% - 8px) 9px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
[dir="rtl"] .object-tools li {
|
| 38 |
+
float: right;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
[dir="rtl"] .object-tools li + li {
|
| 42 |
+
margin-left: 0;
|
| 43 |
+
margin-right: 15px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
[dir="rtl"] .dashboard .module table td a {
|
| 47 |
+
padding-left: 0;
|
| 48 |
+
padding-right: 16px;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* MOBILE */
|
| 53 |
+
|
| 54 |
+
@media (max-width: 767px) {
|
| 55 |
+
[dir="rtl"] .aligned .related-lookup,
|
| 56 |
+
[dir="rtl"] .aligned .datetimeshortcuts {
|
| 57 |
+
margin-left: 0;
|
| 58 |
+
margin-right: 15px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
[dir="rtl"] .aligned ul,
|
| 62 |
+
[dir="rtl"] form .aligned ul.errorlist {
|
| 63 |
+
margin-right: 0;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
[dir="rtl"] #changelist-filter {
|
| 67 |
+
margin-left: 0;
|
| 68 |
+
margin-right: 0;
|
| 69 |
+
}
|
| 70 |
+
[dir="rtl"] .aligned .vCheckboxLabel {
|
| 71 |
+
padding: 1px 5px 0 0;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
[dir="rtl"] .selector-remove {
|
| 75 |
+
background-position: 0 0;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
[dir="rtl"] :enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
| 79 |
+
background-position: 0 -24px;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
[dir="rtl"] .selector-add {
|
| 83 |
+
background-position: 0 -48px;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
[dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover {
|
| 87 |
+
background-position: 0 -72px;
|
| 88 |
+
}
|
| 89 |
+
}
|
static/admin/css/rtl.css
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* GLOBAL */
|
| 2 |
+
|
| 3 |
+
th {
|
| 4 |
+
text-align: right;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
.module h2, .module caption {
|
| 8 |
+
text-align: right;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.module ul, .module ol {
|
| 12 |
+
margin-left: 0;
|
| 13 |
+
margin-right: 1.5em;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.viewlink, .addlink, .changelink, .hidelink {
|
| 17 |
+
padding-left: 0;
|
| 18 |
+
padding-right: 16px;
|
| 19 |
+
background-position: 100% 1px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.deletelink {
|
| 23 |
+
padding-left: 0;
|
| 24 |
+
padding-right: 16px;
|
| 25 |
+
background-position: 100% 1px;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.object-tools {
|
| 29 |
+
float: left;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
thead th:first-child,
|
| 33 |
+
tfoot td:first-child {
|
| 34 |
+
border-left: none;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* LAYOUT */
|
| 38 |
+
|
| 39 |
+
#user-tools {
|
| 40 |
+
right: auto;
|
| 41 |
+
left: 0;
|
| 42 |
+
text-align: left;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
div.breadcrumbs {
|
| 46 |
+
text-align: right;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#content-main {
|
| 50 |
+
float: right;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
#content-related {
|
| 54 |
+
float: left;
|
| 55 |
+
margin-left: -300px;
|
| 56 |
+
margin-right: auto;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.colMS {
|
| 60 |
+
margin-left: 300px;
|
| 61 |
+
margin-right: 0;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* SORTABLE TABLES */
|
| 65 |
+
|
| 66 |
+
table thead th.sorted .sortoptions {
|
| 67 |
+
float: left;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
thead th.sorted .text {
|
| 71 |
+
padding-right: 0;
|
| 72 |
+
padding-left: 42px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* dashboard styles */
|
| 76 |
+
|
| 77 |
+
.dashboard .module table td a {
|
| 78 |
+
padding-left: .6em;
|
| 79 |
+
padding-right: 16px;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* changelists styles */
|
| 83 |
+
|
| 84 |
+
.change-list .filtered table {
|
| 85 |
+
border-left: none;
|
| 86 |
+
border-right: 0px none;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
#changelist-filter {
|
| 90 |
+
border-left: none;
|
| 91 |
+
border-right: none;
|
| 92 |
+
margin-left: 0;
|
| 93 |
+
margin-right: 30px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
#changelist-filter li.selected {
|
| 97 |
+
border-left: none;
|
| 98 |
+
padding-left: 10px;
|
| 99 |
+
margin-left: 0;
|
| 100 |
+
border-right: 5px solid var(--hairline-color);
|
| 101 |
+
padding-right: 10px;
|
| 102 |
+
margin-right: -15px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
|
| 106 |
+
border-right: none;
|
| 107 |
+
border-left: none;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.paginator .end {
|
| 111 |
+
margin-left: 6px;
|
| 112 |
+
margin-right: 0;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.paginator input {
|
| 116 |
+
margin-left: 0;
|
| 117 |
+
margin-right: auto;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* FORMS */
|
| 121 |
+
|
| 122 |
+
.aligned label {
|
| 123 |
+
padding: 0 0 3px 1em;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.submit-row a.deletelink {
|
| 127 |
+
margin-left: 0;
|
| 128 |
+
margin-right: auto;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.vDateField, .vTimeField {
|
| 132 |
+
margin-left: 2px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.aligned .form-row input {
|
| 136 |
+
margin-left: 5px;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
form .aligned ul {
|
| 140 |
+
margin-right: 163px;
|
| 141 |
+
padding-right: 10px;
|
| 142 |
+
margin-left: 0;
|
| 143 |
+
padding-left: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
form ul.inline li {
|
| 147 |
+
float: right;
|
| 148 |
+
padding-right: 0;
|
| 149 |
+
padding-left: 7px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
form .aligned p.help,
|
| 153 |
+
form .aligned div.help {
|
| 154 |
+
margin-left: 0;
|
| 155 |
+
margin-right: 160px;
|
| 156 |
+
padding-right: 10px;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
form div.help ul,
|
| 160 |
+
form .aligned .checkbox-row + .help,
|
| 161 |
+
form .aligned p.date div.help.timezonewarning,
|
| 162 |
+
form .aligned p.datetime div.help.timezonewarning,
|
| 163 |
+
form .aligned p.time div.help.timezonewarning {
|
| 164 |
+
margin-right: 0;
|
| 165 |
+
padding-right: 0;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
form .wide p.help,
|
| 169 |
+
form .wide ul.errorlist,
|
| 170 |
+
form .wide div.help {
|
| 171 |
+
padding-left: 0;
|
| 172 |
+
padding-right: 50px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.submit-row {
|
| 176 |
+
text-align: right;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
fieldset .fieldBox {
|
| 180 |
+
margin-left: 20px;
|
| 181 |
+
margin-right: 0;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.errorlist li {
|
| 185 |
+
background-position: 100% 12px;
|
| 186 |
+
padding: 0;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.errornote {
|
| 190 |
+
background-position: 100% 12px;
|
| 191 |
+
padding: 10px 12px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/* WIDGETS */
|
| 195 |
+
|
| 196 |
+
.calendarnav-previous {
|
| 197 |
+
top: 0;
|
| 198 |
+
left: auto;
|
| 199 |
+
right: 10px;
|
| 200 |
+
background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.calendarnav-next {
|
| 204 |
+
top: 0;
|
| 205 |
+
right: auto;
|
| 206 |
+
left: 10px;
|
| 207 |
+
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.calendar caption, .calendarbox h2 {
|
| 211 |
+
text-align: center;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.selector {
|
| 215 |
+
float: right;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.selector .selector-filter {
|
| 219 |
+
text-align: right;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.selector-add {
|
| 223 |
+
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
| 224 |
+
background-size: 24px auto;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
| 228 |
+
background-position: 0 -120px;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.selector-remove {
|
| 232 |
+
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
|
| 233 |
+
background-size: 24px auto;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
| 237 |
+
background-position: 0 -168px;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.selector-chooseall {
|
| 241 |
+
background: url(../img/selector-icons.svg) right -128px no-repeat;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
|
| 245 |
+
background-position: 100% -144px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.selector-clearall {
|
| 249 |
+
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
|
| 253 |
+
background-position: 0 -176px;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.inline-deletelink {
|
| 257 |
+
float: left;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
form .form-row p.datetime {
|
| 261 |
+
overflow: hidden;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.related-widget-wrapper {
|
| 265 |
+
float: right;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* MISC */
|
| 269 |
+
|
| 270 |
+
.inline-related h2, .inline-group h2 {
|
| 271 |
+
text-align: right
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.inline-related h3 span.delete {
|
| 275 |
+
padding-right: 20px;
|
| 276 |
+
padding-left: inherit;
|
| 277 |
+
left: 10px;
|
| 278 |
+
right: inherit;
|
| 279 |
+
float:left;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.inline-related h3 span.delete label {
|
| 283 |
+
margin-left: inherit;
|
| 284 |
+
margin-right: 2px;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.inline-group .tabular td.original p {
|
| 288 |
+
right: 0;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.selector .selector-chooser {
|
| 292 |
+
margin: 0;
|
| 293 |
+
}
|
static/admin/css/unusable_password_field.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Hide warnings fields if usable password is selected */
|
| 2 |
+
form:has(#id_usable_password input[value="true"]:checked) .messagelist {
|
| 3 |
+
display: none;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
/* Hide password fields if unusable password is selected */
|
| 7 |
+
form:has(#id_usable_password input[value="false"]:checked) .field-password1,
|
| 8 |
+
form:has(#id_usable_password input[value="false"]:checked) .field-password2 {
|
| 9 |
+
display: none;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* Select appropriate submit button */
|
| 13 |
+
form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password {
|
| 14 |
+
display: none;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password {
|
| 18 |
+
display: none;
|
| 19 |
+
}
|
static/admin/css/vendor/select2/LICENSE-SELECT2.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in
|
| 13 |
+
all copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 21 |
+
THE SOFTWARE.
|
static/admin/css/vendor/select2/select2.css
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.select2-container {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
display: inline-block;
|
| 4 |
+
margin: 0;
|
| 5 |
+
position: relative;
|
| 6 |
+
vertical-align: middle; }
|
| 7 |
+
.select2-container .select2-selection--single {
|
| 8 |
+
box-sizing: border-box;
|
| 9 |
+
cursor: pointer;
|
| 10 |
+
display: block;
|
| 11 |
+
height: 28px;
|
| 12 |
+
user-select: none;
|
| 13 |
+
-webkit-user-select: none; }
|
| 14 |
+
.select2-container .select2-selection--single .select2-selection__rendered {
|
| 15 |
+
display: block;
|
| 16 |
+
padding-left: 8px;
|
| 17 |
+
padding-right: 20px;
|
| 18 |
+
overflow: hidden;
|
| 19 |
+
text-overflow: ellipsis;
|
| 20 |
+
white-space: nowrap; }
|
| 21 |
+
.select2-container .select2-selection--single .select2-selection__clear {
|
| 22 |
+
position: relative; }
|
| 23 |
+
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
| 24 |
+
padding-right: 8px;
|
| 25 |
+
padding-left: 20px; }
|
| 26 |
+
.select2-container .select2-selection--multiple {
|
| 27 |
+
box-sizing: border-box;
|
| 28 |
+
cursor: pointer;
|
| 29 |
+
display: block;
|
| 30 |
+
min-height: 32px;
|
| 31 |
+
user-select: none;
|
| 32 |
+
-webkit-user-select: none; }
|
| 33 |
+
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
| 34 |
+
display: inline-block;
|
| 35 |
+
overflow: hidden;
|
| 36 |
+
padding-left: 8px;
|
| 37 |
+
text-overflow: ellipsis;
|
| 38 |
+
white-space: nowrap; }
|
| 39 |
+
.select2-container .select2-search--inline {
|
| 40 |
+
float: left; }
|
| 41 |
+
.select2-container .select2-search--inline .select2-search__field {
|
| 42 |
+
box-sizing: border-box;
|
| 43 |
+
border: none;
|
| 44 |
+
font-size: 100%;
|
| 45 |
+
margin-top: 5px;
|
| 46 |
+
padding: 0; }
|
| 47 |
+
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
| 48 |
+
-webkit-appearance: none; }
|
| 49 |
+
|
| 50 |
+
.select2-dropdown {
|
| 51 |
+
background-color: white;
|
| 52 |
+
border: 1px solid #aaa;
|
| 53 |
+
border-radius: 4px;
|
| 54 |
+
box-sizing: border-box;
|
| 55 |
+
display: block;
|
| 56 |
+
position: absolute;
|
| 57 |
+
left: -100000px;
|
| 58 |
+
width: 100%;
|
| 59 |
+
z-index: 1051; }
|
| 60 |
+
|
| 61 |
+
.select2-results {
|
| 62 |
+
display: block; }
|
| 63 |
+
|
| 64 |
+
.select2-results__options {
|
| 65 |
+
list-style: none;
|
| 66 |
+
margin: 0;
|
| 67 |
+
padding: 0; }
|
| 68 |
+
|
| 69 |
+
.select2-results__option {
|
| 70 |
+
padding: 6px;
|
| 71 |
+
user-select: none;
|
| 72 |
+
-webkit-user-select: none; }
|
| 73 |
+
.select2-results__option[aria-selected] {
|
| 74 |
+
cursor: pointer; }
|
| 75 |
+
|
| 76 |
+
.select2-container--open .select2-dropdown {
|
| 77 |
+
left: 0; }
|
| 78 |
+
|
| 79 |
+
.select2-container--open .select2-dropdown--above {
|
| 80 |
+
border-bottom: none;
|
| 81 |
+
border-bottom-left-radius: 0;
|
| 82 |
+
border-bottom-right-radius: 0; }
|
| 83 |
+
|
| 84 |
+
.select2-container--open .select2-dropdown--below {
|
| 85 |
+
border-top: none;
|
| 86 |
+
border-top-left-radius: 0;
|
| 87 |
+
border-top-right-radius: 0; }
|
| 88 |
+
|
| 89 |
+
.select2-search--dropdown {
|
| 90 |
+
display: block;
|
| 91 |
+
padding: 4px; }
|
| 92 |
+
.select2-search--dropdown .select2-search__field {
|
| 93 |
+
padding: 4px;
|
| 94 |
+
width: 100%;
|
| 95 |
+
box-sizing: border-box; }
|
| 96 |
+
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
| 97 |
+
-webkit-appearance: none; }
|
| 98 |
+
.select2-search--dropdown.select2-search--hide {
|
| 99 |
+
display: none; }
|
| 100 |
+
|
| 101 |
+
.select2-close-mask {
|
| 102 |
+
border: 0;
|
| 103 |
+
margin: 0;
|
| 104 |
+
padding: 0;
|
| 105 |
+
display: block;
|
| 106 |
+
position: fixed;
|
| 107 |
+
left: 0;
|
| 108 |
+
top: 0;
|
| 109 |
+
min-height: 100%;
|
| 110 |
+
min-width: 100%;
|
| 111 |
+
height: auto;
|
| 112 |
+
width: auto;
|
| 113 |
+
opacity: 0;
|
| 114 |
+
z-index: 99;
|
| 115 |
+
background-color: #fff;
|
| 116 |
+
filter: alpha(opacity=0); }
|
| 117 |
+
|
| 118 |
+
.select2-hidden-accessible {
|
| 119 |
+
border: 0 !important;
|
| 120 |
+
clip: rect(0 0 0 0) !important;
|
| 121 |
+
-webkit-clip-path: inset(50%) !important;
|
| 122 |
+
clip-path: inset(50%) !important;
|
| 123 |
+
height: 1px !important;
|
| 124 |
+
overflow: hidden !important;
|
| 125 |
+
padding: 0 !important;
|
| 126 |
+
position: absolute !important;
|
| 127 |
+
width: 1px !important;
|
| 128 |
+
white-space: nowrap !important; }
|
| 129 |
+
|
| 130 |
+
.select2-container--default .select2-selection--single {
|
| 131 |
+
background-color: #fff;
|
| 132 |
+
border: 1px solid #aaa;
|
| 133 |
+
border-radius: 4px; }
|
| 134 |
+
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
| 135 |
+
color: #444;
|
| 136 |
+
line-height: 28px; }
|
| 137 |
+
.select2-container--default .select2-selection--single .select2-selection__clear {
|
| 138 |
+
cursor: pointer;
|
| 139 |
+
float: right;
|
| 140 |
+
font-weight: bold; }
|
| 141 |
+
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
| 142 |
+
color: #999; }
|
| 143 |
+
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
| 144 |
+
height: 26px;
|
| 145 |
+
position: absolute;
|
| 146 |
+
top: 1px;
|
| 147 |
+
right: 1px;
|
| 148 |
+
width: 20px; }
|
| 149 |
+
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
| 150 |
+
border-color: #888 transparent transparent transparent;
|
| 151 |
+
border-style: solid;
|
| 152 |
+
border-width: 5px 4px 0 4px;
|
| 153 |
+
height: 0;
|
| 154 |
+
left: 50%;
|
| 155 |
+
margin-left: -4px;
|
| 156 |
+
margin-top: -2px;
|
| 157 |
+
position: absolute;
|
| 158 |
+
top: 50%;
|
| 159 |
+
width: 0; }
|
| 160 |
+
|
| 161 |
+
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
| 162 |
+
float: left; }
|
| 163 |
+
|
| 164 |
+
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
| 165 |
+
left: 1px;
|
| 166 |
+
right: auto; }
|
| 167 |
+
|
| 168 |
+
.select2-container--default.select2-container--disabled .select2-selection--single {
|
| 169 |
+
background-color: #eee;
|
| 170 |
+
cursor: default; }
|
| 171 |
+
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
| 172 |
+
display: none; }
|
| 173 |
+
|
| 174 |
+
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
| 175 |
+
border-color: transparent transparent #888 transparent;
|
| 176 |
+
border-width: 0 4px 5px 4px; }
|
| 177 |
+
|
| 178 |
+
.select2-container--default .select2-selection--multiple {
|
| 179 |
+
background-color: white;
|
| 180 |
+
border: 1px solid #aaa;
|
| 181 |
+
border-radius: 4px;
|
| 182 |
+
cursor: text; }
|
| 183 |
+
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
| 184 |
+
box-sizing: border-box;
|
| 185 |
+
list-style: none;
|
| 186 |
+
margin: 0;
|
| 187 |
+
padding: 0 5px;
|
| 188 |
+
width: 100%; }
|
| 189 |
+
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
| 190 |
+
list-style: none; }
|
| 191 |
+
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
| 192 |
+
cursor: pointer;
|
| 193 |
+
float: right;
|
| 194 |
+
font-weight: bold;
|
| 195 |
+
margin-top: 5px;
|
| 196 |
+
margin-right: 10px;
|
| 197 |
+
padding: 1px; }
|
| 198 |
+
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
| 199 |
+
background-color: #e4e4e4;
|
| 200 |
+
border: 1px solid #aaa;
|
| 201 |
+
border-radius: 4px;
|
| 202 |
+
cursor: default;
|
| 203 |
+
float: left;
|
| 204 |
+
margin-right: 5px;
|
| 205 |
+
margin-top: 5px;
|
| 206 |
+
padding: 0 5px; }
|
| 207 |
+
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
| 208 |
+
color: #999;
|
| 209 |
+
cursor: pointer;
|
| 210 |
+
display: inline-block;
|
| 211 |
+
font-weight: bold;
|
| 212 |
+
margin-right: 2px; }
|
| 213 |
+
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
| 214 |
+
color: #333; }
|
| 215 |
+
|
| 216 |
+
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
| 217 |
+
float: right; }
|
| 218 |
+
|
| 219 |
+
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
| 220 |
+
margin-left: 5px;
|
| 221 |
+
margin-right: auto; }
|
| 222 |
+
|
| 223 |
+
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
| 224 |
+
margin-left: 2px;
|
| 225 |
+
margin-right: auto; }
|
| 226 |
+
|
| 227 |
+
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
| 228 |
+
border: solid black 1px;
|
| 229 |
+
outline: 0; }
|
| 230 |
+
|
| 231 |
+
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
| 232 |
+
background-color: #eee;
|
| 233 |
+
cursor: default; }
|
| 234 |
+
|
| 235 |
+
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
| 236 |
+
display: none; }
|
| 237 |
+
|
| 238 |
+
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
| 239 |
+
border-top-left-radius: 0;
|
| 240 |
+
border-top-right-radius: 0; }
|
| 241 |
+
|
| 242 |
+
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
| 243 |
+
border-bottom-left-radius: 0;
|
| 244 |
+
border-bottom-right-radius: 0; }
|
| 245 |
+
|
| 246 |
+
.select2-container--default .select2-search--dropdown .select2-search__field {
|
| 247 |
+
border: 1px solid #aaa; }
|
| 248 |
+
|
| 249 |
+
.select2-container--default .select2-search--inline .select2-search__field {
|
| 250 |
+
background: transparent;
|
| 251 |
+
border: none;
|
| 252 |
+
outline: 0;
|
| 253 |
+
box-shadow: none;
|
| 254 |
+
-webkit-appearance: textfield; }
|
| 255 |
+
|
| 256 |
+
.select2-container--default .select2-results > .select2-results__options {
|
| 257 |
+
max-height: 200px;
|
| 258 |
+
overflow-y: auto; }
|
| 259 |
+
|
| 260 |
+
.select2-container--default .select2-results__option[role=group] {
|
| 261 |
+
padding: 0; }
|
| 262 |
+
|
| 263 |
+
.select2-container--default .select2-results__option[aria-disabled=true] {
|
| 264 |
+
color: #999; }
|
| 265 |
+
|
| 266 |
+
.select2-container--default .select2-results__option[aria-selected=true] {
|
| 267 |
+
background-color: #ddd; }
|
| 268 |
+
|
| 269 |
+
.select2-container--default .select2-results__option .select2-results__option {
|
| 270 |
+
padding-left: 1em; }
|
| 271 |
+
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
| 272 |
+
padding-left: 0; }
|
| 273 |
+
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
| 274 |
+
margin-left: -1em;
|
| 275 |
+
padding-left: 2em; }
|
| 276 |
+
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 277 |
+
margin-left: -2em;
|
| 278 |
+
padding-left: 3em; }
|
| 279 |
+
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 280 |
+
margin-left: -3em;
|
| 281 |
+
padding-left: 4em; }
|
| 282 |
+
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 283 |
+
margin-left: -4em;
|
| 284 |
+
padding-left: 5em; }
|
| 285 |
+
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
| 286 |
+
margin-left: -5em;
|
| 287 |
+
padding-left: 6em; }
|
| 288 |
+
|
| 289 |
+
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
| 290 |
+
background-color: #5897fb;
|
| 291 |
+
color: white; }
|
| 292 |
+
|
| 293 |
+
.select2-container--default .select2-results__group {
|
| 294 |
+
cursor: default;
|
| 295 |
+
display: block;
|
| 296 |
+
padding: 6px; }
|
| 297 |
+
|
| 298 |
+
.select2-container--classic .select2-selection--single {
|
| 299 |
+
background-color: #f7f7f7;
|
| 300 |
+
border: 1px solid #aaa;
|
| 301 |
+
border-radius: 4px;
|
| 302 |
+
outline: 0;
|
| 303 |
+
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
| 304 |
+
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
| 305 |
+
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
| 306 |
+
background-repeat: repeat-x;
|
| 307 |
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
| 308 |
+
.select2-container--classic .select2-selection--single:focus {
|
| 309 |
+
border: 1px solid #5897fb; }
|
| 310 |
+
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
| 311 |
+
color: #444;
|
| 312 |
+
line-height: 28px; }
|
| 313 |
+
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
| 314 |
+
cursor: pointer;
|
| 315 |
+
float: right;
|
| 316 |
+
font-weight: bold;
|
| 317 |
+
margin-right: 10px; }
|
| 318 |
+
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
| 319 |
+
color: #999; }
|
| 320 |
+
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
| 321 |
+
background-color: #ddd;
|
| 322 |
+
border: none;
|
| 323 |
+
border-left: 1px solid #aaa;
|
| 324 |
+
border-top-right-radius: 4px;
|
| 325 |
+
border-bottom-right-radius: 4px;
|
| 326 |
+
height: 26px;
|
| 327 |
+
position: absolute;
|
| 328 |
+
top: 1px;
|
| 329 |
+
right: 1px;
|
| 330 |
+
width: 20px;
|
| 331 |
+
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
| 332 |
+
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
| 333 |
+
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
| 334 |
+
background-repeat: repeat-x;
|
| 335 |
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
| 336 |
+
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
|
| 337 |
+
border-color: #888 transparent transparent transparent;
|
| 338 |
+
border-style: solid;
|
| 339 |
+
border-width: 5px 4px 0 4px;
|
| 340 |
+
height: 0;
|
| 341 |
+
left: 50%;
|
| 342 |
+
margin-left: -4px;
|
| 343 |
+
margin-top: -2px;
|
| 344 |
+
position: absolute;
|
| 345 |
+
top: 50%;
|
| 346 |
+
width: 0; }
|
| 347 |
+
|
| 348 |
+
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
| 349 |
+
float: left; }
|
| 350 |
+
|
| 351 |
+
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
| 352 |
+
border: none;
|
| 353 |
+
border-right: 1px solid #aaa;
|
| 354 |
+
border-radius: 0;
|
| 355 |
+
border-top-left-radius: 4px;
|
| 356 |
+
border-bottom-left-radius: 4px;
|
| 357 |
+
left: 1px;
|
| 358 |
+
right: auto; }
|
| 359 |
+
|
| 360 |
+
.select2-container--classic.select2-container--open .select2-selection--single {
|
| 361 |
+
border: 1px solid #5897fb; }
|
| 362 |
+
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
| 363 |
+
background: transparent;
|
| 364 |
+
border: none; }
|
| 365 |
+
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
| 366 |
+
border-color: transparent transparent #888 transparent;
|
| 367 |
+
border-width: 0 4px 5px 4px; }
|
| 368 |
+
|
| 369 |
+
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
| 370 |
+
border-top: none;
|
| 371 |
+
border-top-left-radius: 0;
|
| 372 |
+
border-top-right-radius: 0;
|
| 373 |
+
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
| 374 |
+
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
| 375 |
+
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
| 376 |
+
background-repeat: repeat-x;
|
| 377 |
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
| 378 |
+
|
| 379 |
+
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
| 380 |
+
border-bottom: none;
|
| 381 |
+
border-bottom-left-radius: 0;
|
| 382 |
+
border-bottom-right-radius: 0;
|
| 383 |
+
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
| 384 |
+
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
| 385 |
+
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
| 386 |
+
background-repeat: repeat-x;
|
| 387 |
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
| 388 |
+
|
| 389 |
+
.select2-container--classic .select2-selection--multiple {
|
| 390 |
+
background-color: white;
|
| 391 |
+
border: 1px solid #aaa;
|
| 392 |
+
border-radius: 4px;
|
| 393 |
+
cursor: text;
|
| 394 |
+
outline: 0; }
|
| 395 |
+
.select2-container--classic .select2-selection--multiple:focus {
|
| 396 |
+
border: 1px solid #5897fb; }
|
| 397 |
+
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
| 398 |
+
list-style: none;
|
| 399 |
+
margin: 0;
|
| 400 |
+
padding: 0 5px; }
|
| 401 |
+
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
| 402 |
+
display: none; }
|
| 403 |
+
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
| 404 |
+
background-color: #e4e4e4;
|
| 405 |
+
border: 1px solid #aaa;
|
| 406 |
+
border-radius: 4px;
|
| 407 |
+
cursor: default;
|
| 408 |
+
float: left;
|
| 409 |
+
margin-right: 5px;
|
| 410 |
+
margin-top: 5px;
|
| 411 |
+
padding: 0 5px; }
|
| 412 |
+
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
| 413 |
+
color: #888;
|
| 414 |
+
cursor: pointer;
|
| 415 |
+
display: inline-block;
|
| 416 |
+
font-weight: bold;
|
| 417 |
+
margin-right: 2px; }
|
| 418 |
+
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
| 419 |
+
color: #555; }
|
| 420 |
+
|
| 421 |
+
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
| 422 |
+
float: right;
|
| 423 |
+
margin-left: 5px;
|
| 424 |
+
margin-right: auto; }
|
| 425 |
+
|
| 426 |
+
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
| 427 |
+
margin-left: 2px;
|
| 428 |
+
margin-right: auto; }
|
| 429 |
+
|
| 430 |
+
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
| 431 |
+
border: 1px solid #5897fb; }
|
| 432 |
+
|
| 433 |
+
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
| 434 |
+
border-top: none;
|
| 435 |
+
border-top-left-radius: 0;
|
| 436 |
+
border-top-right-radius: 0; }
|
| 437 |
+
|
| 438 |
+
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
| 439 |
+
border-bottom: none;
|
| 440 |
+
border-bottom-left-radius: 0;
|
| 441 |
+
border-bottom-right-radius: 0; }
|
| 442 |
+
|
| 443 |
+
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
| 444 |
+
border: 1px solid #aaa;
|
| 445 |
+
outline: 0; }
|
| 446 |
+
|
| 447 |
+
.select2-container--classic .select2-search--inline .select2-search__field {
|
| 448 |
+
outline: 0;
|
| 449 |
+
box-shadow: none; }
|
| 450 |
+
|
| 451 |
+
.select2-container--classic .select2-dropdown {
|
| 452 |
+
background-color: white;
|
| 453 |
+
border: 1px solid transparent; }
|
| 454 |
+
|
| 455 |
+
.select2-container--classic .select2-dropdown--above {
|
| 456 |
+
border-bottom: none; }
|
| 457 |
+
|
| 458 |
+
.select2-container--classic .select2-dropdown--below {
|
| 459 |
+
border-top: none; }
|
| 460 |
+
|
| 461 |
+
.select2-container--classic .select2-results > .select2-results__options {
|
| 462 |
+
max-height: 200px;
|
| 463 |
+
overflow-y: auto; }
|
| 464 |
+
|
| 465 |
+
.select2-container--classic .select2-results__option[role=group] {
|
| 466 |
+
padding: 0; }
|
| 467 |
+
|
| 468 |
+
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
| 469 |
+
color: grey; }
|
| 470 |
+
|
| 471 |
+
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
| 472 |
+
background-color: #3875d7;
|
| 473 |
+
color: white; }
|
| 474 |
+
|
| 475 |
+
.select2-container--classic .select2-results__group {
|
| 476 |
+
cursor: default;
|
| 477 |
+
display: block;
|
| 478 |
+
padding: 6px; }
|
| 479 |
+
|
| 480 |
+
.select2-container--classic.select2-container--open .select2-dropdown {
|
| 481 |
+
border-color: #5897fb; }
|
static/admin/css/vendor/select2/select2.min.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.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, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 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, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 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, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 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, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 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:#fff;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:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
|
static/admin/css/widgets.css
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* SELECTOR (FILTER INTERFACE) */
|
| 2 |
+
|
| 3 |
+
.selector {
|
| 4 |
+
display: flex;
|
| 5 |
+
flex: 1;
|
| 6 |
+
gap: 0 10px;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.selector select {
|
| 10 |
+
height: 17.2em;
|
| 11 |
+
flex: 1 0 auto;
|
| 12 |
+
overflow: scroll;
|
| 13 |
+
width: 100%;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.selector-available, .selector-chosen {
|
| 17 |
+
display: flex;
|
| 18 |
+
flex-direction: column;
|
| 19 |
+
flex: 1 1;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.selector-available-title, .selector-chosen-title {
|
| 23 |
+
border: 1px solid var(--border-color);
|
| 24 |
+
border-radius: 4px 4px 0 0;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.selector .helptext {
|
| 28 |
+
font-size: 0.6875rem;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.selector-chosen .list-footer-display {
|
| 32 |
+
border: 1px solid var(--border-color);
|
| 33 |
+
border-top: none;
|
| 34 |
+
border-radius: 0 0 4px 4px;
|
| 35 |
+
margin: 0 0 10px;
|
| 36 |
+
padding: 8px;
|
| 37 |
+
text-align: center;
|
| 38 |
+
background: var(--primary);
|
| 39 |
+
color: var(--header-link-color);
|
| 40 |
+
cursor: pointer;
|
| 41 |
+
}
|
| 42 |
+
.selector-chosen .list-footer-display__clear {
|
| 43 |
+
color: var(--breadcrumbs-fg);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.selector-chosen-title {
|
| 47 |
+
background: var(--secondary);
|
| 48 |
+
color: var(--header-link-color);
|
| 49 |
+
padding: 8px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.selector-chosen-title label {
|
| 53 |
+
color: var(--header-link-color);
|
| 54 |
+
width: 100%;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.selector-available-title {
|
| 58 |
+
background: var(--darkened-bg);
|
| 59 |
+
color: var(--body-quiet-color);
|
| 60 |
+
padding: 8px;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.selector-available-title label {
|
| 64 |
+
width: 100%;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.selector .selector-filter {
|
| 68 |
+
border: 1px solid var(--border-color);
|
| 69 |
+
border-width: 0 1px;
|
| 70 |
+
padding: 8px;
|
| 71 |
+
color: var(--body-quiet-color);
|
| 72 |
+
font-size: 0.625rem;
|
| 73 |
+
margin: 0;
|
| 74 |
+
text-align: left;
|
| 75 |
+
display: flex;
|
| 76 |
+
gap: 8px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.selector .selector-filter label,
|
| 80 |
+
.inline-group .aligned .selector .selector-filter label {
|
| 81 |
+
float: left;
|
| 82 |
+
margin: 7px 0 0;
|
| 83 |
+
width: 18px;
|
| 84 |
+
height: 18px;
|
| 85 |
+
padding: 0;
|
| 86 |
+
overflow: hidden;
|
| 87 |
+
line-height: 1;
|
| 88 |
+
min-width: auto;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.selector-filter input {
|
| 92 |
+
flex-grow: 1;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.selector ul.selector-chooser {
|
| 96 |
+
align-self: center;
|
| 97 |
+
width: 30px;
|
| 98 |
+
background-color: var(--selected-bg);
|
| 99 |
+
border-radius: 10px;
|
| 100 |
+
margin: 0;
|
| 101 |
+
padding: 0;
|
| 102 |
+
transform: translateY(-17px);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.selector-chooser li {
|
| 106 |
+
margin: 0;
|
| 107 |
+
padding: 3px;
|
| 108 |
+
list-style-type: none;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.selector select {
|
| 112 |
+
padding: 0 10px;
|
| 113 |
+
margin: 0 0 10px;
|
| 114 |
+
border-radius: 0 0 4px 4px;
|
| 115 |
+
}
|
| 116 |
+
.selector .selector-chosen--with-filtered select {
|
| 117 |
+
margin: 0;
|
| 118 |
+
border-radius: 0;
|
| 119 |
+
height: 14em;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display {
|
| 123 |
+
display: none;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.selector-add, .selector-remove {
|
| 127 |
+
width: 24px;
|
| 128 |
+
height: 24px;
|
| 129 |
+
display: block;
|
| 130 |
+
text-indent: -3000px;
|
| 131 |
+
overflow: hidden;
|
| 132 |
+
cursor: default;
|
| 133 |
+
opacity: 0.55;
|
| 134 |
+
border: none;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
:enabled.selector-add, :enabled.selector-remove {
|
| 138 |
+
opacity: 1;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
:enabled.selector-add:hover, :enabled.selector-remove:hover {
|
| 142 |
+
cursor: pointer;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.selector-add {
|
| 146 |
+
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
|
| 147 |
+
background-size: 24px auto;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
| 151 |
+
background-position: 0 -168px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.selector-remove {
|
| 155 |
+
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
| 156 |
+
background-size: 24px auto;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
| 160 |
+
background-position: 0 -120px;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.selector-chooseall, .selector-clearall {
|
| 164 |
+
display: inline-block;
|
| 165 |
+
height: 16px;
|
| 166 |
+
text-align: left;
|
| 167 |
+
margin: 0 auto;
|
| 168 |
+
overflow: hidden;
|
| 169 |
+
font-weight: bold;
|
| 170 |
+
line-height: 16px;
|
| 171 |
+
color: var(--body-quiet-color);
|
| 172 |
+
text-decoration: none;
|
| 173 |
+
opacity: 0.55;
|
| 174 |
+
border: none;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
:enabled.selector-chooseall:focus, :enabled.selector-clearall:focus,
|
| 178 |
+
:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
|
| 179 |
+
color: var(--link-fg);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
:enabled.selector-chooseall, :enabled.selector-clearall {
|
| 183 |
+
opacity: 1;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
|
| 187 |
+
cursor: pointer;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.selector-chooseall {
|
| 191 |
+
padding: 0 18px 0 0;
|
| 192 |
+
background: url(../img/selector-icons.svg) right -160px no-repeat;
|
| 193 |
+
cursor: default;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
|
| 197 |
+
background-position: 100% -176px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.selector-clearall {
|
| 201 |
+
padding: 0 0 0 18px;
|
| 202 |
+
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
|
| 203 |
+
cursor: default;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
|
| 207 |
+
background-position: 0 -144px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* STACKED SELECTORS */
|
| 211 |
+
|
| 212 |
+
.stacked {
|
| 213 |
+
float: left;
|
| 214 |
+
width: 490px;
|
| 215 |
+
display: block;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.stacked select {
|
| 219 |
+
width: 480px;
|
| 220 |
+
height: 10.1em;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.stacked .selector-available, .stacked .selector-chosen {
|
| 224 |
+
width: 480px;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.stacked .selector-available {
|
| 228 |
+
margin-bottom: 0;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.stacked .selector-available input {
|
| 232 |
+
width: 422px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.stacked ul.selector-chooser {
|
| 236 |
+
display: flex;
|
| 237 |
+
height: 30px;
|
| 238 |
+
width: 64px;
|
| 239 |
+
margin: 0 0 10px 40%;
|
| 240 |
+
background-color: #eee;
|
| 241 |
+
border-radius: 10px;
|
| 242 |
+
transform: none;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.stacked .selector-chooser li {
|
| 246 |
+
float: left;
|
| 247 |
+
padding: 3px 3px 3px 5px;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.stacked .selector-chooseall, .stacked .selector-clearall {
|
| 251 |
+
display: none;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.stacked .selector-add {
|
| 255 |
+
background: url(../img/selector-icons.svg) 0 -48px no-repeat;
|
| 256 |
+
background-size: 24px auto;
|
| 257 |
+
cursor: default;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.stacked :enabled.selector-add {
|
| 261 |
+
background-position: 0 -48px;
|
| 262 |
+
cursor: pointer;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.stacked :enabled.selector-add:focus, .stacked :enabled.selector-add:hover {
|
| 266 |
+
background-position: 0 -72px;
|
| 267 |
+
cursor: pointer;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.stacked .selector-remove {
|
| 271 |
+
background: url(../img/selector-icons.svg) 0 0 no-repeat;
|
| 272 |
+
background-size: 24px auto;
|
| 273 |
+
cursor: default;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.stacked :enabled.selector-remove {
|
| 277 |
+
background-position: 0 0px;
|
| 278 |
+
cursor: pointer;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.stacked :enabled.selector-remove:focus, .stacked :enabled.selector-remove:hover {
|
| 282 |
+
background-position: 0 -24px;
|
| 283 |
+
cursor: pointer;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.selector .help-icon {
|
| 287 |
+
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
|
| 288 |
+
display: inline-block;
|
| 289 |
+
vertical-align: middle;
|
| 290 |
+
margin: -2px 0 0 2px;
|
| 291 |
+
width: 13px;
|
| 292 |
+
height: 13px;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.selector .selector-chosen .help-icon {
|
| 296 |
+
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.selector .search-label-icon {
|
| 300 |
+
background: url(../img/search.svg) 0 0 no-repeat;
|
| 301 |
+
display: inline-block;
|
| 302 |
+
height: 1.125rem;
|
| 303 |
+
width: 1.125rem;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* DATE AND TIME */
|
| 307 |
+
|
| 308 |
+
p.datetime {
|
| 309 |
+
line-height: 20px;
|
| 310 |
+
margin: 0;
|
| 311 |
+
padding: 0;
|
| 312 |
+
color: var(--body-quiet-color);
|
| 313 |
+
font-weight: bold;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.datetime span {
|
| 317 |
+
white-space: nowrap;
|
| 318 |
+
font-weight: normal;
|
| 319 |
+
font-size: 0.6875rem;
|
| 320 |
+
color: var(--body-quiet-color);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
| 324 |
+
margin-left: 5px;
|
| 325 |
+
margin-bottom: 4px;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
table p.datetime {
|
| 329 |
+
font-size: 0.6875rem;
|
| 330 |
+
margin-left: 0;
|
| 331 |
+
padding-left: 0;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
|
| 335 |
+
position: relative;
|
| 336 |
+
display: inline-block;
|
| 337 |
+
vertical-align: middle;
|
| 338 |
+
height: 24px;
|
| 339 |
+
width: 24px;
|
| 340 |
+
overflow: hidden;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.datetimeshortcuts .clock-icon {
|
| 344 |
+
background: url(../img/icon-clock.svg) 0 0 no-repeat;
|
| 345 |
+
background-size: 24px auto;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.datetimeshortcuts a:focus .clock-icon,
|
| 349 |
+
.datetimeshortcuts a:hover .clock-icon {
|
| 350 |
+
background-position: 0 -24px;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.datetimeshortcuts .date-icon {
|
| 354 |
+
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
|
| 355 |
+
background-size: 24px auto;
|
| 356 |
+
top: -1px;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.datetimeshortcuts a:focus .date-icon,
|
| 360 |
+
.datetimeshortcuts a:hover .date-icon {
|
| 361 |
+
background-position: 0 -24px;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.timezonewarning {
|
| 365 |
+
font-size: 0.6875rem;
|
| 366 |
+
color: var(--body-quiet-color);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/* URL */
|
| 370 |
+
|
| 371 |
+
p.url {
|
| 372 |
+
line-height: 20px;
|
| 373 |
+
margin: 0;
|
| 374 |
+
padding: 0;
|
| 375 |
+
color: var(--body-quiet-color);
|
| 376 |
+
font-size: 0.6875rem;
|
| 377 |
+
font-weight: bold;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.url a {
|
| 381 |
+
font-weight: normal;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
/* FILE UPLOADS */
|
| 385 |
+
|
| 386 |
+
p.file-upload {
|
| 387 |
+
line-height: 20px;
|
| 388 |
+
margin: 0;
|
| 389 |
+
padding: 0;
|
| 390 |
+
color: var(--body-quiet-color);
|
| 391 |
+
font-size: 0.6875rem;
|
| 392 |
+
font-weight: bold;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.file-upload a {
|
| 396 |
+
font-weight: normal;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.file-upload .deletelink {
|
| 400 |
+
margin-left: 5px;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
span.clearable-file-input label {
|
| 404 |
+
color: var(--body-fg);
|
| 405 |
+
font-size: 0.6875rem;
|
| 406 |
+
display: inline;
|
| 407 |
+
float: none;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/* CALENDARS & CLOCKS */
|
| 411 |
+
|
| 412 |
+
.calendarbox, .clockbox {
|
| 413 |
+
margin: 5px auto;
|
| 414 |
+
font-size: 0.75rem;
|
| 415 |
+
width: 19em;
|
| 416 |
+
text-align: center;
|
| 417 |
+
background: var(--body-bg);
|
| 418 |
+
color: var(--body-fg);
|
| 419 |
+
border: 1px solid var(--hairline-color);
|
| 420 |
+
border-radius: 4px;
|
| 421 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
| 422 |
+
overflow: hidden;
|
| 423 |
+
position: relative;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.clockbox {
|
| 427 |
+
width: auto;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.calendar {
|
| 431 |
+
margin: 0;
|
| 432 |
+
padding: 0;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.calendar table {
|
| 436 |
+
margin: 0;
|
| 437 |
+
padding: 0;
|
| 438 |
+
border-collapse: collapse;
|
| 439 |
+
background: white;
|
| 440 |
+
width: 100%;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.calendar caption, .calendarbox h2 {
|
| 444 |
+
margin: 0;
|
| 445 |
+
text-align: center;
|
| 446 |
+
border-top: none;
|
| 447 |
+
font-weight: 700;
|
| 448 |
+
font-size: 0.75rem;
|
| 449 |
+
color: #333;
|
| 450 |
+
background: var(--accent);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.calendar th {
|
| 454 |
+
padding: 8px 5px;
|
| 455 |
+
background: var(--darkened-bg);
|
| 456 |
+
border-bottom: 1px solid var(--border-color);
|
| 457 |
+
font-weight: 400;
|
| 458 |
+
font-size: 0.75rem;
|
| 459 |
+
text-align: center;
|
| 460 |
+
color: var(--body-quiet-color);
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.calendar td {
|
| 464 |
+
font-weight: 400;
|
| 465 |
+
font-size: 0.75rem;
|
| 466 |
+
text-align: center;
|
| 467 |
+
padding: 0;
|
| 468 |
+
border-top: 1px solid var(--hairline-color);
|
| 469 |
+
border-bottom: none;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.calendar td.selected a {
|
| 473 |
+
background: var(--secondary);
|
| 474 |
+
color: var(--button-fg);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.calendar td.nonday {
|
| 478 |
+
background: var(--darkened-bg);
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.calendar td.today a {
|
| 482 |
+
font-weight: 700;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.calendar td a, .timelist a {
|
| 486 |
+
display: block;
|
| 487 |
+
font-weight: 400;
|
| 488 |
+
padding: 6px;
|
| 489 |
+
text-decoration: none;
|
| 490 |
+
color: var(--body-quiet-color);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
.calendar td a:focus, .timelist a:focus,
|
| 494 |
+
.calendar td a:hover, .timelist a:hover {
|
| 495 |
+
background: var(--primary);
|
| 496 |
+
color: white;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.calendar td a:active, .timelist a:active {
|
| 500 |
+
background: var(--header-bg);
|
| 501 |
+
color: white;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.calendarnav {
|
| 505 |
+
font-size: 0.625rem;
|
| 506 |
+
text-align: center;
|
| 507 |
+
color: #ccc;
|
| 508 |
+
margin: 0;
|
| 509 |
+
padding: 1px 3px;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.calendarnav a:link, #calendarnav a:visited,
|
| 513 |
+
#calendarnav a:focus, #calendarnav a:hover {
|
| 514 |
+
color: var(--body-quiet-color);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.calendar-shortcuts {
|
| 518 |
+
background: var(--body-bg);
|
| 519 |
+
color: var(--body-quiet-color);
|
| 520 |
+
font-size: 0.6875rem;
|
| 521 |
+
line-height: 0.6875rem;
|
| 522 |
+
border-top: 1px solid var(--hairline-color);
|
| 523 |
+
padding: 8px 0;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
| 527 |
+
display: block;
|
| 528 |
+
position: absolute;
|
| 529 |
+
top: 8px;
|
| 530 |
+
width: 15px;
|
| 531 |
+
height: 15px;
|
| 532 |
+
text-indent: -9999px;
|
| 533 |
+
padding: 0;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.calendarnav-previous {
|
| 537 |
+
left: 10px;
|
| 538 |
+
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.calendarnav-next {
|
| 542 |
+
right: 10px;
|
| 543 |
+
background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.calendar-cancel {
|
| 547 |
+
margin: 0;
|
| 548 |
+
padding: 4px 0;
|
| 549 |
+
font-size: 0.75rem;
|
| 550 |
+
background: var(--close-button-bg);
|
| 551 |
+
border-top: 1px solid var(--border-color);
|
| 552 |
+
color: var(--button-fg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.calendar-cancel:focus, .calendar-cancel:hover {
|
| 556 |
+
background: var(--close-button-hover-bg);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.calendar-cancel a {
|
| 560 |
+
color: var(--button-fg);
|
| 561 |
+
display: block;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
ul.timelist, .timelist li {
|
| 565 |
+
list-style-type: none;
|
| 566 |
+
margin: 0;
|
| 567 |
+
padding: 0;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.timelist a {
|
| 571 |
+
padding: 2px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
/* EDIT INLINE */
|
| 575 |
+
|
| 576 |
+
.inline-deletelink {
|
| 577 |
+
float: right;
|
| 578 |
+
text-indent: -9999px;
|
| 579 |
+
background: url(../img/inline-delete.svg) 0 0 no-repeat;
|
| 580 |
+
width: 1.5rem;
|
| 581 |
+
height: 1.5rem;
|
| 582 |
+
border: 0px none;
|
| 583 |
+
margin-bottom: .25rem;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.inline-deletelink:focus, .inline-deletelink:hover {
|
| 587 |
+
cursor: pointer;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
/* RELATED WIDGET WRAPPER */
|
| 591 |
+
.related-widget-wrapper {
|
| 592 |
+
display: flex;
|
| 593 |
+
gap: 0 10px;
|
| 594 |
+
flex-grow: 1;
|
| 595 |
+
flex-wrap: wrap;
|
| 596 |
+
margin-bottom: 5px;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.related-widget-wrapper-link {
|
| 600 |
+
opacity: .6;
|
| 601 |
+
filter: grayscale(1);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.related-widget-wrapper-link:link {
|
| 605 |
+
opacity: 1;
|
| 606 |
+
filter: grayscale(0);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
/* GIS MAPS */
|
| 610 |
+
.dj_map {
|
| 611 |
+
width: 600px;
|
| 612 |
+
height: 400px;
|
| 613 |
+
}
|
static/admin/img/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2014 Code Charm Ltd
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
| 6 |
+
this software and associated documentation files (the "Software"), to deal in
|
| 7 |
+
the Software without restriction, including without limitation the rights to
|
| 8 |
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
| 9 |
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
| 10 |
+
subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
| 17 |
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
| 18 |
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
| 19 |
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
| 20 |
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
static/admin/img/README.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
All icons are taken from Font Awesome (https://fontawesome.com/) project.
|
| 2 |
+
The Font Awesome font is licensed under the SIL OFL 1.1:
|
| 3 |
+
- https://scripts.sil.org/OFL
|
| 4 |
+
|
| 5 |
+
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
|
| 6 |
+
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
|
| 7 |
+
in current folder).
|
static/admin/img/calendar-icons.svg
ADDED
|
|
static/admin/img/gis/move_vertex_off.svg
ADDED
|
|
static/admin/img/gis/move_vertex_on.svg
ADDED
|
|
static/admin/img/icon-addlink.svg
ADDED
|
|
static/admin/img/icon-alert.svg
ADDED
|
|
static/admin/img/icon-calendar.svg
ADDED
|
|