Файлы прода
This commit is contained in:
parent
b9b9d1b391
commit
a5c110a35b
0
OneCprogsite/__init__.py
Normal file
0
OneCprogsite/__init__.py
Normal file
BIN
OneCprogsite/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
OneCprogsite/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
OneCprogsite/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/settings.cpython-310.pyc
Normal file
BIN
OneCprogsite/__pycache__/settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/settings.cpython-311.pyc
Normal file
BIN
OneCprogsite/__pycache__/settings.cpython-311.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/urls.cpython-310.pyc
Normal file
BIN
OneCprogsite/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/urls.cpython-311.pyc
Normal file
BIN
OneCprogsite/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/wsgi.cpython-310.pyc
Normal file
BIN
OneCprogsite/__pycache__/wsgi.cpython-310.pyc
Normal file
Binary file not shown.
BIN
OneCprogsite/__pycache__/wsgi.cpython-311.pyc
Normal file
BIN
OneCprogsite/__pycache__/wsgi.cpython-311.pyc
Normal file
Binary file not shown.
16
OneCprogsite/asgi.py
Normal file
16
OneCprogsite/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for OneCprogsite project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
212
OneCprogsite/settings.py
Normal file
212
OneCprogsite/settings.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
"""
|
||||||
|
Django settings for OneCprogsite project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.2.7.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
|
"""
|
||||||
|
import os.path
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm'
|
||||||
|
|
||||||
|
# Безопасность cookies для HTTPS
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
# Если используете другие cookies
|
||||||
|
LANGUAGE_COOKIE_SECURE = True
|
||||||
|
LANGUAGE_COOKIE_HTTPONLY = True
|
||||||
|
LANGUAGE_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
# Или разрешить конкретные домены (Django 4.0+)
|
||||||
|
X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru'
|
||||||
|
|
||||||
|
# ОБЯЗАТЕЛЬНО укажите ваши домены
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
'nikdizell.ru',
|
||||||
|
'www.nikdizell.ru',
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
'192.168.31.88' # Добавьте IP сервера
|
||||||
|
]
|
||||||
|
|
||||||
|
# Важно для работы за прокси
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
SECURE_SSL_REDIRECT = True
|
||||||
|
|
||||||
|
# Дополнительная безопасность
|
||||||
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_HSTS_SECONDS = 31536000
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
'https://nikdizell.ru',
|
||||||
|
'https://www.nikdizell.ru',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'programmer.apps.ProgrammerConfig',
|
||||||
|
'django_bootstrap5',
|
||||||
|
'django_extensions',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.sitemaps',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'programmer.middleware.PageViewMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'OneCprogsite.urls'
|
||||||
|
|
||||||
|
# Кастомный middleware для CSP
|
||||||
|
class CSPMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
response['Content-Security-Policy'] = "frame-ancestors 'self' https://metrika.yandex.ru https://metrika.yandex.by https://metrica.yandex.com https://metrica.yandex.com.tr https://*.webvisor.com"
|
||||||
|
return response
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'programmer.context_processors.menu_processor',
|
||||||
|
'programmer.context_processors.contact_info',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'OneCprogsite.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': 'App',
|
||||||
|
'USER': 'postgres',
|
||||||
|
'PASSWORD': 'NikDi94Zell',
|
||||||
|
'HOST': 'postgres',
|
||||||
|
'PORT': 5432,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'ru'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Moscow'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
STATICFILES_DIRS = []
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
# Настройки email
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru
|
||||||
|
# EMAIL_PORT = 587
|
||||||
|
# EMAIL_USE_TLS = True
|
||||||
|
# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru')
|
||||||
|
# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc')
|
||||||
|
# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
||||||
|
# SERVER_EMAIL = EMAIL_HOST_USER
|
||||||
|
|
||||||
|
EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru
|
||||||
|
EMAIL_PORT = 587
|
||||||
|
EMAIL_USE_TLS = True
|
||||||
|
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com')
|
||||||
|
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj')
|
||||||
|
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
||||||
|
SERVER_EMAIL = EMAIL_HOST_USER
|
||||||
|
|
||||||
|
# Email для уведомлений (можно указать несколько через запятую)
|
||||||
|
# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',')
|
||||||
|
ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',')
|
||||||
|
|
||||||
|
|
||||||
39
OneCprogsite/urls.py
Normal file
39
OneCprogsite/urls.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for OneCprogsite project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from OneCprogsite import settings
|
||||||
|
from programmer.views import *
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('programmer.urls')),
|
||||||
|
# path('', index, name='home'),
|
||||||
|
# path('about/', about, name='about'),
|
||||||
|
# path('solution/', solution, name='solution'),
|
||||||
|
# path('ability/', ability, name='ability'),
|
||||||
|
# path('recall/', recall, name='recall'),
|
||||||
|
# path('post/<int:post_id>', show_post, name='post'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
handler404 = pageNotFound
|
||||||
16
OneCprogsite/wsgi.py
Normal file
16
OneCprogsite/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for OneCprogsite project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
0
db.sqlite3
Normal file
0
db.sqlite3
Normal file
33
dockerfile
Normal file
33
dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Устанавливаем системные зависимости
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
python3-dev \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем весь проект
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# ОБНОВЛЯЕМ PIP до последней версии
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
|
# Создаем базовый requirements.txt если его нет
|
||||||
|
RUN if [ ! -f requirements.txt ]; then \
|
||||||
|
echo "Django>=4.2,<5.0" > requirements.txt; \
|
||||||
|
echo "gunicorn==21.2.0" >> requirements.txt; \
|
||||||
|
echo "whitenoise==6.5.0" >> requirements.txt; \
|
||||||
|
echo "Pillow==10.0.0" >> requirements.txt; \
|
||||||
|
echo "psycopg2-binary==2.9.7" >> requirements.txt; \
|
||||||
|
echo "django-bootstrap5==23.3" >> requirements.txt; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
22
manage.py
Normal file
22
manage.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
media/home_image/2023/11/25/9.png
Normal file
BIN
media/home_image/2023/11/25/9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
media/home_image/2025/10/12/photomode_05082024_232531.png
Normal file
BIN
media/home_image/2025/10/12/photomode_05082024_232531.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 943 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 911 KiB |
BIN
media/photos/2023/11/24/Табличный_документ.jpg
Normal file
BIN
media/photos/2023/11/24/Табличный_документ.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
BIN
media/scan/2025/11/14/Отзыв_о_работе_РОВЕН_Сердюк_Н..jpg
Normal file
BIN
media/scan/2025/11/14/Отзыв_о_работе_РОВЕН_Сердюк_Н..jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 850 KiB |
0
programmer/__init__.py
Normal file
0
programmer/__init__.py
Normal file
BIN
programmer/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/admin.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/admin.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/apps.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/apps.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/context_processors.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/context_processors.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/context_processors.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/context_processors.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/forms.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/forms.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/forms.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/middleware.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/middleware.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/models.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/models.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/sitemaps.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/sitemaps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/urls.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/urls.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/views.cpython-310.pyc
Normal file
BIN
programmer/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/__pycache__/views.cpython-311.pyc
Normal file
BIN
programmer/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
169
programmer/admin.py
Normal file
169
programmer/admin.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.urls import path
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
|
class ProgrammerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
class RecallAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'description', 'implementation')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CallbackRequest)
|
||||||
|
class CallbackAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge')
|
||||||
|
list_display_links = ('name', 'phone')
|
||||||
|
list_editable = ('is_processed', 'is_read')
|
||||||
|
list_filter = ('time_create', 'is_processed', 'is_read')
|
||||||
|
search_fields = ('name', 'phone', 'email')
|
||||||
|
readonly_fields = ('time_create',)
|
||||||
|
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed']
|
||||||
|
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
|
||||||
|
|
||||||
|
def resend_notification(self, request, queryset):
|
||||||
|
from .utils.email_notifications import send_callback_notification
|
||||||
|
count = 0
|
||||||
|
for callback in queryset:
|
||||||
|
success = send_callback_notification(callback)
|
||||||
|
if success:
|
||||||
|
count += 1
|
||||||
|
self.message_user(request, f'Уведомления отправлены для {count} заявок')
|
||||||
|
|
||||||
|
resend_notification.short_description = "Переотправить email уведомления"
|
||||||
|
|
||||||
|
def new_badge(self, obj):
|
||||||
|
if not obj.is_read:
|
||||||
|
return format_html('<span style="color: red; font-weight: bold;">🆕 НОВАЯ</span>')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
new_badge.short_description = 'Статус'
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
# Показываем количество непрочитанных в заголовке
|
||||||
|
unread_count = CallbackRequest.objects.filter(is_read=False).count()
|
||||||
|
if unread_count > 0:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'У вас {unread_count} непрочитанных заявок!',
|
||||||
|
messages.WARNING
|
||||||
|
)
|
||||||
|
return super().get_queryset(request)
|
||||||
|
|
||||||
|
def mark_as_read(self, request, queryset):
|
||||||
|
updated = queryset.update(is_read=True)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
|
||||||
|
|
||||||
|
mark_as_read.short_description = "Отметить как прочитанные"
|
||||||
|
|
||||||
|
def mark_as_unread(self, request, queryset):
|
||||||
|
updated = queryset.update(is_read=False)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как непрочитанные')
|
||||||
|
|
||||||
|
mark_as_unread.short_description = "Отметить как непрочитанные"
|
||||||
|
|
||||||
|
def mark_as_processed(self, request, queryset):
|
||||||
|
updated = queryset.update(is_processed=True)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как обработанные')
|
||||||
|
|
||||||
|
mark_as_processed.short_description = "Отметить как обработанные"
|
||||||
|
|
||||||
|
# Добавляем кастомное представление для статистики
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def callback_stats(self, request):
|
||||||
|
today = timezone.now().date()
|
||||||
|
week_ago = today - timezone.timedelta(days=7)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total': CallbackRequest.objects.count(),
|
||||||
|
'today': CallbackRequest.objects.filter(time_create__date=today).count(),
|
||||||
|
'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(),
|
||||||
|
'unread': CallbackRequest.objects.filter(is_read=False).count(),
|
||||||
|
'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**self.admin_site.each_context(request),
|
||||||
|
'title': 'Статистика заявок',
|
||||||
|
'stats': stats,
|
||||||
|
}
|
||||||
|
return render(request, 'admin/callback_stats.html', context)
|
||||||
|
|
||||||
|
class ProgrammerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class RecallAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'description', 'implementation')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class HomeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
@admin.register(PageView)
|
||||||
|
class PageViewAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['url', 'timestamp', 'ip_address']
|
||||||
|
list_filter = ['timestamp', 'url']
|
||||||
|
search_fields = ['url', 'ip_address']
|
||||||
|
date_hierarchy = 'timestamp'
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).order_by('-timestamp')
|
||||||
|
|
||||||
|
admin.site.register(Competence, ProgrammerAdmin)
|
||||||
|
admin.site.register(Recall, RecallAdmin)
|
||||||
|
admin.site.register(Solution, SolutionAdmin)
|
||||||
|
admin.site.register(Home, HomeAdmin)
|
||||||
7
programmer/apps.py
Normal file
7
programmer/apps.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProgrammerConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'programmer'
|
||||||
|
verbose_name = 'Программисты'
|
||||||
12
programmer/context_processors.py
Normal file
12
programmer/context_processors.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from .views import menu
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def menu_processor(request):
|
||||||
|
return {'menu': menu}
|
||||||
|
|
||||||
|
def contact_info(request):
|
||||||
|
return {
|
||||||
|
'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'),
|
||||||
|
'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'),
|
||||||
|
}
|
||||||
33
programmer/forms.py
Normal file
33
programmer/forms.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import CallbackRequest
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CallbackRequest
|
||||||
|
fields = ['name', 'phone', 'email', 'question']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-input',
|
||||||
|
'placeholder': 'Ваше имя'
|
||||||
|
}),
|
||||||
|
'phone': forms.TextInput(attrs={
|
||||||
|
'class': 'form-input',
|
||||||
|
'placeholder': '+7 (___) ___-__-__'
|
||||||
|
}),
|
||||||
|
'email': forms.EmailInput(attrs={
|
||||||
|
'class': 'form-input',
|
||||||
|
'placeholder': 'your@email.com'
|
||||||
|
}),
|
||||||
|
'question': forms.Textarea(attrs={
|
||||||
|
'class': 'form-textarea',
|
||||||
|
'placeholder': 'Опишите ваш вопрос или задачу...',
|
||||||
|
'rows': 4
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'name': 'Имя',
|
||||||
|
'phone': 'Телефон',
|
||||||
|
'email': 'Электронная почта',
|
||||||
|
'question': 'Ваш вопрос'
|
||||||
|
}
|
||||||
0
programmer/management/__init__.py
Normal file
0
programmer/management/__init__.py
Normal file
BIN
programmer/management/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
programmer/management/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
programmer/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
programmer/management/commands/__init__.py
Normal file
0
programmer/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
33
programmer/management/commands/send_daily_summary.py
Normal file
33
programmer/management/commands/send_daily_summary.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# programmer/management/commands/send_daily_summary.py
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from programmer.utils.email_notifications import send_daily_summary
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Отправляет ежедневную сводку по заявкам на email'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--test',
|
||||||
|
action='store_true',
|
||||||
|
help='Тестовая отправка (не учитывает реальные данные)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options['test']:
|
||||||
|
self.stdout.write(self.style.WARNING('Тестовая отправка ежедневной сводки...'))
|
||||||
|
# Здесь можно добавить тестовые данные
|
||||||
|
else:
|
||||||
|
self.stdout.write('Отправка ежедневной сводки...')
|
||||||
|
|
||||||
|
success = send_daily_summary()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Ежедневная сводка отправлена успешно! Время: {timezone.now()}')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING('Ежедневная сводка не отправлена (нет данных или ошибка)')
|
||||||
|
)
|
||||||
29
programmer/management/commands/test_email.py
Normal file
29
programmer/management/commands/test_email.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# programmer/management/commands/test_email.py
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from programmer.utils.email_notifications import send_test_email
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Test email configuration'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write("Testing email configuration...")
|
||||||
|
|
||||||
|
# Проверяем настройки
|
||||||
|
self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}")
|
||||||
|
self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}")
|
||||||
|
self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}")
|
||||||
|
self.stdout.write(f"ADMIN_EMAILS: {settings.ADMIN_EMAILS}")
|
||||||
|
|
||||||
|
# Тестируем отправку
|
||||||
|
success = send_test_email()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS('✅ Test email sent successfully!')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR('❌ Failed to send test email. Check your email settings.')
|
||||||
|
)
|
||||||
27
programmer/management/commands/test_sitemap.py
Normal file
27
programmer/management/commands/test_sitemap.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.sitemaps import Sitemap
|
||||||
|
from django.urls import reverse
|
||||||
|
from programmer.models import Home, Solution, Competence, Recall
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Test sitemap generation'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
from programmer.sitemaps import sitemaps
|
||||||
|
|
||||||
|
self.stdout.write('Testing sitemap generation...')
|
||||||
|
|
||||||
|
for name, sitemap in sitemaps.items():
|
||||||
|
self.stdout.write(f'\n{name}:')
|
||||||
|
items = sitemap().items()
|
||||||
|
self.stdout.write(f' Items found: {len(items)}')
|
||||||
|
|
||||||
|
for item in items[:3]: # Показываем первые 3 элемента
|
||||||
|
try:
|
||||||
|
url = sitemap().location(item)
|
||||||
|
self.stdout.write(f' - {url}')
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(f' - Error: {e}')
|
||||||
|
|
||||||
|
self.stdout.write('\nSitemap test completed!')
|
||||||
54
programmer/middleware.py
Normal file
54
programmer/middleware.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from .models import PageView, Visitor
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
|
||||||
|
class PageViewMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
# Игнорируем статические файлы и админку
|
||||||
|
if not request.path.startswith('/static/') and not request.path.startswith('/admin/'):
|
||||||
|
self.track_page_view(request)
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def track_page_view(self, request):
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# Сохраняем просмотр страницы
|
||||||
|
PageView.objects.create(
|
||||||
|
url=request.path,
|
||||||
|
ip_address=self.get_client_ip(request),
|
||||||
|
user_agent=request.META.get('HTTP_USER_AGENT', ''),
|
||||||
|
referer=request.META.get('HTTP_REFERER', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем статистику посетителя
|
||||||
|
ip = self.get_client_ip(request)
|
||||||
|
visitor, created = Visitor.objects.get_or_create(
|
||||||
|
ip_address=ip,
|
||||||
|
defaults={
|
||||||
|
'first_visit': timezone.now(),
|
||||||
|
'last_visit': timezone.now()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
visitor.last_visit = timezone.now()
|
||||||
|
visitor.visit_count += 1
|
||||||
|
visitor.save()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Логируем ошибку, но не прерываем выполнение
|
||||||
|
print(f"Error tracking page view: {e}")
|
||||||
|
|
||||||
|
def get_client_ip(self, request):
|
||||||
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||||
|
if x_forwarded_for:
|
||||||
|
ip = x_forwarded_for.split(',')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
return ip
|
||||||
26
programmer/migrations/0001_initial.py
Normal file
26
programmer/migrations/0001_initial.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-23 12:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Competence',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('content', models.TextField(blank=True)),
|
||||||
|
('photo', models.ImageField(upload_to='photos/%Y/%m/%d/')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True)),
|
||||||
|
('is_publiched', models.BooleanField(default=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 08:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='competence',
|
||||||
|
options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компитенция', 'verbose_name_plural': 'Компитенции'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Компетенция'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='is_publiched',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Опубликован'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='photo',
|
||||||
|
field=models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='time_create',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='time_update',
|
||||||
|
field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(max_length=255, verbose_name='Программист'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 11:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0002_alter_competence_options_alter_competence_content_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Recall',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Организация')),
|
||||||
|
('content', models.TextField(blank=True, verbose_name='Отзыв')),
|
||||||
|
('photo', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
|
||||||
|
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Отзыв',
|
||||||
|
'verbose_name_plural': 'Отзывы',
|
||||||
|
'ordering': ['time_create', 'title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='competence',
|
||||||
|
old_name='is_publiched',
|
||||||
|
new_name='is_published',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
programmer/migrations/0004_rename_photo_recall_scan.py
Normal file
18
programmer/migrations/0004_rename_photo_recall_scan.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 12:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0003_recall_rename_is_publiched_competence_is_published'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='recall',
|
||||||
|
old_name='photo',
|
||||||
|
new_name='scan',
|
||||||
|
),
|
||||||
|
]
|
||||||
31
programmer/migrations/0005_auto_20231124_1519.py
Normal file
31
programmer/migrations/0005_auto_20231124_1519.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 12:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0004_rename_photo_recall_scan'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Recall',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Организация')),
|
||||||
|
('content', models.TextField(blank=True, verbose_name='Отзыв')),
|
||||||
|
('scan', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Скан')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
|
||||||
|
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Отзыв',
|
||||||
|
'verbose_name_plural': 'Отзывы',
|
||||||
|
'ordering': ['time_create', 'title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
programmer/migrations/0006_alter_recall_scan.py
Normal file
18
programmer/migrations/0006_alter_recall_scan.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-25 09:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0005_auto_20231124_1519'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='recall',
|
||||||
|
name='scan',
|
||||||
|
field=models.ImageField(upload_to='scan/%Y/%m/%d/', verbose_name='Фото'),
|
||||||
|
),
|
||||||
|
]
|
||||||
31
programmer/migrations/0007_solution.py
Normal file
31
programmer/migrations/0007_solution.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-25 09:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0006_alter_recall_scan'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Solution',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Наименование')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||||
|
('implementation', models.TextField(blank=True, verbose_name='Реализация')),
|
||||||
|
('closing', models.TextField(blank=True, verbose_name='Заключение')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
|
||||||
|
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Проекты',
|
||||||
|
'verbose_name_plural': 'Проекты',
|
||||||
|
'ordering': ['time_create', 'title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
30
programmer/migrations/0008_home.py
Normal file
30
programmer/migrations/0008_home.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-25 10:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0007_solution'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Home',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Наименование')),
|
||||||
|
('content', models.TextField(blank=True, verbose_name='Статья')),
|
||||||
|
('home_image', models.ImageField(upload_to='home_image/%Y/%m/%d/', verbose_name='Фото')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
|
||||||
|
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Главная страница',
|
||||||
|
'verbose_name_plural': 'Главная страница',
|
||||||
|
'ordering': ['time_create', 'title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-11-09 12:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0008_home'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CallbackRequest',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Имя')),
|
||||||
|
('phone', models.CharField(max_length=20, verbose_name='Телефон')),
|
||||||
|
('email', models.EmailField(max_length=254, verbose_name='Электронная почта')),
|
||||||
|
('question', models.TextField(verbose_name='Ваш вопрос')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
|
('is_processed', models.BooleanField(default=False, verbose_name='Обработано')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Заявка на звонок',
|
||||||
|
'verbose_name_plural': 'Заявки на звонок',
|
||||||
|
'ordering': ['-time_create'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='competence',
|
||||||
|
options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компетенция', 'verbose_name_plural': 'Компетенции'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-11-09 12:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0009_callbackrequest_alter_competence_options'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='callbackrequest',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='callbackrequest',
|
||||||
|
name='question',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Ваш вопрос'),
|
||||||
|
),
|
||||||
|
]
|
||||||
41
programmer/migrations/0011_visitor_pageview.py
Normal file
41
programmer/migrations/0011_visitor_pageview.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 4.2.26 on 2025-11-12 11:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0010_alter_callbackrequest_email_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Visitor',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ip_address', models.GenericIPAddressField()),
|
||||||
|
('first_visit', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('last_visit', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('visit_count', models.IntegerField(default=1)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'indexes': [models.Index(fields=['ip_address'], name='programmer__ip_addr_2c6dca_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PageView',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('url', models.CharField(max_length=500)),
|
||||||
|
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('ip_address', models.GenericIPAddressField()),
|
||||||
|
('user_agent', models.TextField(blank=True)),
|
||||||
|
('referer', models.CharField(blank=True, max_length=500)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'indexes': [models.Index(fields=['url', 'timestamp'], name='programmer__url_9a41b2_idx'), models.Index(fields=['timestamp'], name='programmer__timesta_070072_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.26 on 2025-11-14 10:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0011_visitor_pageview'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callbackrequest',
|
||||||
|
name='is_read',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Прочитано'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callbackrequest',
|
||||||
|
name='notification_sent',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Уведомление отправлено'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
programmer/migrations/__init__.py
Normal file
0
programmer/migrations/__init__.py
Normal file
BIN
programmer/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
programmer/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
programmer/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
programmer/migrations/__pycache__/0007_solution.cpython-310.pyc
Normal file
BIN
programmer/migrations/__pycache__/0007_solution.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/migrations/__pycache__/0007_solution.cpython-311.pyc
Normal file
BIN
programmer/migrations/__pycache__/0007_solution.cpython-311.pyc
Normal file
Binary file not shown.
BIN
programmer/migrations/__pycache__/0008_home.cpython-310.pyc
Normal file
BIN
programmer/migrations/__pycache__/0008_home.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/migrations/__pycache__/0008_home.cpython-311.pyc
Normal file
BIN
programmer/migrations/__pycache__/0008_home.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
programmer/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
programmer/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
programmer/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
programmer/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
154
programmer/models.py
Normal file
154
programmer/models.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.core.cache import cache
|
||||||
|
from .utils.email_notifications import send_callback_notification
|
||||||
|
|
||||||
|
|
||||||
|
class Recall(models.Model):
|
||||||
|
title = models.CharField(max_length=255, verbose_name='Организация')
|
||||||
|
content = models.TextField(blank=True, verbose_name='Отзыв')
|
||||||
|
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото')
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('post', kwargs={'post_id': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Отзыв'
|
||||||
|
verbose_name_plural = 'Отзывы'
|
||||||
|
ordering = ['time_create', 'title']
|
||||||
|
|
||||||
|
|
||||||
|
class Competence(models.Model):
|
||||||
|
title = models.CharField(max_length=255, verbose_name='Программист')
|
||||||
|
content = models.TextField(blank=True, verbose_name='Компетенция')
|
||||||
|
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото')
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('post', kwargs={'post_id': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Компетенция'
|
||||||
|
verbose_name_plural = 'Компетенции'
|
||||||
|
ordering = ['time_create', 'title']
|
||||||
|
|
||||||
|
|
||||||
|
class Solution(models.Model):
|
||||||
|
title = models.CharField(max_length=255, verbose_name='Наименование')
|
||||||
|
description = models.TextField(blank=True, verbose_name='Описание')
|
||||||
|
implementation = models.TextField(blank=True, verbose_name='Реализация')
|
||||||
|
closing = models.TextField(blank=True, verbose_name='Заключение')
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('post', kwargs={'post_id': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Проекты'
|
||||||
|
verbose_name_plural = 'Проекты'
|
||||||
|
ordering = ['time_create', 'title']
|
||||||
|
|
||||||
|
|
||||||
|
class Home(models.Model):
|
||||||
|
title = models.CharField(max_length=255, verbose_name='Наименование')
|
||||||
|
content = models.TextField(blank=True, verbose_name='Статья')
|
||||||
|
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", verbose_name='Фото')
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('post', kwargs={'post_id': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Главная страница'
|
||||||
|
verbose_name_plural = 'Главная страница'
|
||||||
|
ordering = ['time_create', 'title']
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackRequest(models.Model):
|
||||||
|
name = models.CharField(max_length=100, verbose_name='Имя')
|
||||||
|
phone = models.CharField(max_length=20, verbose_name='Телефон')
|
||||||
|
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным
|
||||||
|
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
|
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
|
||||||
|
is_read = models.BooleanField(default=False, verbose_name='Прочитано')
|
||||||
|
notification_sent = models.BooleanField(default=False, verbose_name='Уведомление отправлено')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} - {self.phone}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Заявка на звонок'
|
||||||
|
verbose_name_plural = 'Заявки на звонок'
|
||||||
|
ordering = ['-time_create']
|
||||||
|
|
||||||
|
|
||||||
|
# Сигнал для отправки уведомления при создании заявки
|
||||||
|
@receiver(post_save, sender=CallbackRequest)
|
||||||
|
def send_callback_email_notification(sender, instance, created, **kwargs):
|
||||||
|
if created and not instance.notification_sent:
|
||||||
|
# Отправляем email уведомление
|
||||||
|
success = send_callback_notification(instance)
|
||||||
|
if success:
|
||||||
|
instance.notification_sent = True
|
||||||
|
# Сохраняем без повторного вызова сигнала
|
||||||
|
sender.objects.filter(pk=instance.pk).update(notification_sent=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PageView(models.Model):
|
||||||
|
url = models.CharField(max_length=500)
|
||||||
|
timestamp = models.DateTimeField(default=timezone.now)
|
||||||
|
ip_address = models.GenericIPAddressField()
|
||||||
|
user_agent = models.TextField(blank=True)
|
||||||
|
referer = models.CharField(max_length=500, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['url', 'timestamp']),
|
||||||
|
models.Index(fields=['timestamp']),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Visitor(models.Model):
|
||||||
|
ip_address = models.GenericIPAddressField()
|
||||||
|
first_visit = models.DateTimeField(default=timezone.now)
|
||||||
|
last_visit = models.DateTimeField(default=timezone.now)
|
||||||
|
visit_count = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['ip_address']),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@receiver([post_save, post_delete], sender=Home)
|
||||||
|
@receiver([post_save, post_delete], sender=Solution)
|
||||||
|
@receiver([post_save, post_delete], sender=Competence)
|
||||||
|
@receiver([post_save, post_delete], sender=Recall)
|
||||||
|
def clear_sitemap_cache(sender, **kwargs):
|
||||||
|
"""Очищаем кэш sitemap при изменении контента"""
|
||||||
|
cache.delete('sitemap_cache')
|
||||||
64
programmer/sitemaps.py
Normal file
64
programmer/sitemaps.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from django.contrib.sitemaps import Sitemap
|
||||||
|
from django.urls import reverse
|
||||||
|
from .models import Home, Solution, Competence, Recall
|
||||||
|
|
||||||
|
class StaticViewSitemap(Sitemap):
|
||||||
|
priority = 1.0
|
||||||
|
changefreq = 'monthly'
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return ['home', 'about', 'solution', 'ability', 'recall']
|
||||||
|
|
||||||
|
def location(self, item):
|
||||||
|
return reverse(item)
|
||||||
|
|
||||||
|
class HomeSitemap(Sitemap):
|
||||||
|
changefreq = 'weekly'
|
||||||
|
priority = 1.0
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Home.objects.filter(is_published=True)
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.time_update
|
||||||
|
|
||||||
|
# УБИРАЕМ метод location - используем default
|
||||||
|
# Django автоматически сгенерирует правильные URL
|
||||||
|
|
||||||
|
class SolutionSitemap(Sitemap):
|
||||||
|
changefreq = 'weekly'
|
||||||
|
priority = 0.9
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Solution.objects.filter(is_published=True)
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.time_update
|
||||||
|
|
||||||
|
class CompetenceSitemap(Sitemap):
|
||||||
|
changefreq = 'monthly'
|
||||||
|
priority = 0.8
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Competence.objects.filter(is_published=True)
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.time_update
|
||||||
|
|
||||||
|
class RecallSitemap(Sitemap):
|
||||||
|
changefreq = 'monthly'
|
||||||
|
priority = 0.7
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Recall.objects.filter(is_published=True)
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.time_update
|
||||||
|
|
||||||
|
# Упрощаем sitemaps - убираем HomeSitemap если он дублирует главную
|
||||||
|
sitemaps = {
|
||||||
|
'static': StaticViewSitemap,
|
||||||
|
'solutions': SolutionSitemap,
|
||||||
|
'competence': CompetenceSitemap,
|
||||||
|
'recall': RecallSitemap,
|
||||||
|
}
|
||||||
275
programmer/static/admin/css/autocomplete.css
Normal file
275
programmer/static/admin/css/autocomplete.css
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
select.admin-autocomplete {
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single,
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||||
|
color: var(--body-fg);
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: #888 transparent transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 4px 0 4px;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||||
|
left: 1px;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||||
|
border-color: transparent transparent #888 transparent;
|
||||||
|
border-width: 0 4px 5px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
box-sizing: border-box;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 10px 5px 5px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
margin-top: 5px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 5px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
float: left;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||||
|
border: solid var(--body-quiet-color) 1px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||||
|
background-color: var(--darkened-bg);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-search--dropdown {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--body-fg);
|
||||||
|
background: var(--body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||||
|
background-color: var(--selected-bg);
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -1em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -2em;
|
||||||
|
padding-left: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -3em;
|
||||||
|
padding-left: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -4em;
|
||||||
|
padding-left: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||||
|
margin-left: -5em;
|
||||||
|
padding-left: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--primary-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--admin-autocomplete .select2-results__group {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
1138
programmer/static/admin/css/base.css
Normal file
1138
programmer/static/admin/css/base.css
Normal file
File diff suppressed because it is too large
Load Diff
328
programmer/static/admin/css/changelists.css
Normal file
328
programmer/static/admin/css/changelists.css
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
/* CHANGELISTS */
|
||||||
|
|
||||||
|
#changelist {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .changelist-form-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .hiddenfields { display:none; }
|
||||||
|
|
||||||
|
.change-list .filtered table {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered .results, .change-list .filtered .paginator,
|
||||||
|
.filtered #toolbar, .filtered div.xfull {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered table tbody th {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-form .results {
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .toplinks {
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .paginator {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
background: var(--body-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CHANGELIST TABLES */
|
||||||
|
|
||||||
|
#changelist table thead th {
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table thead th.action-checkbox-column {
|
||||||
|
width: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table tbody td.action-checkbox {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table tfoot {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TOOLBAR */
|
||||||
|
|
||||||
|
#toolbar {
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-top: 1px solid var(--hairline-color);
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar form input {
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 5px;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar #searchbar {
|
||||||
|
height: 1.1875rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2px 5px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar #searchbar:focus {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar form input[type="submit"] {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: var(--body-bg);
|
||||||
|
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar form input[type="submit"]:focus,
|
||||||
|
#toolbar form input[type="submit"]:hover {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-search img {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-search .help {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FILTER COLUMN */
|
||||||
|
|
||||||
|
#changelist-filter {
|
||||||
|
flex: 0 0 240px;
|
||||||
|
order: 1;
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
border-left: none;
|
||||||
|
margin: 0 0 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter h2 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 5px 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter h3,
|
||||||
|
#changelist-filter details summary {
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details summary > * {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details > summary {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details > summary::before {
|
||||||
|
content: '→';
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter details[open] > summary::before {
|
||||||
|
content: '↓';
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter ul {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter ul:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter a {
|
||||||
|
display: block;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected {
|
||||||
|
border-left: 5px solid var(--hairline-color);
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-left: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected a {
|
||||||
|
color: var(--link-selected-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter a:focus, #changelist-filter a:hover,
|
||||||
|
#changelist-filter li.selected a:focus,
|
||||||
|
#changelist-filter li.selected a:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter #changelist-filter-clear a {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DATE DRILLDOWN */
|
||||||
|
|
||||||
|
.change-list .toplinks {
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px 17px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .toplinks a {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .toplinks .date-back {
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .toplinks .date-back:focus,
|
||||||
|
.change-list .toplinks .date-back:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACTIONS */
|
||||||
|
|
||||||
|
.filtered .actions {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist table input {
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
|
||||||
|
selector and the JS adding the class can be removed. */
|
||||||
|
#changelist tbody tr.selected {
|
||||||
|
background-color: var(--selected-row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist tbody tr:has(.action-select:checked) {
|
||||||
|
background-color: var(--selected-row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions {
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions span.all,
|
||||||
|
#changelist .actions span.action-counter,
|
||||||
|
#changelist .actions span.clear,
|
||||||
|
#changelist .actions span.question {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions select {
|
||||||
|
vertical-align: top;
|
||||||
|
height: 1.5rem;
|
||||||
|
color: var(--body-fg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions select:focus {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions label {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions .button {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
137
programmer/static/admin/css/dark_mode.css
Normal file
137
programmer/static/admin/css/dark_mode.css
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--primary: #264b5d;
|
||||||
|
--primary-fg: #f7f7f7;
|
||||||
|
|
||||||
|
--body-fg: #eeeeee;
|
||||||
|
--body-bg: #121212;
|
||||||
|
--body-quiet-color: #e0e0e0;
|
||||||
|
--body-loud-color: #ffffff;
|
||||||
|
|
||||||
|
--breadcrumbs-link-fg: #e0e0e0;
|
||||||
|
--breadcrumbs-bg: var(--primary);
|
||||||
|
|
||||||
|
--link-fg: #81d4fa;
|
||||||
|
--link-hover-color: #4ac1f7;
|
||||||
|
--link-selected-fg: #6f94c6;
|
||||||
|
|
||||||
|
--hairline-color: #272727;
|
||||||
|
--border-color: #353535;
|
||||||
|
|
||||||
|
--error-fg: #e35f5f;
|
||||||
|
--message-success-bg: #006b1b;
|
||||||
|
--message-warning-bg: #583305;
|
||||||
|
--message-error-bg: #570808;
|
||||||
|
|
||||||
|
--darkened-bg: #212121;
|
||||||
|
--selected-bg: #1b1b1b;
|
||||||
|
--selected-row: #00363a;
|
||||||
|
|
||||||
|
--close-button-bg: #333333;
|
||||||
|
--close-button-hover-bg: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--primary: #264b5d;
|
||||||
|
--primary-fg: #f7f7f7;
|
||||||
|
|
||||||
|
--body-fg: #eeeeee;
|
||||||
|
--body-bg: #121212;
|
||||||
|
--body-quiet-color: #e0e0e0;
|
||||||
|
--body-loud-color: #ffffff;
|
||||||
|
|
||||||
|
--breadcrumbs-link-fg: #e0e0e0;
|
||||||
|
--breadcrumbs-bg: var(--primary);
|
||||||
|
|
||||||
|
--link-fg: #81d4fa;
|
||||||
|
--link-hover-color: #4ac1f7;
|
||||||
|
--link-selected-fg: #6f94c6;
|
||||||
|
|
||||||
|
--hairline-color: #272727;
|
||||||
|
--border-color: #353535;
|
||||||
|
|
||||||
|
--error-fg: #e35f5f;
|
||||||
|
--message-success-bg: #006b1b;
|
||||||
|
--message-warning-bg: #583305;
|
||||||
|
--message-error-bg: #570808;
|
||||||
|
|
||||||
|
--darkened-bg: #212121;
|
||||||
|
--selected-bg: #1b1b1b;
|
||||||
|
--selected-row: #00363a;
|
||||||
|
|
||||||
|
--close-button-bg: #333333;
|
||||||
|
--close-button-hover-bg: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THEME SWITCH */
|
||||||
|
.theme-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-inline-start: 5px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Fully hide screen reader text so we only show the one matching the current
|
||||||
|
theme.
|
||||||
|
*/
|
||||||
|
.theme-toggle .visually-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] .theme-toggle .theme-label-when-light {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ICONS */
|
||||||
|
.theme-toggle svg.theme-icon-when-auto,
|
||||||
|
.theme-toggle svg.theme-icon-when-dark,
|
||||||
|
.theme-toggle svg.theme-icon-when-light {
|
||||||
|
fill: var(--header-link-color);
|
||||||
|
color: var(--header-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0,0,0,0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
color: var(--body-fg);
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
}
|
||||||
29
programmer/static/admin/css/dashboard.css
Normal file
29
programmer/static/admin/css/dashboard.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/* DASHBOARD */
|
||||||
|
.dashboard td, .dashboard th {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .module table th {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .module table td {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .module table td a {
|
||||||
|
display: block;
|
||||||
|
padding-right: .6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RECENT ACTIONS MODULE */
|
||||||
|
|
||||||
|
.module ul.actionlist {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.actionlist li {
|
||||||
|
list-style-type: none;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user