Файлы прода

This commit is contained in:
NikDizell 2025-11-19 19:32:39 +03:00
parent b9b9d1b391
commit a5c110a35b
411 changed files with 73821 additions and 0 deletions

0
OneCprogsite/__init__.py Normal file
View 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.

16
OneCprogsite/asgi.py Normal file
View 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
View 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
View 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
View 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
View File

33
dockerfile Normal file
View 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
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

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: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

0
programmer/__init__.py Normal file
View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

169
programmer/admin.py Normal file
View 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
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class ProgrammerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'programmer'
verbose_name = 'Программисты'

View 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
View 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': 'Ваш вопрос'
}

View File

View 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('Ежедневная сводка не отправлена (нет данных или ошибка)')
)

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

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

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

View File

@ -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='Программист'),
),
]

View File

@ -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',
),
]

View 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',
),
]

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

View 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='Фото'),
),
]

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

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

View File

@ -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': 'Компетенции'},
),
]

View File

@ -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='Ваш вопрос'),
),
]

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

View File

@ -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='Уведомление отправлено'),
),
]

View File

154
programmer/models.py Normal file
View 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
View 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,
}

View File

@ -0,0 +1,275 @@
select.admin-autocomplete {
width: 20em;
}
.select2-container--admin-autocomplete.select2-container {
min-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single,
.select2-container--admin-autocomplete .select2-selection--multiple {
min-height: 30px;
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
border-color: var(--body-quiet-color);
min-height: 30px;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
padding: 0;
}
.select2-container--admin-autocomplete .select2-selection--single {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
color: var(--body-fg);
line-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--admin-autocomplete .select2-selection--multiple {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: text;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 10px 5px 5px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
color: var(--body-quiet-color);
margin-top: 5px;
float: left;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin: 5px;
position: absolute;
right: 0;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
background-color: var(--darkened-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-quiet-color);
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
color: var(--body-fg);
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
border: solid var(--body-quiet-color) 1px;
outline: 0;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--admin-autocomplete .select2-search--dropdown {
background: var(--darkened-bg);
}
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
background: transparent;
color: var(--body-fg);
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
color: var(--body-fg);
background: var(--body-bg);
}
.select2-container--admin-autocomplete .select2-results__option[role=group] {
padding: 0;
}
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
background-color: var(--selected-bg);
color: var(--body-fg);
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary);
color: var(--primary-fg);
}
.select2-container--admin-autocomplete .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,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);
}

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

View 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