Последние измения на прод

This commit is contained in:
Server Deploy 2025-12-08 14:49:13 +03:00
parent 27ac664e56
commit c947dc980a
423 changed files with 88195 additions and 14555 deletions

View File

@ -1,16 +1,16 @@
""" """
ASGI config for OneCprogsite project. ASGI config for OneCprogsite project.
It exposes the ASGI callable as a module-level variable named ``application``. It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
""" """
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
application = get_asgi_application() application = get_asgi_application()

View File

@ -1,212 +1,212 @@
""" """
Django settings for OneCprogsite project. Django settings for OneCprogsite project.
Generated by 'django-admin startproject' using Django 4.2.7. Generated by 'django-admin startproject' using Django 4.2.7.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/ https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/ https://docs.djangoproject.com/en/4.2/ref/settings/
""" """
import os.path import os.path
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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' SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm'
# Безопасность cookies для HTTPS # Безопасность cookies для HTTPS
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS
SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_SAMESITE = 'Lax'
# Если используете другие cookies # Если используете другие cookies
LANGUAGE_COOKIE_SECURE = True LANGUAGE_COOKIE_SECURE = True
LANGUAGE_COOKIE_HTTPONLY = True LANGUAGE_COOKIE_HTTPONLY = True
LANGUAGE_COOKIE_SAMESITE = 'Lax' LANGUAGE_COOKIE_SAMESITE = 'Lax'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# Или разрешить конкретные домены (Django 4.0+) # Или разрешить конкретные домены (Django 4.0+)
X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru' X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru'
# ОБЯЗАТЕЛЬНО укажите ваши домены # ОБЯЗАТЕЛЬНО укажите ваши домены
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'nikdizell.ru', 'nikdizell.ru',
'www.nikdizell.ru', 'www.nikdizell.ru',
'localhost', 'localhost',
'127.0.0.1', '127.0.0.1',
'192.168.31.88' # Добавьте IP сервера '192.168.31.88' # Добавьте IP сервера
] ]
# Важно для работы за прокси # Важно для работы за прокси
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True SECURE_SSL_REDIRECT = True
# Дополнительная безопасность # Дополнительная безопасность
SECURE_BROWSER_XSS_FILTER = True SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True SECURE_HSTS_PRELOAD = True
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'https://nikdizell.ru', 'https://nikdizell.ru',
'https://www.nikdizell.ru', 'https://www.nikdizell.ru',
] ]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'programmer.apps.ProgrammerConfig', 'programmer.apps.ProgrammerConfig',
'django_bootstrap5', 'django_bootstrap5',
'django_extensions', 'django_extensions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.sitemaps', 'django.contrib.sitemaps',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'programmer.middleware.PageViewMiddleware', 'programmer.middleware.PageViewMiddleware',
] ]
ROOT_URLCONF = 'OneCprogsite.urls' ROOT_URLCONF = 'OneCprogsite.urls'
# Кастомный middleware для CSP # Кастомный middleware для CSP
class CSPMiddleware: class CSPMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
response = self.get_response(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" 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 return response
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'programmer.context_processors.menu_processor', 'programmer.context_processors.menu_processor',
'programmer.context_processors.contact_info', 'programmer.context_processors.contact_info',
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'OneCprogsite.wsgi.application' WSGI_APPLICATION = 'OneCprogsite.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'App', 'NAME': 'App',
'USER': 'postgres', 'USER': 'postgres',
'PASSWORD': 'NikDi94Zell', 'PASSWORD': 'NikDi94Zell',
'HOST': 'postgres', 'HOST': 'postgres',
'PORT': 5432, 'PORT': 5432,
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'ru' LANGUAGE_CODE = 'ru'
TIME_ZONE = 'Europe/Moscow' TIME_ZONE = 'Europe/Moscow'
USE_I18N = True USE_I18N = True
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/ # https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [] STATICFILES_DIRS = []
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
# Настройки email # Настройки email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru # EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru
# EMAIL_PORT = 587 # EMAIL_PORT = 587
# EMAIL_USE_TLS = True # EMAIL_USE_TLS = True
# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru') # EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru')
# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc') # EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc')
# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
# SERVER_EMAIL = EMAIL_HOST_USER # SERVER_EMAIL = EMAIL_HOST_USER
EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com') EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER
# Email для уведомлений (можно указать несколько через запятую) # Email для уведомлений (можно указать несколько через запятую)
# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',') # ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',')
ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',') ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',')

View File

@ -1,39 +1,39 @@
""" """
URL configuration for OneCprogsite project. URL configuration for OneCprogsite project.
The `urlpatterns` list routes URLs to views. For more information please see: The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/ https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples: Examples:
Function views Function views
1. Add an import: from my_app import views 1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home') 2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views Class-based views
1. Add an import: from other_app.views import Home 1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from OneCprogsite import settings from OneCprogsite import settings
from programmer.views import * from programmer.views import *
from django.urls import path, include from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('programmer.urls')), path('', include('programmer.urls')),
# path('', index, name='home'), # path('', index, name='home'),
# path('about/', about, name='about'), # path('about/', about, name='about'),
# path('solution/', solution, name='solution'), # path('solution/', solution, name='solution'),
# path('ability/', ability, name='ability'), # path('ability/', ability, name='ability'),
# path('recall/', recall, name='recall'), # path('recall/', recall, name='recall'),
# path('post/<int:post_id>', show_post, name='post'), # path('post/<int:post_id>', show_post, name='post'),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
handler404 = pageNotFound handler404 = pageNotFound

View File

@ -1,16 +1,16 @@
""" """
WSGI config for OneCprogsite project. WSGI config for OneCprogsite project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
""" """
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -1,16 +1,16 @@
""" """
ASGI config for OneCprogsite project. ASGI config for OneCprogsite project.
It exposes the ASGI callable as a module-level variable named ``application``. It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
""" """
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
application = get_asgi_application() application = get_asgi_application()

View File

@ -1,22 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
raise ImportError( raise ImportError(
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you " "available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?" "forget to activate a virtual environment?"
) from exc ) from exc
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,169 +1,169 @@
from django.contrib import admin from django.contrib import admin
from django.utils import timezone from django.utils import timezone
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import path from django.urls import path
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.contrib import messages from django.contrib import messages
from .models import * from .models import *
class ProgrammerAdmin(admin.ModelAdmin): class ProgrammerAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'photo', 'is_published') list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'content') search_fields = ('title', 'content')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
class RecallAdmin(admin.ModelAdmin): class RecallAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'scan', 'is_published') list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'content') search_fields = ('title', 'content')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
class SolutionAdmin(admin.ModelAdmin): class SolutionAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'description', 'implementation') search_fields = ('title', 'description', 'implementation')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
class HomeAdmin(admin.ModelAdmin): class HomeAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'content') search_fields = ('title', 'content')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
@admin.register(CallbackRequest) @admin.register(CallbackRequest)
class CallbackAdmin(admin.ModelAdmin): class CallbackAdmin(admin.ModelAdmin):
list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge') list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge')
list_display_links = ('name', 'phone') list_display_links = ('name', 'phone')
list_editable = ('is_processed', 'is_read') list_editable = ('is_processed', 'is_read')
list_filter = ('time_create', 'is_processed', 'is_read') list_filter = ('time_create', 'is_processed', 'is_read')
search_fields = ('name', 'phone', 'email') search_fields = ('name', 'phone', 'email')
readonly_fields = ('time_create',) readonly_fields = ('time_create',)
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed'] actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed']
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification'] actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
def resend_notification(self, request, queryset): def resend_notification(self, request, queryset):
from .utils.email_notifications import send_callback_notification from .utils.email_notifications import send_callback_notification
count = 0 count = 0
for callback in queryset: for callback in queryset:
success = send_callback_notification(callback) success = send_callback_notification(callback)
if success: if success:
count += 1 count += 1
self.message_user(request, f'Уведомления отправлены для {count} заявок') self.message_user(request, f'Уведомления отправлены для {count} заявок')
resend_notification.short_description = "Переотправить email уведомления" resend_notification.short_description = "Переотправить email уведомления"
def new_badge(self, obj): def new_badge(self, obj):
if not obj.is_read: if not obj.is_read:
return format_html('<span style="color: red; font-weight: bold;">🆕 НОВАЯ</span>') return format_html('<span style="color: red; font-weight: bold;">🆕 НОВАЯ</span>')
return "" return ""
new_badge.short_description = 'Статус' new_badge.short_description = 'Статус'
def get_queryset(self, request): def get_queryset(self, request):
# Показываем количество непрочитанных в заголовке # Показываем количество непрочитанных в заголовке
unread_count = CallbackRequest.objects.filter(is_read=False).count() unread_count = CallbackRequest.objects.filter(is_read=False).count()
if unread_count > 0: if unread_count > 0:
self.message_user( self.message_user(
request, request,
f'У вас {unread_count} непрочитанных заявок!', f'У вас {unread_count} непрочитанных заявок!',
messages.WARNING messages.WARNING
) )
return super().get_queryset(request) return super().get_queryset(request)
def mark_as_read(self, request, queryset): def mark_as_read(self, request, queryset):
updated = queryset.update(is_read=True) updated = queryset.update(is_read=True)
self.message_user(request, f'{updated} заявок отмечены как прочитанные') self.message_user(request, f'{updated} заявок отмечены как прочитанные')
mark_as_read.short_description = "Отметить как прочитанные" mark_as_read.short_description = "Отметить как прочитанные"
def mark_as_unread(self, request, queryset): def mark_as_unread(self, request, queryset):
updated = queryset.update(is_read=False) updated = queryset.update(is_read=False)
self.message_user(request, f'{updated} заявок отмечены как непрочитанные') self.message_user(request, f'{updated} заявок отмечены как непрочитанные')
mark_as_unread.short_description = "Отметить как непрочитанные" mark_as_unread.short_description = "Отметить как непрочитанные"
def mark_as_processed(self, request, queryset): def mark_as_processed(self, request, queryset):
updated = queryset.update(is_processed=True) updated = queryset.update(is_processed=True)
self.message_user(request, f'{updated} заявок отмечены как обработанные') self.message_user(request, f'{updated} заявок отмечены как обработанные')
mark_as_processed.short_description = "Отметить как обработанные" mark_as_processed.short_description = "Отметить как обработанные"
# Добавляем кастомное представление для статистики # Добавляем кастомное представление для статистики
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'), path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'),
] ]
return custom_urls + urls return custom_urls + urls
def callback_stats(self, request): def callback_stats(self, request):
today = timezone.now().date() today = timezone.now().date()
week_ago = today - timezone.timedelta(days=7) week_ago = today - timezone.timedelta(days=7)
stats = { stats = {
'total': CallbackRequest.objects.count(), 'total': CallbackRequest.objects.count(),
'today': CallbackRequest.objects.filter(time_create__date=today).count(), 'today': CallbackRequest.objects.filter(time_create__date=today).count(),
'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(), 'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(),
'unread': CallbackRequest.objects.filter(is_read=False).count(), 'unread': CallbackRequest.objects.filter(is_read=False).count(),
'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(), 'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(),
} }
context = { context = {
**self.admin_site.each_context(request), **self.admin_site.each_context(request),
'title': 'Статистика заявок', 'title': 'Статистика заявок',
'stats': stats, 'stats': stats,
} }
return render(request, 'admin/callback_stats.html', context) return render(request, 'admin/callback_stats.html', context)
class ProgrammerAdmin(admin.ModelAdmin): class ProgrammerAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'photo', 'is_published') list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'content') search_fields = ('title', 'content')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
class RecallAdmin(admin.ModelAdmin): class RecallAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'scan', 'is_published') list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'content') search_fields = ('title', 'content')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
class SolutionAdmin(admin.ModelAdmin): class SolutionAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'description', 'implementation') search_fields = ('title', 'description', 'implementation')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
class HomeAdmin(admin.ModelAdmin): class HomeAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
search_fields = ('title', 'content') search_fields = ('title', 'content')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
@admin.register(PageView) @admin.register(PageView)
class PageViewAdmin(admin.ModelAdmin): class PageViewAdmin(admin.ModelAdmin):
list_display = ['url', 'timestamp', 'ip_address'] list_display = ['url', 'timestamp', 'ip_address']
list_filter = ['timestamp', 'url'] list_filter = ['timestamp', 'url']
search_fields = ['url', 'ip_address'] search_fields = ['url', 'ip_address']
date_hierarchy = 'timestamp' date_hierarchy = 'timestamp'
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).order_by('-timestamp') return super().get_queryset(request).order_by('-timestamp')
admin.site.register(Competence, ProgrammerAdmin) admin.site.register(Competence, ProgrammerAdmin)
admin.site.register(Recall, RecallAdmin) admin.site.register(Recall, RecallAdmin)
admin.site.register(Solution, SolutionAdmin) admin.site.register(Solution, SolutionAdmin)
admin.site.register(Home, HomeAdmin) admin.site.register(Home, HomeAdmin)

View File

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

View File

@ -1,12 +1,12 @@
from .views import menu from .views import menu
from django.conf import settings from django.conf import settings
def menu_processor(request): def menu_processor(request):
return {'menu': menu} return {'menu': menu}
def contact_info(request): def contact_info(request):
return { return {
'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'), 'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'),
'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'), 'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'),
} }

View File

@ -1,33 +1,33 @@
from django import forms from django import forms
from .models import CallbackRequest from .models import CallbackRequest
class CallbackForm(forms.ModelForm): class CallbackForm(forms.ModelForm):
class Meta: class Meta:
model = CallbackRequest model = CallbackRequest
fields = ['name', 'phone', 'email', 'question'] fields = ['name', 'phone', 'email', 'question']
widgets = { widgets = {
'name': forms.TextInput(attrs={ 'name': forms.TextInput(attrs={
'class': 'form-input', 'class': 'form-input',
'placeholder': 'Ваше имя' 'placeholder': 'Ваше имя'
}), }),
'phone': forms.TextInput(attrs={ 'phone': forms.TextInput(attrs={
'class': 'form-input', 'class': 'form-input',
'placeholder': '+7 (___) ___-__-__' 'placeholder': '+7 (___) ___-__-__'
}), }),
'email': forms.EmailInput(attrs={ 'email': forms.EmailInput(attrs={
'class': 'form-input', 'class': 'form-input',
'placeholder': 'your@email.com' 'placeholder': 'your@email.com'
}), }),
'question': forms.Textarea(attrs={ 'question': forms.Textarea(attrs={
'class': 'form-textarea', 'class': 'form-textarea',
'placeholder': 'Опишите ваш вопрос или задачу...', 'placeholder': 'Опишите ваш вопрос или задачу...',
'rows': 4 'rows': 4
}), }),
} }
labels = { labels = {
'name': 'Имя', 'name': 'Имя',
'phone': 'Телефон', 'phone': 'Телефон',
'email': 'Электронная почта', 'email': 'Электронная почта',
'question': 'Ваш вопрос' 'question': 'Ваш вопрос'
} }

View File

@ -1,33 +1,33 @@
# programmer/management/commands/send_daily_summary.py # programmer/management/commands/send_daily_summary.py
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from programmer.utils.email_notifications import send_daily_summary from programmer.utils.email_notifications import send_daily_summary
class Command(BaseCommand): class Command(BaseCommand):
help = 'Отправляет ежедневную сводку по заявкам на email' help = 'Отправляет ежедневную сводку по заявкам на email'
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--test', '--test',
action='store_true', action='store_true',
help='Тестовая отправка (не учитывает реальные данные)', help='Тестовая отправка (не учитывает реальные данные)',
) )
def handle(self, *args, **options): def handle(self, *args, **options):
if options['test']: if options['test']:
self.stdout.write(self.style.WARNING('Тестовая отправка ежедневной сводки...')) self.stdout.write(self.style.WARNING('Тестовая отправка ежедневной сводки...'))
# Здесь можно добавить тестовые данные # Здесь можно добавить тестовые данные
else: else:
self.stdout.write('Отправка ежедневной сводки...') self.stdout.write('Отправка ежедневной сводки...')
success = send_daily_summary() success = send_daily_summary()
if success: if success:
self.stdout.write( self.stdout.write(
self.style.SUCCESS(f'Ежедневная сводка отправлена успешно! Время: {timezone.now()}') self.style.SUCCESS(f'Ежедневная сводка отправлена успешно! Время: {timezone.now()}')
) )
else: else:
self.stdout.write( self.stdout.write(
self.style.WARNING('Ежедневная сводка не отправлена (нет данных или ошибка)') self.style.WARNING('Ежедневная сводка не отправлена (нет данных или ошибка)')
) )

View File

@ -1,29 +1,29 @@
# programmer/management/commands/test_email.py # programmer/management/commands/test_email.py
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
from programmer.utils.email_notifications import send_test_email from programmer.utils.email_notifications import send_test_email
class Command(BaseCommand): class Command(BaseCommand):
help = 'Test email configuration' help = 'Test email configuration'
def handle(self, *args, **options): def handle(self, *args, **options):
self.stdout.write("Testing email configuration...") self.stdout.write("Testing email configuration...")
# Проверяем настройки # Проверяем настройки
self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}") self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}")
self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}") self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}")
self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}") self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}")
self.stdout.write(f"ADMIN_EMAILS: {settings.ADMIN_EMAILS}") self.stdout.write(f"ADMIN_EMAILS: {settings.ADMIN_EMAILS}")
# Тестируем отправку # Тестируем отправку
success = send_test_email() success = send_test_email()
if success: if success:
self.stdout.write( self.stdout.write(
self.style.SUCCESS('✅ Test email sent successfully!') self.style.SUCCESS('✅ Test email sent successfully!')
) )
else: else:
self.stdout.write( self.stdout.write(
self.style.ERROR('❌ Failed to send test email. Check your email settings.') self.style.ERROR('❌ Failed to send test email. Check your email settings.')
) )

View File

@ -1,27 +1,27 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.sitemaps import Sitemap from django.contrib.sitemaps import Sitemap
from django.urls import reverse from django.urls import reverse
from programmer.models import Home, Solution, Competence, Recall from programmer.models import Home, Solution, Competence, Recall
class Command(BaseCommand): class Command(BaseCommand):
help = 'Test sitemap generation' help = 'Test sitemap generation'
def handle(self, *args, **options): def handle(self, *args, **options):
from programmer.sitemaps import sitemaps from programmer.sitemaps import sitemaps
self.stdout.write('Testing sitemap generation...') self.stdout.write('Testing sitemap generation...')
for name, sitemap in sitemaps.items(): for name, sitemap in sitemaps.items():
self.stdout.write(f'\n{name}:') self.stdout.write(f'\n{name}:')
items = sitemap().items() items = sitemap().items()
self.stdout.write(f' Items found: {len(items)}') self.stdout.write(f' Items found: {len(items)}')
for item in items[:3]: # Показываем первые 3 элемента for item in items[:3]: # Показываем первые 3 элемента
try: try:
url = sitemap().location(item) url = sitemap().location(item)
self.stdout.write(f' - {url}') self.stdout.write(f' - {url}')
except Exception as e: except Exception as e:
self.stdout.write(f' - Error: {e}') self.stdout.write(f' - Error: {e}')
self.stdout.write('\nSitemap test completed!') self.stdout.write('\nSitemap test completed!')

View File

@ -1,54 +1,54 @@
from .models import PageView, Visitor from .models import PageView, Visitor
from django.utils import timezone from django.utils import timezone
from django.db import transaction from django.db import transaction
class PageViewMiddleware: class PageViewMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
# Игнорируем статические файлы и админку # Игнорируем статические файлы и админку
if not request.path.startswith('/static/') and not request.path.startswith('/admin/'): if not request.path.startswith('/static/') and not request.path.startswith('/admin/'):
self.track_page_view(request) self.track_page_view(request)
response = self.get_response(request) response = self.get_response(request)
return response return response
def track_page_view(self, request): def track_page_view(self, request):
try: try:
with transaction.atomic(): with transaction.atomic():
# Сохраняем просмотр страницы # Сохраняем просмотр страницы
PageView.objects.create( PageView.objects.create(
url=request.path, url=request.path,
ip_address=self.get_client_ip(request), ip_address=self.get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''), user_agent=request.META.get('HTTP_USER_AGENT', ''),
referer=request.META.get('HTTP_REFERER', '') referer=request.META.get('HTTP_REFERER', '')
) )
# Обновляем статистику посетителя # Обновляем статистику посетителя
ip = self.get_client_ip(request) ip = self.get_client_ip(request)
visitor, created = Visitor.objects.get_or_create( visitor, created = Visitor.objects.get_or_create(
ip_address=ip, ip_address=ip,
defaults={ defaults={
'first_visit': timezone.now(), 'first_visit': timezone.now(),
'last_visit': timezone.now() 'last_visit': timezone.now()
} }
) )
if not created: if not created:
visitor.last_visit = timezone.now() visitor.last_visit = timezone.now()
visitor.visit_count += 1 visitor.visit_count += 1
visitor.save() visitor.save()
except Exception as e: except Exception as e:
# Логируем ошибку, но не прерываем выполнение # Логируем ошибку, но не прерываем выполнение
print(f"Error tracking page view: {e}") print(f"Error tracking page view: {e}")
def get_client_ip(self, request): def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for: if x_forwarded_for:
ip = x_forwarded_for.split(',')[0] ip = x_forwarded_for.split(',')[0]
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')
return ip return ip

View File

@ -1,26 +1,26 @@
# Generated by Django 4.2.7 on 2023-11-23 12:47 # Generated by Django 4.2.7 on 2023-11-23 12:47
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Competence', name='Competence',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)), ('title', models.CharField(max_length=255)),
('content', models.TextField(blank=True)), ('content', models.TextField(blank=True)),
('photo', models.ImageField(upload_to='photos/%Y/%m/%d/')), ('photo', models.ImageField(upload_to='photos/%Y/%m/%d/')),
('time_create', models.DateTimeField(auto_now_add=True)), ('time_create', models.DateTimeField(auto_now_add=True)),
('time_update', models.DateTimeField(auto_now=True)), ('time_update', models.DateTimeField(auto_now=True)),
('is_publiched', models.BooleanField(default=True)), ('is_publiched', models.BooleanField(default=True)),
], ],
), ),
] ]

View File

@ -1,47 +1,47 @@
# Generated by Django 4.2.7 on 2023-11-24 08:03 # Generated by Django 4.2.7 on 2023-11-24 08:03
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0001_initial'), ('programmer', '0001_initial'),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='competence', name='competence',
options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компитенция', 'verbose_name_plural': 'Компитенции'}, options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компитенция', 'verbose_name_plural': 'Компитенции'},
), ),
migrations.AlterField( migrations.AlterField(
model_name='competence', model_name='competence',
name='content', name='content',
field=models.TextField(blank=True, verbose_name='Компетенция'), field=models.TextField(blank=True, verbose_name='Компетенция'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='competence', model_name='competence',
name='is_publiched', name='is_publiched',
field=models.BooleanField(default=True, verbose_name='Опубликован'), field=models.BooleanField(default=True, verbose_name='Опубликован'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='competence', model_name='competence',
name='photo', name='photo',
field=models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото'), field=models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='competence', model_name='competence',
name='time_create', name='time_create',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'), field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='competence', model_name='competence',
name='time_update', name='time_update',
field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'), field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='competence', model_name='competence',
name='title', name='title',
field=models.CharField(max_length=255, verbose_name='Программист'), field=models.CharField(max_length=255, verbose_name='Программист'),
), ),
] ]

View File

@ -1,35 +1,35 @@
# Generated by Django 4.2.7 on 2023-11-24 11:54 # Generated by Django 4.2.7 on 2023-11-24 11:54
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0002_alter_competence_options_alter_competence_content_and_more'), ('programmer', '0002_alter_competence_options_alter_competence_content_and_more'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Recall', name='Recall',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='Организация')), ('title', models.CharField(max_length=255, verbose_name='Организация')),
('content', models.TextField(blank=True, verbose_name='Отзыв')), ('content', models.TextField(blank=True, verbose_name='Отзыв')),
('photo', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото')), ('photo', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
], ],
options={ options={
'verbose_name': 'Отзыв', 'verbose_name': 'Отзыв',
'verbose_name_plural': 'Отзывы', 'verbose_name_plural': 'Отзывы',
'ordering': ['time_create', 'title'], 'ordering': ['time_create', 'title'],
}, },
), ),
migrations.RenameField( migrations.RenameField(
model_name='competence', model_name='competence',
old_name='is_publiched', old_name='is_publiched',
new_name='is_published', new_name='is_published',
), ),
] ]

View File

@ -1,18 +1,18 @@
# Generated by Django 4.2.7 on 2023-11-24 12:02 # Generated by Django 4.2.7 on 2023-11-24 12:02
from django.db import migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0003_recall_rename_is_publiched_competence_is_published'), ('programmer', '0003_recall_rename_is_publiched_competence_is_published'),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='recall', model_name='recall',
old_name='photo', old_name='photo',
new_name='scan', new_name='scan',
), ),
] ]

View File

@ -1,31 +1,31 @@
# Generated by Django 4.2.7 on 2023-11-24 12:19 # Generated by Django 4.2.7 on 2023-11-24 12:19
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0004_rename_photo_recall_scan'), ('programmer', '0004_rename_photo_recall_scan'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Recall', name='Recall',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='Организация')), ('title', models.CharField(max_length=255, verbose_name='Организация')),
('content', models.TextField(blank=True, verbose_name='Отзыв')), ('content', models.TextField(blank=True, verbose_name='Отзыв')),
('scan', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Скан')), ('scan', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Скан')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
], ],
options={ options={
'verbose_name': 'Отзыв', 'verbose_name': 'Отзыв',
'verbose_name_plural': 'Отзывы', 'verbose_name_plural': 'Отзывы',
'ordering': ['time_create', 'title'], 'ordering': ['time_create', 'title'],
}, },
), ),
] ]

View File

@ -1,18 +1,18 @@
# Generated by Django 4.2.7 on 2023-11-25 09:18 # Generated by Django 4.2.7 on 2023-11-25 09:18
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0005_auto_20231124_1519'), ('programmer', '0005_auto_20231124_1519'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='recall', model_name='recall',
name='scan', name='scan',
field=models.ImageField(upload_to='scan/%Y/%m/%d/', verbose_name='Фото'), field=models.ImageField(upload_to='scan/%Y/%m/%d/', verbose_name='Фото'),
), ),
] ]

View File

@ -1,31 +1,31 @@
# Generated by Django 4.2.7 on 2023-11-25 09:51 # Generated by Django 4.2.7 on 2023-11-25 09:51
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0006_alter_recall_scan'), ('programmer', '0006_alter_recall_scan'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Solution', name='Solution',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='Наименование')), ('title', models.CharField(max_length=255, verbose_name='Наименование')),
('description', models.TextField(blank=True, verbose_name='Описание')), ('description', models.TextField(blank=True, verbose_name='Описание')),
('implementation', models.TextField(blank=True, verbose_name='Реализация')), ('implementation', models.TextField(blank=True, verbose_name='Реализация')),
('closing', models.TextField(blank=True, verbose_name='Заключение')), ('closing', models.TextField(blank=True, verbose_name='Заключение')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
], ],
options={ options={
'verbose_name': 'Проекты', 'verbose_name': 'Проекты',
'verbose_name_plural': 'Проекты', 'verbose_name_plural': 'Проекты',
'ordering': ['time_create', 'title'], 'ordering': ['time_create', 'title'],
}, },
), ),
] ]

View File

@ -1,30 +1,30 @@
# Generated by Django 4.2.7 on 2023-11-25 10:45 # Generated by Django 4.2.7 on 2023-11-25 10:45
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0007_solution'), ('programmer', '0007_solution'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Home', name='Home',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='Наименование')), ('title', models.CharField(max_length=255, verbose_name='Наименование')),
('content', models.TextField(blank=True, verbose_name='Статья')), ('content', models.TextField(blank=True, verbose_name='Статья')),
('home_image', models.ImageField(upload_to='home_image/%Y/%m/%d/', 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_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
], ],
options={ options={
'verbose_name': 'Главная страница', 'verbose_name': 'Главная страница',
'verbose_name_plural': 'Главная страница', 'verbose_name_plural': 'Главная страница',
'ordering': ['time_create', 'title'], 'ordering': ['time_create', 'title'],
}, },
), ),
] ]

View File

@ -1,34 +1,34 @@
# Generated by Django 4.2.7 on 2025-11-09 12:00 # Generated by Django 4.2.7 on 2025-11-09 12:00
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0008_home'), ('programmer', '0008_home'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CallbackRequest', name='CallbackRequest',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Имя')), ('name', models.CharField(max_length=100, verbose_name='Имя')),
('phone', models.CharField(max_length=20, verbose_name='Телефон')), ('phone', models.CharField(max_length=20, verbose_name='Телефон')),
('email', models.EmailField(max_length=254, verbose_name='Электронная почта')), ('email', models.EmailField(max_length=254, verbose_name='Электронная почта')),
('question', models.TextField(verbose_name='Ваш вопрос')), ('question', models.TextField(verbose_name='Ваш вопрос')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('is_processed', models.BooleanField(default=False, verbose_name='Обработано')), ('is_processed', models.BooleanField(default=False, verbose_name='Обработано')),
], ],
options={ options={
'verbose_name': 'Заявка на звонок', 'verbose_name': 'Заявка на звонок',
'verbose_name_plural': 'Заявки на звонок', 'verbose_name_plural': 'Заявки на звонок',
'ordering': ['-time_create'], 'ordering': ['-time_create'],
}, },
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='competence', name='competence',
options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компетенция', 'verbose_name_plural': 'Компетенции'}, options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компетенция', 'verbose_name_plural': 'Компетенции'},
), ),
] ]

View File

@ -1,23 +1,23 @@
# Generated by Django 4.2.7 on 2025-11-09 12:09 # Generated by Django 4.2.7 on 2025-11-09 12:09
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0009_callbackrequest_alter_competence_options'), ('programmer', '0009_callbackrequest_alter_competence_options'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='callbackrequest', model_name='callbackrequest',
name='email', name='email',
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта'), field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='callbackrequest', model_name='callbackrequest',
name='question', name='question',
field=models.TextField(blank=True, verbose_name='Ваш вопрос'), field=models.TextField(blank=True, verbose_name='Ваш вопрос'),
), ),
] ]

View File

@ -1,41 +1,41 @@
# Generated by Django 4.2.7 on 2025-11-12 11:43 # Generated by Django 4.2.7 on 2025-11-12 11:43
from django.db import migrations, models from django.db import migrations, models
import django.utils.timezone import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0010_alter_callbackrequest_email_and_more'), ('programmer', '0010_alter_callbackrequest_email_and_more'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Visitor', name='Visitor',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip_address', models.GenericIPAddressField()), ('ip_address', models.GenericIPAddressField()),
('first_visit', models.DateTimeField(default=django.utils.timezone.now)), ('first_visit', models.DateTimeField(default=django.utils.timezone.now)),
('last_visit', models.DateTimeField(default=django.utils.timezone.now)), ('last_visit', models.DateTimeField(default=django.utils.timezone.now)),
('visit_count', models.IntegerField(default=1)), ('visit_count', models.IntegerField(default=1)),
], ],
options={ options={
'indexes': [models.Index(fields=['ip_address'], name='programmer__ip_addr_2c6dca_idx')], 'indexes': [models.Index(fields=['ip_address'], name='programmer__ip_addr_2c6dca_idx')],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='PageView', name='PageView',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.CharField(max_length=500)), ('url', models.CharField(max_length=500)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)), ('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('ip_address', models.GenericIPAddressField()), ('ip_address', models.GenericIPAddressField()),
('user_agent', models.TextField(blank=True)), ('user_agent', models.TextField(blank=True)),
('referer', models.CharField(blank=True, max_length=500)), ('referer', models.CharField(blank=True, max_length=500)),
], ],
options={ options={
'indexes': [models.Index(fields=['url', 'timestamp'], name='programmer__url_9a41b2_idx'), models.Index(fields=['timestamp'], name='programmer__timesta_070072_idx')], 'indexes': [models.Index(fields=['url', 'timestamp'], name='programmer__url_9a41b2_idx'), models.Index(fields=['timestamp'], name='programmer__timesta_070072_idx')],
}, },
), ),
] ]

View File

@ -1,18 +1,18 @@
# Generated by Django 4.2.7 on 2025-11-14 09:37 # Generated by Django 4.2.7 on 2025-11-14 09:37
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0011_visitor_pageview'), ('programmer', '0011_visitor_pageview'),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='callbackrequest', model_name='callbackrequest',
name='is_read', name='is_read',
field=models.BooleanField(default=False, verbose_name='Прочитано'), field=models.BooleanField(default=False, verbose_name='Прочитано'),
), ),
] ]

View File

@ -1,18 +1,18 @@
# Generated by Django 4.2.7 on 2025-11-14 10:03 # Generated by Django 4.2.7 on 2025-11-14 10:03
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('programmer', '0012_callbackrequest_is_read'), ('programmer', '0012_callbackrequest_is_read'),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='callbackrequest', model_name='callbackrequest',
name='notification_sent', name='notification_sent',
field=models.BooleanField(default=False, verbose_name='Уведомление отправлено'), field=models.BooleanField(default=False, verbose_name='Уведомление отправлено'),
), ),
] ]

View File

@ -1,180 +1,180 @@
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.core.cache import cache from django.core.cache import cache
from .utils.email_notifications import send_callback_notification from .utils.email_notifications import send_callback_notification
class Recall(models.Model): class Recall(models.Model):
title = models.CharField(max_length=255, verbose_name='Организация') title = models.CharField(max_length=255, verbose_name='Организация')
content = models.TextField(blank=True, verbose_name='Отзыв') content = models.TextField(blank=True, verbose_name='Отзыв')
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото') scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован') is_published = models.BooleanField(default=True, verbose_name='Опубликован')
def __str__(self): def __str__(self):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse('post', kwargs={'post_id': self.pk}) return reverse('post', kwargs={'post_id': self.pk})
class Meta: class Meta:
verbose_name = 'Отзыв' verbose_name = 'Отзыв'
verbose_name_plural = 'Отзывы' verbose_name_plural = 'Отзывы'
ordering = ['time_create', 'title'] ordering = ['time_create', 'title']
def get_seo_title(self): def get_seo_title(self):
return f"Отзыв от {self.title} | Программист 1С" return f"Отзыв от {self.title} | Программист 1С"
def get_seo_description(self): def get_seo_description(self):
if self.content: if self.content:
clean_content = self.content[:160].replace('\n', ' ').strip() clean_content = self.content[:160].replace('\n', ' ').strip()
return f"Отзыв о работе программиста 1С от {self.title}. {clean_content}..." return f"Отзыв о работе программиста 1С от {self.title}. {clean_content}..."
return f"Отзыв клиента {self.title} о работе программиста 1С Николая Сердюк" return f"Отзыв клиента {self.title} о работе программиста 1С Николая Сердюк"
class Competence(models.Model): class Competence(models.Model):
title = models.CharField(max_length=255, verbose_name='Программист') title = models.CharField(max_length=255, verbose_name='Программист')
content = models.TextField(blank=True, verbose_name='Компетенция') content = models.TextField(blank=True, verbose_name='Компетенция')
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото') photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован') is_published = models.BooleanField(default=True, verbose_name='Опубликован')
def __str__(self): def __str__(self):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse('post', kwargs={'post_id': self.pk}) return reverse('post', kwargs={'post_id': self.pk})
class Meta: class Meta:
verbose_name = 'Компетенция' verbose_name = 'Компетенция'
verbose_name_plural = 'Компетенции' verbose_name_plural = 'Компетенции'
ordering = ['time_create', 'title'] ordering = ['time_create', 'title']
class Solution(models.Model): class Solution(models.Model):
title = models.CharField(max_length=255, verbose_name='Наименование') title = models.CharField(max_length=255, verbose_name='Наименование')
description = models.TextField(blank=True, verbose_name='Описание') description = models.TextField(blank=True, verbose_name='Описание')
implementation = models.TextField(blank=True, verbose_name='Реализация') implementation = models.TextField(blank=True, verbose_name='Реализация')
closing = models.TextField(blank=True, verbose_name='Заключение') closing = models.TextField(blank=True, verbose_name='Заключение')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован') is_published = models.BooleanField(default=True, verbose_name='Опубликован')
def __str__(self): def __str__(self):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse('post', kwargs={'post_id': self.pk}) return reverse('post', kwargs={'post_id': self.pk})
class Meta: class Meta:
verbose_name = 'Проекты' verbose_name = 'Проекты'
verbose_name_plural = 'Проекты' verbose_name_plural = 'Проекты'
ordering = ['time_create', 'title'] ordering = ['time_create', 'title']
def get_seo_title(self): def get_seo_title(self):
"""Генерирует SEO-заголовок для проекта""" """Генерирует SEO-заголовок для проекта"""
return f"Проект: {self.title} | Автоматизация 1С" return f"Проект: {self.title} | Автоматизация 1С"
def get_seo_description(self): def get_seo_description(self):
"""Генерирует SEO-описание для проекта""" """Генерирует SEO-описание для проекта"""
if self.description: if self.description:
clean_desc = self.description[:160].replace('\n', ' ').strip() clean_desc = self.description[:160].replace('\n', ' ').strip()
return f"Проект автоматизации: {self.title}. {clean_desc}..." return f"Проект автоматизации: {self.title}. {clean_desc}..."
return f"Реализация проекта {self.title} - программист 1С Николай Сердюк" return f"Реализация проекта {self.title} - программист 1С Николай Сердюк"
def get_meta_keywords(self): def get_meta_keywords(self):
"""Автоматические ключевые слова для проекта""" """Автоматические ключевые слова для проекта"""
base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"] base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"]
title_words = self.title.lower().split() title_words = self.title.lower().split()
return base_keywords + title_words return base_keywords + title_words
class Home(models.Model): class Home(models.Model):
title = models.CharField(max_length=255, verbose_name='Наименование') title = models.CharField(max_length=255, verbose_name='Наименование')
content = models.TextField(blank=True, verbose_name='Статья') content = models.TextField(blank=True, verbose_name='Статья')
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", 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_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован') is_published = models.BooleanField(default=True, verbose_name='Опубликован')
def __str__(self): def __str__(self):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse('post', kwargs={'post_id': self.pk}) return reverse('post', kwargs={'post_id': self.pk})
class Meta: class Meta:
verbose_name = 'Главная страница' verbose_name = 'Главная страница'
verbose_name_plural = 'Главная страница' verbose_name_plural = 'Главная страница'
ordering = ['time_create', 'title'] ordering = ['time_create', 'title']
class CallbackRequest(models.Model): class CallbackRequest(models.Model):
name = models.CharField(max_length=100, verbose_name='Имя') name = models.CharField(max_length=100, verbose_name='Имя')
phone = models.CharField(max_length=20, verbose_name='Телефон') phone = models.CharField(max_length=20, verbose_name='Телефон')
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
is_processed = models.BooleanField(default=False, verbose_name='Обработано') is_processed = models.BooleanField(default=False, verbose_name='Обработано')
is_read = models.BooleanField(default=False, verbose_name='Прочитано') is_read = models.BooleanField(default=False, verbose_name='Прочитано')
notification_sent = models.BooleanField(default=False, verbose_name='Уведомление отправлено') notification_sent = models.BooleanField(default=False, verbose_name='Уведомление отправлено')
def __str__(self): def __str__(self):
return f"{self.name} - {self.phone}" return f"{self.name} - {self.phone}"
class Meta: class Meta:
verbose_name = 'Заявка на звонок' verbose_name = 'Заявка на звонок'
verbose_name_plural = 'Заявки на звонок' verbose_name_plural = 'Заявки на звонок'
ordering = ['-time_create'] ordering = ['-time_create']
# Сигнал для отправки уведомления при создании заявки # Сигнал для отправки уведомления при создании заявки
@receiver(post_save, sender=CallbackRequest) @receiver(post_save, sender=CallbackRequest)
def send_callback_email_notification(sender, instance, created, **kwargs): def send_callback_email_notification(sender, instance, created, **kwargs):
if created and not instance.notification_sent: if created and not instance.notification_sent:
# Отправляем email уведомление # Отправляем email уведомление
success = send_callback_notification(instance) success = send_callback_notification(instance)
if success: if success:
instance.notification_sent = True instance.notification_sent = True
# Сохраняем без повторного вызова сигнала # Сохраняем без повторного вызова сигнала
sender.objects.filter(pk=instance.pk).update(notification_sent=True) sender.objects.filter(pk=instance.pk).update(notification_sent=True)
class PageView(models.Model): class PageView(models.Model):
url = models.CharField(max_length=500) url = models.CharField(max_length=500)
timestamp = models.DateTimeField(default=timezone.now) timestamp = models.DateTimeField(default=timezone.now)
ip_address = models.GenericIPAddressField() ip_address = models.GenericIPAddressField()
user_agent = models.TextField(blank=True) user_agent = models.TextField(blank=True)
referer = models.CharField(max_length=500, blank=True) referer = models.CharField(max_length=500, blank=True)
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['url', 'timestamp']), models.Index(fields=['url', 'timestamp']),
models.Index(fields=['timestamp']), models.Index(fields=['timestamp']),
] ]
class Visitor(models.Model): class Visitor(models.Model):
ip_address = models.GenericIPAddressField() ip_address = models.GenericIPAddressField()
first_visit = models.DateTimeField(default=timezone.now) first_visit = models.DateTimeField(default=timezone.now)
last_visit = models.DateTimeField(default=timezone.now) last_visit = models.DateTimeField(default=timezone.now)
visit_count = models.IntegerField(default=1) visit_count = models.IntegerField(default=1)
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['ip_address']), models.Index(fields=['ip_address']),
] ]
@receiver([post_save, post_delete], sender=Home) @receiver([post_save, post_delete], sender=Home)
@receiver([post_save, post_delete], sender=Solution) @receiver([post_save, post_delete], sender=Solution)
@receiver([post_save, post_delete], sender=Competence) @receiver([post_save, post_delete], sender=Competence)
@receiver([post_save, post_delete], sender=Recall) @receiver([post_save, post_delete], sender=Recall)
def clear_sitemap_cache(sender, **kwargs): def clear_sitemap_cache(sender, **kwargs):
"""Очищаем кэш sitemap при изменении контента""" """Очищаем кэш sitemap при изменении контента"""
cache.delete('sitemap_cache') cache.delete('sitemap_cache')

View File

@ -1,64 +1,64 @@
from django.contrib.sitemaps import Sitemap from django.contrib.sitemaps import Sitemap
from django.urls import reverse from django.urls import reverse
from .models import Home, Solution, Competence, Recall from .models import Home, Solution, Competence, Recall
class StaticViewSitemap(Sitemap): class StaticViewSitemap(Sitemap):
priority = 1.0 priority = 1.0
changefreq = 'monthly' changefreq = 'monthly'
def items(self): def items(self):
return ['home', 'about', 'solution', 'ability', 'recall'] return ['home', 'about', 'solution', 'ability', 'recall']
def location(self, item): def location(self, item):
return reverse(item) return reverse(item)
class HomeSitemap(Sitemap): class HomeSitemap(Sitemap):
changefreq = 'weekly' changefreq = 'weekly'
priority = 1.0 priority = 1.0
def items(self): def items(self):
return Home.objects.filter(is_published=True) return Home.objects.filter(is_published=True)
def lastmod(self, obj): def lastmod(self, obj):
return obj.time_update return obj.time_update
# УБИРАЕМ метод location - используем default # УБИРАЕМ метод location - используем default
# Django автоматически сгенерирует правильные URL # Django автоматически сгенерирует правильные URL
class SolutionSitemap(Sitemap): class SolutionSitemap(Sitemap):
changefreq = 'weekly' changefreq = 'weekly'
priority = 0.9 priority = 0.9
def items(self): def items(self):
return Solution.objects.filter(is_published=True) return Solution.objects.filter(is_published=True)
def lastmod(self, obj): def lastmod(self, obj):
return obj.time_update return obj.time_update
class CompetenceSitemap(Sitemap): class CompetenceSitemap(Sitemap):
changefreq = 'monthly' changefreq = 'monthly'
priority = 0.8 priority = 0.8
def items(self): def items(self):
return Competence.objects.filter(is_published=True) return Competence.objects.filter(is_published=True)
def lastmod(self, obj): def lastmod(self, obj):
return obj.time_update return obj.time_update
class RecallSitemap(Sitemap): class RecallSitemap(Sitemap):
changefreq = 'monthly' changefreq = 'monthly'
priority = 0.7 priority = 0.7
def items(self): def items(self):
return Recall.objects.filter(is_published=True) return Recall.objects.filter(is_published=True)
def lastmod(self, obj): def lastmod(self, obj):
return obj.time_update return obj.time_update
# Упрощаем sitemaps - убираем HomeSitemap если он дублирует главную # Упрощаем sitemaps - убираем HomeSitemap если он дублирует главную
sitemaps = { sitemaps = {
'static': StaticViewSitemap, 'static': StaticViewSitemap,
'solutions': SolutionSitemap, 'solutions': SolutionSitemap,
'competence': CompetenceSitemap, 'competence': CompetenceSitemap,
'recall': RecallSitemap, 'recall': RecallSitemap,
} }

View File

@ -1,299 +1,299 @@
/* competence.css - Стили для страницы компетенций */ /* competence.css - Стили для страницы компетенций */
/* Основные стили для страницы компетенций */ /* Основные стили для страницы компетенций */
.competence-item { .competence-item {
display: flex; display: flex;
gap: 2rem; gap: 2rem;
align-items: flex-start; align-items: flex-start;
padding: 2rem; padding: 2rem;
background: var(--bg-card); background: var(--bg-card);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
border-left: 4px solid var(--secondary); border-left: 4px solid var(--secondary);
transition: var(--transition); transition: var(--transition);
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
} }
.competence-item:hover { .competence-item:hover {
transform: translateX(8px); transform: translateX(8px);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
border-color: var(--primary-light); border-color: var(--primary-light);
} }
.competence-scan-wrapper { .competence-scan-wrapper {
flex-shrink: 0; flex-shrink: 0;
} }
.competence-scan-container { .competence-scan-container {
width: 280px; width: 280px;
cursor: pointer; cursor: pointer;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
transition: var(--transition); transition: var(--transition);
position: relative; position: relative;
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.competence-scan-container:hover { .competence-scan-container:hover {
transform: translateY(-4px) scale(1.02); transform: translateY(-4px) scale(1.02);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
border-color: var(--primary); border-color: var(--primary);
} }
.competence-scan { .competence-scan {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
transition: var(--transition); transition: var(--transition);
} }
.competence-content { .competence-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.competence-title { .competence-title {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.competence-description { .competence-description {
line-height: 1.7; line-height: 1.7;
font-size: 1.05rem; font-size: 1.05rem;
color: var(--text-primary); color: var(--text-primary);
} }
.competence-description p { .competence-description p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.competence-description p:last-child { .competence-description p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.scan-hint { .scan-hint {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8)); background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white; color: white;
padding: 1rem; padding: 1rem;
text-align: center; text-align: center;
opacity: 0; opacity: 0;
transition: var(--transition); transition: var(--transition);
transform: translateY(10px); transform: translateY(10px);
} }
.competence-scan-container:hover .scan-hint { .competence-scan-container:hover .scan-hint {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
/* Стили для модального окна с изображением компетенций */ /* Стили для модального окна с изображением компетенций */
.modal.competence-modal { .modal.competence-modal {
background-color: rgba(15, 19, 31, 0.95); background-color: rgba(15, 19, 31, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
max-width: 95vw; max-width: 95vw;
max-height: 95vh; max-height: 95vh;
margin: 2% auto; margin: 2% auto;
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
background: var(--bg-card); background: var(--bg-card);
border-bottom: 2px solid var(--border-light); border-bottom: 2px solid var(--border-light);
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
} }
.modal.competence-modal .modal-header h3 { .modal.competence-modal .modal-header h3 {
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.modal.competence-modal .modal-body { .modal.competence-modal .modal-body {
padding: 1rem; padding: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
} }
.modal-image { .modal-image {
max-width: 90vw; max-width: 90vw;
max-height: 80vh; max-height: 80vh;
width: auto; width: auto;
height: auto; height: auto;
display: block; display: block;
margin: 0 auto; margin: 0 auto;
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Анимации для модального окна */ /* Анимации для модального окна */
.modal.competence-modal { .modal.competence-modal {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
opacity: 0; opacity: 0;
} }
.modal.competence-modal.active { .modal.competence-modal.active {
opacity: 1; opacity: 1;
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
transform: scale(0.7); transform: scale(0.7);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.modal.competence-modal.active .modal-content { .modal.competence-modal.active .modal-content {
transform: scale(1); transform: scale(1);
} }
/* Улучшенные тени и границы */ /* Улучшенные тени и границы */
.competence-scan-container { .competence-scan-container {
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
.competence-scan-container:hover { .competence-scan-container:hover {
border-color: var(--primary-light); border-color: var(--primary-light);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
} }
/* Адаптивность для мобильных устройств */ /* Адаптивность для мобильных устройств */
@media (max-width: 768px) { @media (max-width: 768px) {
.competence-item { .competence-item {
flex-direction: column; flex-direction: column;
padding: 1.5rem; padding: 1.5rem;
gap: 1.5rem; gap: 1.5rem;
} }
.competence-scan-container { .competence-scan-container {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
} }
.competence-title { .competence-title {
font-size: 1.375rem; font-size: 1.375rem;
text-align: center; text-align: center;
} }
.modal-image { .modal-image {
max-width: 95vw; max-width: 95vw;
max-height: 70vh; max-height: 70vh;
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
margin: 10% auto; margin: 10% auto;
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.competence-item { .competence-item {
padding: 1.25rem; padding: 1.25rem;
} }
.competence-scan-container { .competence-scan-container {
max-width: 100%; max-width: 100%;
} }
.competence-title { .competence-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
margin: 5% auto; margin: 5% auto;
max-width: 98vw; max-width: 98vw;
} }
.modal.competence-modal .modal-body { .modal.competence-modal .modal-body {
padding: 0.5rem; padding: 0.5rem;
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
padding: 1rem; padding: 1rem;
} }
.modal.competence-modal .modal-header h3 { .modal.competence-modal .modal-header h3 {
font-size: 1.1rem; font-size: 1.1rem;
} }
} }
/* Стили для светлой темы */ /* Стили для светлой темы */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
.modal.competence-modal { .modal.competence-modal {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
background: var(--bg-primary); background: var(--bg-primary);
} }
.scan-hint { .scan-hint {
background: linear-gradient(transparent, rgba(0,0,0,0.7)); background: linear-gradient(transparent, rgba(0,0,0,0.7));
} }
.competence-item { .competence-item {
background: var(--bg-primary); background: var(--bg-primary);
} }
.competence-description { .competence-description {
color: var(--text-primary); color: var(--text-primary);
} }
} }
/* Улучшенные стили для сетки компетенций */ /* Улучшенные стили для сетки компетенций */
.competence-grid { .competence-grid {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
} }
.competence-grid .modern-card { .competence-grid .modern-card {
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
.competence-grid .modern-card::before { .competence-grid .modern-card::before {
height: 4px; height: 4px;
background: var(--gradient-secondary); background: var(--gradient-secondary);
} }
/* Анимации появления */ /* Анимации появления */
.fade-in { .fade-in {
animation: fadeInUp 0.8s ease-out; animation: fadeInUp 0.8s ease-out;
} }
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(40px); transform: translateY(40px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }

View File

@ -1,228 +1,228 @@
/* recall.css - Стили для страницы отзывов */ /* recall.css - Стили для страницы отзывов */
/* Основные стили для страницы отзывов */ /* Основные стили для страницы отзывов */
.recall-item { .recall-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.recall-content { .recall-content {
display: flex; display: flex;
gap: 2rem; gap: 2rem;
align-items: flex-start; align-items: flex-start;
} }
.recall-scan-wrapper { .recall-scan-wrapper {
flex-shrink: 0; flex-shrink: 0;
} }
.recall-scan-container { .recall-scan-container {
width: 280px; width: 280px;
cursor: pointer; cursor: pointer;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
transition: var(--transition); transition: var(--transition);
position: relative; position: relative;
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.recall-scan-container:hover { .recall-scan-container:hover {
transform: translateY(-4px) scale(1.02); transform: translateY(-4px) scale(1.02);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
border-color: var(--primary); border-color: var(--primary);
} }
.recall-scan { .recall-scan {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
transition: var(--transition); transition: var(--transition);
} }
.recall-text { .recall-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
line-height: 1.7; line-height: 1.7;
font-size: 1.05rem; font-size: 1.05rem;
color: var(--text-primary); color: var(--text-primary);
} }
.recall-text p { .recall-text p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.recall-text p:last-child { .recall-text p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.scan-hint { .scan-hint {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8)); background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white; color: white;
padding: 1rem; padding: 1rem;
text-align: center; text-align: center;
opacity: 0; opacity: 0;
transition: var(--transition); transition: var(--transition);
transform: translateY(10px); transform: translateY(10px);
} }
.recall-scan-container:hover .scan-hint { .recall-scan-container:hover .scan-hint {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
/* Стили для модального окна с изображением */ /* Стили для модального окна с изображением */
.modal.image-modal { .modal.image-modal {
background-color: rgba(15, 19, 31, 0.95); background-color: rgba(15, 19, 31, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
max-width: 95vw; max-width: 95vw;
max-height: 95vh; max-height: 95vh;
margin: 2% auto; margin: 2% auto;
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
background: var(--bg-card); background: var(--bg-card);
border-bottom: 2px solid var(--border-light); border-bottom: 2px solid var(--border-light);
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
} }
.modal.image-modal .modal-header h3 { .modal.image-modal .modal-header h3 {
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.modal.image-modal .modal-body { .modal.image-modal .modal-body {
padding: 1rem; padding: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
} }
.modal-image { .modal-image {
max-width: 90vw; max-width: 90vw;
max-height: 80vh; max-height: 80vh;
width: auto; width: auto;
height: auto; height: auto;
display: block; display: block;
margin: 0 auto; margin: 0 auto;
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Анимации для модального окна */ /* Анимации для модального окна */
.modal.image-modal { .modal.image-modal {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
opacity: 0; opacity: 0;
} }
.modal.image-modal.active { .modal.image-modal.active {
opacity: 1; opacity: 1;
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
transform: scale(0.7); transform: scale(0.7);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.modal.image-modal.active .modal-content { .modal.image-modal.active .modal-content {
transform: scale(1); transform: scale(1);
} }
/* Улучшенные тени и границы */ /* Улучшенные тени и границы */
.recall-scan-container { .recall-scan-container {
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
.recall-scan-container:hover { .recall-scan-container:hover {
border-color: var(--primary-light); border-color: var(--primary-light);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
} }
/* Адаптивность для мобильных устройств */ /* Адаптивность для мобильных устройств */
@media (max-width: 768px) { @media (max-width: 768px) {
.recall-content { .recall-content {
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.recall-scan-container { .recall-scan-container {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
} }
.recall-scan-wrapper { .recall-scan-wrapper {
order: -1; order: -1;
} }
.modal-image { .modal-image {
max-width: 95vw; max-width: 95vw;
max-height: 70vh; max-height: 70vh;
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
margin: 10% auto; margin: 10% auto;
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.recall-scan-container { .recall-scan-container {
max-width: 100%; max-width: 100%;
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
margin: 5% auto; margin: 5% auto;
max-width: 98vw; max-width: 98vw;
} }
.modal.image-modal .modal-body { .modal.image-modal .modal-body {
padding: 0.5rem; padding: 0.5rem;
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
padding: 1rem; padding: 1rem;
} }
.modal.image-modal .modal-header h3 { .modal.image-modal .modal-header h3 {
font-size: 1.1rem; font-size: 1.1rem;
} }
} }
/* Стили для светлой темы */ /* Стили для светлой темы */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
.modal.image-modal { .modal.image-modal {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
background: var(--bg-primary); background: var(--bg-primary);
} }
.scan-hint { .scan-hint {
background: linear-gradient(transparent, rgba(0,0,0,0.7)); background: linear-gradient(transparent, rgba(0,0,0,0.7));
} }
} }

View File

@ -1,67 +1,67 @@
/* solution-accordion.css */ /* solution-accordion.css */
.solution-accordion { .solution-accordion {
margin: 2rem 0; margin: 2rem 0;
} }
.accordion-item { .accordion-item {
background: var(--bg-primary); background: var(--bg-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
margin-bottom: 1rem; margin-bottom: 1rem;
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
overflow: hidden; overflow: hidden;
transition: var(--transition); transition: var(--transition);
} }
.accordion-item:hover { .accordion-item:hover {
border-color: var(--primary-light); border-color: var(--primary-light);
} }
.accordion-header { .accordion-header {
padding: 1.5rem; padding: 1.5rem;
background: var(--gradient-primary); background: var(--gradient-primary);
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 1.125rem; font-size: 1.125rem;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
transition: var(--transition); transition: var(--transition);
user-select: none; user-select: none;
} }
.accordion-header:hover { .accordion-header:hover {
background: var(--primary-dark); background: var(--primary-dark);
} }
.accordion-content { .accordion-content {
padding: 0; padding: 0;
background: var(--bg-card); background: var(--bg-card);
line-height: 1.7; line-height: 1.7;
color: var(--text-secondary); color: var(--text-secondary);
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.accordion-content.active { .accordion-content.active {
padding: 1.5rem; padding: 1.5rem;
max-height: 5000px; max-height: 5000px;
} }
.accordion-icon { .accordion-icon {
transition: transform 0.3s ease; transition: transform 0.3s ease;
font-size: 0.8em; font-size: 0.8em;
} }
.accordion-header.active .accordion-icon { .accordion-header.active .accordion-icon {
transform: rotate(180deg); transform: rotate(180deg);
} }
.accordion-content p { .accordion-content p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.accordion-content p:last-child { .accordion-content p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +1,73 @@
// competence.js - Скрипты для страницы компетенций // competence.js - Скрипты для страницы компетенций
function openCompetenceModal(imageUrl, title) { function openCompetenceModal(imageUrl, title) {
console.log('Opening competence modal with:', imageUrl); console.log('Opening competence modal with:', imageUrl);
const modal = document.getElementById('competenceModal'); const modal = document.getElementById('competenceModal');
const modalImg = document.getElementById('competenceModalImage'); const modalImg = document.getElementById('competenceModalImage');
const modalTitle = document.getElementById('competenceModalTitle'); const modalTitle = document.getElementById('competenceModalTitle');
if (modal && modalImg) { if (modal && modalImg) {
modal.style.display = "block"; modal.style.display = "block";
modalImg.src = imageUrl; modalImg.src = imageUrl;
if (title && modalTitle) { if (title && modalTitle) {
modalTitle.textContent = title; modalTitle.textContent = title;
} }
// Добавляем класс для анимации // Добавляем класс для анимации
setTimeout(() => { setTimeout(() => {
modal.classList.add('active'); modal.classList.add('active');
}, 10); }, 10);
// Подстраиваем размер изображения // Подстраиваем размер изображения
adjustCompetenceModalImageSize(); adjustCompetenceModalImageSize();
} }
} }
function closeCompetenceModal() { function closeCompetenceModal() {
const modal = document.getElementById('competenceModal'); const modal = document.getElementById('competenceModal');
if (modal) { if (modal) {
modal.classList.remove('active'); modal.classList.remove('active');
setTimeout(() => { setTimeout(() => {
modal.style.display = "none"; modal.style.display = "none";
}, 300); }, 300);
} }
} }
function adjustCompetenceModalImageSize() { function adjustCompetenceModalImageSize() {
const modalImg = document.getElementById('competenceModalImage'); const modalImg = document.getElementById('competenceModalImage');
if (modalImg) { if (modalImg) {
const maxWidth = window.innerWidth * 0.9; const maxWidth = window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.8; const maxHeight = window.innerHeight * 0.8;
modalImg.style.maxWidth = `${maxWidth}px`; modalImg.style.maxWidth = `${maxWidth}px`;
modalImg.style.maxHeight = `${maxHeight}px`; modalImg.style.maxHeight = `${maxHeight}px`;
} }
} }
// Инициализация после загрузки DOM // Инициализация после загрузки DOM
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Закрытие модального окна при клике вне изображения // Закрытие модального окна при клике вне изображения
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const modal = document.getElementById('competenceModal'); const modal = document.getElementById('competenceModal');
if (event.target === modal) { if (event.target === modal) {
closeCompetenceModal(); closeCompetenceModal();
} }
}); });
// Закрытие по ESC // Закрытие по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeCompetenceModal(); closeCompetenceModal();
} }
}); });
// Адаптация размера изображения при изменении размера окна // Адаптация размера изображения при изменении размера окна
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
const modalImg = document.getElementById('competenceModalImage'); const modalImg = document.getElementById('competenceModalImage');
if (modalImg && modalImg.src) { if (modalImg && modalImg.src) {
adjustCompetenceModalImageSize(); adjustCompetenceModalImageSize();
} }
}); });
console.log('Competence page scripts initialized'); console.log('Competence page scripts initialized');
}); });

View File

@ -1,94 +1,94 @@
// Mobile Menu Script // Mobile Menu Script
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('Mobile menu script loaded'); // Для отладки console.log('Mobile menu script loaded'); // Для отладки
const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenuClose = document.getElementById('mobileMenuClose'); const mobileMenuClose = document.getElementById('mobileMenuClose');
const mobileMenuOverlay = document.getElementById('mobileMenuOverlay'); const mobileMenuOverlay = document.getElementById('mobileMenuOverlay');
const mobileMenu = document.getElementById('mobileMenu'); const mobileMenu = document.getElementById('mobileMenu');
const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); const mobileThemeToggle = document.getElementById('mobile-theme-toggle');
const mainThemeToggle = document.getElementById('theme-toggle'); const mainThemeToggle = document.getElementById('theme-toggle');
// Проверяем, что элементы существуют // Проверяем, что элементы существуют
if (!mobileMenuBtn || !mobileMenu) { if (!mobileMenuBtn || !mobileMenu) {
console.error('Mobile menu elements not found'); console.error('Mobile menu elements not found');
return; return;
} }
console.log('Mobile menu elements found:', { console.log('Mobile menu elements found:', {
mobileMenuBtn, mobileMenuBtn,
mobileMenuClose, mobileMenuClose,
mobileMenuOverlay, mobileMenuOverlay,
mobileMenu, mobileMenu,
mobileThemeToggle, mobileThemeToggle,
mainThemeToggle mainThemeToggle
}); });
// Открытие мобильного меню // Открытие мобильного меню
mobileMenuBtn.addEventListener('click', function() { mobileMenuBtn.addEventListener('click', function() {
console.log('Opening mobile menu'); console.log('Opening mobile menu');
mobileMenu.classList.add('active'); mobileMenu.classList.add('active');
mobileMenuOverlay.style.display = 'block'; mobileMenuOverlay.style.display = 'block';
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
}); });
// Закрытие мобильного меню // Закрытие мобильного меню
function closeMobileMenu() { function closeMobileMenu() {
console.log('Closing mobile menu'); console.log('Closing mobile menu');
mobileMenu.classList.remove('active'); mobileMenu.classList.remove('active');
mobileMenuOverlay.style.display = 'none'; mobileMenuOverlay.style.display = 'none';
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
if (mobileMenuClose) { if (mobileMenuClose) {
mobileMenuClose.addEventListener('click', closeMobileMenu); mobileMenuClose.addEventListener('click', closeMobileMenu);
} }
if (mobileMenuOverlay) { if (mobileMenuOverlay) {
mobileMenuOverlay.addEventListener('click', closeMobileMenu); mobileMenuOverlay.addEventListener('click', closeMobileMenu);
} }
// Закрытие меню при клике на ссылку // Закрытие меню при клике на ссылку
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link'); const mobileNavLinks = document.querySelectorAll('.mobile-nav-link');
mobileNavLinks.forEach(link => { mobileNavLinks.forEach(link => {
link.addEventListener('click', closeMobileMenu); link.addEventListener('click', closeMobileMenu);
}); });
// Синхронизация переключателей темы // Синхронизация переключателей темы
function syncThemeToggles() { function syncThemeToggles() {
if (mobileThemeToggle && mainThemeToggle) { if (mobileThemeToggle && mainThemeToggle) {
mobileThemeToggle.checked = mainThemeToggle.checked; mobileThemeToggle.checked = mainThemeToggle.checked;
} }
} }
if (mainThemeToggle) { if (mainThemeToggle) {
mainThemeToggle.addEventListener('change', function() { mainThemeToggle.addEventListener('change', function() {
console.log('Main theme toggle changed:', this.checked); console.log('Main theme toggle changed:', this.checked);
syncThemeToggles(); syncThemeToggles();
}); });
} }
if (mobileThemeToggle) { if (mobileThemeToggle) {
mobileThemeToggle.addEventListener('change', function() { mobileThemeToggle.addEventListener('change', function() {
console.log('Mobile theme toggle changed:', this.checked); console.log('Mobile theme toggle changed:', this.checked);
if (mainThemeToggle) { if (mainThemeToggle) {
mainThemeToggle.checked = this.checked; mainThemeToggle.checked = this.checked;
// Триггерим событие change // Триггерим событие change
const event = new Event('change'); const event = new Event('change');
mainThemeToggle.dispatchEvent(event); mainThemeToggle.dispatchEvent(event);
} }
}); });
} }
// Инициализация синхронизации // Инициализация синхронизации
syncThemeToggles(); syncThemeToggles();
// Закрытие меню по ESC // Закрытие меню по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeMobileMenu(); closeMobileMenu();
} }
}); });
console.log('Mobile menu script initialized successfully'); console.log('Mobile menu script initialized successfully');
}); });

View File

@ -1,74 +1,74 @@
// recall.js - Скрипты для страницы отзывов // recall.js - Скрипты для страницы отзывов
function openModal(imageUrl, title) { function openModal(imageUrl, title) {
console.log('Opening modal with:', imageUrl); console.log('Opening modal with:', imageUrl);
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
if (modal && modalImg) { if (modal && modalImg) {
modal.style.display = "block"; modal.style.display = "block";
modalImg.src = imageUrl; modalImg.src = imageUrl;
if (title && modalTitle) { if (title && modalTitle) {
modalTitle.textContent = title; modalTitle.textContent = title;
} }
// Добавляем класс для анимации // Добавляем класс для анимации
setTimeout(() => { setTimeout(() => {
modal.classList.add('active'); modal.classList.add('active');
}, 10); }, 10);
// Подстраиваем размер изображения // Подстраиваем размер изображения
adjustModalImageSize(); adjustModalImageSize();
} }
} }
function closeModal() { function closeModal() {
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
if (modal) { if (modal) {
modal.classList.remove('active'); modal.classList.remove('active');
setTimeout(() => { setTimeout(() => {
modal.style.display = "none"; modal.style.display = "none";
}, 300); }, 300);
} }
} }
function adjustModalImageSize() { function adjustModalImageSize() {
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
const modalContent = document.querySelector('.modal-content'); const modalContent = document.querySelector('.modal-content');
if (modalImg && modalContent) { if (modalImg && modalContent) {
const maxWidth = window.innerWidth * 0.9; const maxWidth = window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.8; const maxHeight = window.innerHeight * 0.8;
modalImg.style.maxWidth = `${maxWidth}px`; modalImg.style.maxWidth = `${maxWidth}px`;
modalImg.style.maxHeight = `${maxHeight}px`; modalImg.style.maxHeight = `${maxHeight}px`;
} }
} }
// Инициализация после загрузки DOM // Инициализация после загрузки DOM
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Закрытие модального окна при клике вне изображения // Закрытие модального окна при клике вне изображения
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
if (event.target === modal) { if (event.target === modal) {
closeModal(); closeModal();
} }
}); });
// Закрытие по ESC // Закрытие по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeModal(); closeModal();
} }
}); });
// Адаптация размера изображения при изменении размера окна // Адаптация размера изображения при изменении размера окна
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
if (modalImg && modalImg.src) { if (modalImg && modalImg.src) {
adjustModalImageSize(); adjustModalImageSize();
} }
}); });
console.log('Recall page scripts initialized'); console.log('Recall page scripts initialized');
}); });

View File

@ -1,46 +1,46 @@
// solution-accordion.js // solution-accordion.js
function toggleAccordion(header) { function toggleAccordion(header) {
const content = header.nextElementSibling; const content = header.nextElementSibling;
const icon = header.querySelector('.accordion-icon'); const icon = header.querySelector('.accordion-icon');
// Переключаем только текущий аккордеон // Переключаем только текущий аккордеон
header.classList.toggle('active'); header.classList.toggle('active');
content.classList.toggle('active'); content.classList.toggle('active');
icon.style.transform = header.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)'; icon.style.transform = header.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)';
} }
// Функция для открытия всех аккордеонов // Функция для открытия всех аккордеонов
function expandAll() { function expandAll() {
document.querySelectorAll('.accordion-content').forEach(content => { document.querySelectorAll('.accordion-content').forEach(content => {
content.classList.add('active'); content.classList.add('active');
}); });
document.querySelectorAll('.accordion-header').forEach(header => { document.querySelectorAll('.accordion-header').forEach(header => {
header.classList.add('active'); header.classList.add('active');
}); });
document.querySelectorAll('.accordion-icon').forEach(icon => { document.querySelectorAll('.accordion-icon').forEach(icon => {
icon.style.transform = 'rotate(180deg)'; icon.style.transform = 'rotate(180deg)';
}); });
} }
// Функция для закрытия всех аккордеонов // Функция для закрытия всех аккордеонов
function collapseAll() { function collapseAll() {
document.querySelectorAll('.accordion-content').forEach(content => { document.querySelectorAll('.accordion-content').forEach(content => {
content.classList.remove('active'); content.classList.remove('active');
}); });
document.querySelectorAll('.accordion-header').forEach(header => { document.querySelectorAll('.accordion-header').forEach(header => {
header.classList.remove('active'); header.classList.remove('active');
}); });
document.querySelectorAll('.accordion-icon').forEach(icon => { document.querySelectorAll('.accordion-icon').forEach(icon => {
icon.style.transform = 'rotate(0deg)'; icon.style.transform = 'rotate(0deg)';
}); });
} }
// Автоматически открываем первый аккордеон в каждой карточке при загрузке // Автоматически открываем первый аккордеон в каждой карточке при загрузке
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.content-card').forEach(card => { document.querySelectorAll('.content-card').forEach(card => {
const firstAccordion = card.querySelector('.accordion-header'); const firstAccordion = card.querySelector('.accordion-header');
if (firstAccordion) { if (firstAccordion) {
toggleAccordion(firstAccordion); toggleAccordion(firstAccordion);
} }
}); });
}); });

View File

@ -1,68 +1,68 @@
// Theme Switcher Script // Theme Switcher Script
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); const mobileThemeToggle = document.getElementById('mobile-theme-toggle');
const themeCSS = document.getElementById('theme-css'); const themeCSS = document.getElementById('theme-css');
// Проверяем сохраненную тему в localStorage // Проверяем сохраненную тему в localStorage
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
// Устанавливаем светлую тему по умолчанию // Устанавливаем светлую тему по умолчанию
if (savedTheme === 'dark') { if (savedTheme === 'dark') {
switchToDarkTheme(); switchToDarkTheme();
} else { } else {
switchToLightTheme(); // Светлая тема по умолчанию switchToLightTheme(); // Светлая тема по умолчанию
} }
// Обработчик переключения темы для десктопного переключателя // Обработчик переключения темы для десктопного переключателя
if (themeToggle) { if (themeToggle) {
themeToggle.addEventListener('change', function() { themeToggle.addEventListener('change', function() {
if (this.checked) { if (this.checked) {
switchToLightTheme(); switchToLightTheme();
} else { } else {
switchToDarkTheme(); switchToDarkTheme();
} }
}); });
} }
// Обработчик переключения темы для мобильного переключателя // Обработчик переключения темы для мобильного переключателя
if (mobileThemeToggle) { if (mobileThemeToggle) {
mobileThemeToggle.addEventListener('change', function() { mobileThemeToggle.addEventListener('change', function() {
if (this.checked) { if (this.checked) {
switchToLightTheme(); switchToLightTheme();
} else { } else {
switchToDarkTheme(); switchToDarkTheme();
} }
// Синхронизируем оба переключателя // Синхронизируем оба переключателя
if (themeToggle) { if (themeToggle) {
themeToggle.checked = this.checked; themeToggle.checked = this.checked;
} }
}); });
} }
function switchToLightTheme() { function switchToLightTheme() {
themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css');
if (themeToggle) themeToggle.checked = true; if (themeToggle) themeToggle.checked = true;
if (mobileThemeToggle) mobileThemeToggle.checked = true; if (mobileThemeToggle) mobileThemeToggle.checked = true;
localStorage.setItem('theme', 'light'); localStorage.setItem('theme', 'light');
} }
function switchToDarkTheme() { function switchToDarkTheme() {
themeCSS.href = themeCSS.href.replace('styles_w.css', 'styles_dark.css'); themeCSS.href = themeCSS.href.replace('styles_w.css', 'styles_dark.css');
if (themeToggle) themeToggle.checked = false; if (themeToggle) themeToggle.checked = false;
if (mobileThemeToggle) mobileThemeToggle.checked = false; if (mobileThemeToggle) mobileThemeToggle.checked = false;
localStorage.setItem('theme', 'dark'); localStorage.setItem('theme', 'dark');
} }
// Синхронизация переключателей при загрузке // Синхронизация переключателей при загрузке
if (themeToggle && mobileThemeToggle) { if (themeToggle && mobileThemeToggle) {
mobileThemeToggle.checked = themeToggle.checked; mobileThemeToggle.checked = themeToggle.checked;
} }
// Обработка ошибок загрузки CSS // Обработка ошибок загрузки CSS
themeCSS.onerror = function() { themeCSS.onerror = function() {
console.error('Ошибка загрузки CSS файла темы'); console.error('Ошибка загрузки CSS файла темы');
// Восстанавливаем светлую тему по умолчанию при ошибке // Восстанавливаем светлую тему по умолчанию при ошибке
themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css');
}; };
}); });

View File

@ -1,79 +1,79 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Админ-панель - Статистика{% endblock %}</title> <title>{% block title %}Админ-панель - Статистика{% endblock %}</title>
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% bootstrap_css %} {% bootstrap_css %}
<style> <style>
body { body {
background: #f8f9fa; background: #f8f9fa;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }
.admin-header { .admin-header {
background: #343a40; background: #343a40;
color: white; color: white;
padding: 1rem 0; padding: 1rem 0;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem; gap: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.stat-card { .stat-card {
background: white; background: white;
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center; text-align: center;
} }
.stat-number { .stat-number {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
color: #007bff; color: #007bff;
margin: 0; margin: 0;
} }
table { table {
width: 100%; width: 100%;
background: white; background: white;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
th, td { th, td {
padding: 1rem; padding: 1rem;
text-align: left; text-align: left;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #dee2e6;
} }
th { th {
background: #f8f9fa; background: #f8f9fa;
font-weight: 600; font-weight: 600;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="admin-header"> <div class="admin-header">
<div class="container"> <div class="container">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h1>{% block page_title %}Админ-панель{% endblock %}</h1> <h1>{% block page_title %}Админ-панель{% endblock %}</h1>
<div> <div>
<a href="{% url 'home' %}" class="btn btn-outline-light btn-sm">На сайт</a> <a href="{% url 'home' %}" class="btn btn-outline-light btn-sm">На сайт</a>
<a href="{% url 'admin:index' %}" class="btn btn-outline-light btn-sm">Django Admin</a> <a href="{% url 'admin:index' %}" class="btn btn-outline-light btn-sm">Django Admin</a>
<a href="{% url 'admin:logout' %}" class="btn btn-outline-light btn-sm">Выйти</a> <a href="{% url 'admin:logout' %}" class="btn btn-outline-light btn-sm">Выйти</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="container"> <div class="container">
{% bootstrap_messages %} {% bootstrap_messages %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>
{% bootstrap_javascript %} {% bootstrap_javascript %}
</body> </body>
</html> </html>

View File

@ -1,26 +1,26 @@
{% extends "admin/base.html" %} {% extends "admin/base.html" %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %} {% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1> <h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
{% endblock %} {% endblock %}
{% block nav-global %} {% block nav-global %}
{% endblock %} {% endblock %}
{% block userlinks %} {% block userlinks %}
{% load programmer_tags %} {% load programmer_tags %}
<!-- Уведомление о новых заявках --> <!-- Уведомление о новых заявках -->
{% get_unread_callbacks as unread_callbacks %} {% get_unread_callbacks as unread_callbacks %}
{% if unread_callbacks %} {% if unread_callbacks %}
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" style="color: #dc3545; font-weight: bold;"> <a href="{% url 'admin:programmer_callbackrequest_changelist' %}" style="color: #dc3545; font-weight: bold;">
🚨 {{ unread_callbacks }} новых заявок 🚨 {{ unread_callbacks }} новых заявок
</a> / </a> /
{% endif %} {% endif %}
<a href="{% url 'callback_stats' %}">📊 Статистика заявок</a> / <a href="{% url 'callback_stats' %}">📊 Статистика заявок</a> /
<a href="{% url 'statistics' %}">📈 Посещения</a> / <a href="{% url 'statistics' %}">📈 Посещения</a> /
{{ block.super }} {{ block.super }}
{% endblock %} {% endblock %}

View File

@ -1,45 +1,45 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% block title %}Статистика заявок{% endblock %} {% block title %}Статистика заявок{% endblock %}
{% block content %} {% block content %}
<div class="module"> <div class="module">
<h1>📊 Статистика заявок на обратный звонок</h1> <h1>📊 Статистика заявок на обратный звонок</h1>
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0;"> <div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0;">
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;"> <div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
<h3>📋 Всего заявок</h3> <h3>📋 Всего заявок</h3>
<p style="font-size: 2rem; font-weight: bold; color: #007bff; margin: 0;">{{ stats.total }}</p> <p style="font-size: 2rem; font-weight: bold; color: #007bff; margin: 0;">{{ stats.total }}</p>
</div> </div>
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;"> <div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
<h3>📅 Сегодня</h3> <h3>📅 Сегодня</h3>
<p style="font-size: 2rem; font-weight: bold; color: #28a745; margin: 0;">{{ stats.today }}</p> <p style="font-size: 2rem; font-weight: bold; color: #28a745; margin: 0;">{{ stats.today }}</p>
</div> </div>
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;"> <div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
<h3>📈 За неделю</h3> <h3>📈 За неделю</h3>
<p style="font-size: 2rem; font-weight: bold; color: #17a2b8; margin: 0;">{{ stats.week }}</p> <p style="font-size: 2rem; font-weight: bold; color: #17a2b8; margin: 0;">{{ stats.week }}</p>
</div> </div>
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;"> <div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
<h3>🆕 Непрочитанные</h3> <h3>🆕 Непрочитанные</h3>
<p style="font-size: 2rem; font-weight: bold; color: #dc3545; margin: 0;">{{ stats.unread }}</p> <p style="font-size: 2rem; font-weight: bold; color: #dc3545; margin: 0;">{{ stats.unread }}</p>
</div> </div>
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;"> <div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
<h3>В обработке</h3> <h3>В обработке</h3>
<p style="font-size: 2rem; font-weight: bold; color: #ffc107; margin: 0;">{{ stats.unprocessed }}</p> <p style="font-size: 2rem; font-weight: bold; color: #ffc107; margin: 0;">{{ stats.unprocessed }}</p>
</div> </div>
</div> </div>
<div style="margin-top: 2rem;"> <div style="margin-top: 2rem;">
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="button" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;"> <a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="button" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">
📋 Перейти к списку заявок 📋 Перейти к списку заявок
</a> </a>
<a href="{% url 'admin:index' %}" class="button" style="background: #6c757d; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin-left: 10px;"> <a href="{% url 'admin:index' %}" class="button" style="background: #6c757d; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin-left: 10px;">
🏠 На главную админки 🏠 На главную админки
</a> </a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,131 +1,131 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% load programmer_tags %} {% load programmer_tags %}
{% block title %}Статистика посещений{% endblock %} {% block title %}Статистика посещений{% endblock %}
{% block page_title %}Статистика посещений{% endblock %} {% block page_title %}Статистика посещений{% endblock %}
{% block content %} {% block content %}
<!-- Уведомления о заявках --> <!-- Уведомления о заявках -->
{% get_unread_callbacks as unread_callbacks %} {% get_unread_callbacks as unread_callbacks %}
{% get_today_callbacks as today_callbacks %} {% get_today_callbacks as today_callbacks %}
{% if unread_callbacks > 0 %} {% if unread_callbacks > 0 %}
<div class="alert alert-warning alert-dismissible fade show" role="alert"> <div class="alert alert-warning alert-dismissible fade show" role="alert">
<h4>🚨 Внимание!</h4> <h4>🚨 Внимание!</h4>
<p> <p>
У вас <strong>{{ unread_callbacks }}</strong> непрочитанных заявок на обратный звонок У вас <strong>{{ unread_callbacks }}</strong> непрочитанных заявок на обратный звонок
{% if today_callbacks > 0 %} {% if today_callbacks > 0 %}
({{ today_callbacks }} из них сегодня) ({{ today_callbacks }} из них сегодня)
{% endif %} {% endif %}
</p> </p>
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="btn btn-warning btn-sm"> <a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="btn btn-warning btn-sm">
📋 Перейти к заявкам 📋 Перейти к заявкам
</a> </a>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endif %} {% endif %}
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<h3>📊 Просмотров сегодня</h3> <h3>📊 Просмотров сегодня</h3>
<p class="stat-number">{{ today_views }}</p> <p class="stat-number">{{ today_views }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>📈 Просмотров за неделю</h3> <h3>📈 Просмотров за неделю</h3>
<p class="stat-number">{{ weekly_views }}</p> <p class="stat-number">{{ weekly_views }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>👥 Уникальных посетителей</h3> <h3>👥 Уникальных посетителей</h3>
<p class="stat-number">{{ unique_visitors }}</p> <p class="stat-number">{{ unique_visitors }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>🕒 Всего просмотров</h3> <h3>🕒 Всего просмотров</h3>
<p class="stat-number">{{ total_views }}</p> <p class="stat-number">{{ total_views }}</p>
</div> </div>
<!-- Добавляем статистику по заявкам --> <!-- Добавляем статистику по заявкам -->
<div class="stat-card"> <div class="stat-card">
<h3>📞 Заявок сегодня</h3> <h3>📞 Заявок сегодня</h3>
<p class="stat-number">{{ today_callbacks }}</p> <p class="stat-number">{{ today_callbacks }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>📋 Всего заявок</h3> <h3>📋 Всего заявок</h3>
<p class="stat-number">{% get_unread_callbacks %}/{{ total_callbacks }}</p> <p class="stat-number">{% get_unread_callbacks %}/{{ total_callbacks }}</p>
<small>(непрочитанные/всего)</small> <small>(непрочитанные/всего)</small>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Остальной код статистики остается без изменений --> <!-- Остальной код статистики остается без изменений -->
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3>🔥 Популярные страницы (за неделю)</h3> <h3>🔥 Популярные страницы (за неделю)</h3>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-striped mb-0"> <table class="table table-striped mb-0">
<thead> <thead>
<tr> <tr>
<th>Страница</th> <th>Страница</th>
<th>Просмотров</th> <th>Просмотров</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for page in popular_pages %} {% for page in popular_pages %}
<tr> <tr>
<td> <td>
<code>{{ page.url }}</code> <code>{{ page.url }}</code>
{% if page.url == '/' %} {% if page.url == '/' %}
<span class="badge bg-primary">Главная</span> <span class="badge bg-primary">Главная</span>
{% endif %} {% endif %}
</td> </td>
<td><strong>{{ page.views }}</strong></td> <td><strong>{{ page.views }}</strong></td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="2" class="text-center text-muted">Нет данных за выбранный период</td> <td colspan="2" class="text-center text-muted">Нет данных за выбранный период</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3>📋 Последние посещения</h3> <h3>📋 Последние посещения</h3>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-striped mb-0"> <table class="table table-striped mb-0">
<thead> <thead>
<tr> <tr>
<th>Время</th> <th>Время</th>
<th>Страница</th> <th>Страница</th>
<th>IP-адрес</th> <th>IP-адрес</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for view in recent_views %} {% for view in recent_views %}
<tr> <tr>
<td>{{ view.timestamp|date:"d.m.Y H:i" }}</td> <td>{{ view.timestamp|date:"d.m.Y H:i" }}</td>
<td><code>{{ view.url }}</code></td> <td><code>{{ view.url }}</code></td>
<td><small>{{ view.ip_address }}</small></td> <td><small>{{ view.ip_address }}</small></td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="3" class="text-center text-muted">Нет данных</td> <td colspan="3" class="text-center text-muted">Нет данных</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,57 +1,57 @@
<!-- templates/emails/callback_notification.html --> <!-- templates/emails/callback_notification.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; } body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; } .container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0; } .header { background: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9f9f9; padding: 20px; border-radius: 0 0 10px 10px; } .content { background: #f9f9f9; padding: 20px; border-radius: 0 0 10px 10px; }
.alert { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; } .alert { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; }
.info-box { background: white; padding: 15px; border-radius: 5px; margin: 10px 0; border-left: 4px solid #007bff; } .info-box { background: white; padding: 15px; border-radius: 5px; margin: 10px 0; border-left: 4px solid #007bff; }
.btn { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; } .btn { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }
.footer { text-align: center; margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px; } .footer { text-align: center; margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>🚨 Новая заявка на сайте</h1> <h1>🚨 Новая заявка на сайте</h1>
<p>Требуется ваше внимание!</p> <p>Требуется ваше внимание!</p>
</div> </div>
<div class="content"> <div class="content">
<div class="alert"> <div class="alert">
<strong>⚠️ Срочно!</strong> Пользователь оставил заявку на обратный звонок. <strong>⚠️ Срочно!</strong> Пользователь оставил заявку на обратный звонок.
</div> </div>
<div class="info-box"> <div class="info-box">
<h3>📋 Информация о заявке:</h3> <h3>📋 Информация о заявке:</h3>
<p><strong>👤 Имя:</strong> {{ callback.name }}</p> <p><strong>👤 Имя:</strong> {{ callback.name }}</p>
<p><strong>📞 Телефон:</strong> {{ callback.phone }}</p> <p><strong>📞 Телефон:</strong> {{ callback.phone }}</p>
{% if callback.email %} {% if callback.email %}
<p><strong>📧 Email:</strong> {{ callback.email }}</p> <p><strong>📧 Email:</strong> {{ callback.email }}</p>
{% endif %} {% endif %}
{% if callback.question %} {% if callback.question %}
<p><strong>❓ Вопрос:</strong><br>{{ callback.question }}</p> <p><strong>❓ Вопрос:</strong><br>{{ callback.question }}</p>
{% endif %} {% endif %}
<p><strong>🕒 Время отправки:</strong> {{ callback.time_create|date:"d.m.Y H:i" }}</p> <p><strong>🕒 Время отправки:</strong> {{ callback.time_create|date:"d.m.Y H:i" }}</p>
</div> </div>
<div style="text-align: center; margin: 20px 0;"> <div style="text-align: center; margin: 20px 0;">
<a href="http://{{ site_url }}/admin/programmer/callbackrequest/" class="btn"> <a href="http://{{ site_url }}/admin/programmer/callbackrequest/" class="btn">
📋 Перейти к заявкам в админке 📋 Перейти к заявкам в админке
</a> </a>
</div> </div>
<p><em>Не забудьте отметить заявку как обработанную после связи с клиентом!</em></p> <p><em>Не забудьте отметить заявку как обработанную после связи с клиентом!</em></p>
</div> </div>
<div class="footer"> <div class="footer">
<p>Это автоматическое уведомление от системы сайта.</p> <p>Это автоматическое уведомление от системы сайта.</p>
<p>Если вы получили это письмо по ошибке, пожалуйста, свяжитесь с администратором.</p> <p>Если вы получили это письмо по ошибке, пожалуйста, свяжитесь с администратором.</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,60 +1,60 @@
<!-- templates/emails/daily_summary.html --> <!-- templates/emails/daily_summary.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; } body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; } .container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0; } .header { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9f9f9; padding: 20px; border-radius: 0 0 10px 10px; } .content { background: #f9f9f9; padding: 20px; border-radius: 0 0 10px 10px; }
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0; } .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0; }
.stat-card { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stat-card { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-number { font-size: 2rem; font-weight: bold; margin: 0; } .stat-number { font-size: 2rem; font-weight: bold; margin: 0; }
.alert { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; } .alert { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; }
.btn { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; } .btn { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }
.footer { text-align: center; margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px; } .footer { text-align: center; margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>📊 Ежедневная сводка</h1> <h1>📊 Ежедневная сводка</h1>
<p>Статистика заявок за {{ yesterday|date:"d.m.Y" }}</p> <p>Статистика заявок за {{ yesterday|date:"d.m.Y" }}</p>
</div> </div>
<div class="content"> <div class="content">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<h3>📅 Вчерашние заявки</h3> <h3>📅 Вчерашние заявки</h3>
<p class="stat-number" style="color: #28a745;">{{ yesterday_callbacks }}</p> <p class="stat-number" style="color: #28a745;">{{ yesterday_callbacks }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>⏳ Ожидают обработки</h3> <h3>⏳ Ожидают обработки</h3>
<p class="stat-number" style="color: #ffc107;">{{ unprocessed_callbacks }}</p> <p class="stat-number" style="color: #ffc107;">{{ unprocessed_callbacks }}</p>
</div> </div>
</div> </div>
{% if unprocessed_callbacks > 0 %} {% if unprocessed_callbacks > 0 %}
<div class="alert"> <div class="alert">
<strong>⚠️ Внимание!</strong> У вас есть {{ unprocessed_callbacks }} необработанных заявок. <strong>⚠️ Внимание!</strong> У вас есть {{ unprocessed_callbacks }} необработанных заявок.
</div> </div>
{% endif %} {% endif %}
<div style="text-align: center; margin: 20px 0;"> <div style="text-align: center; margin: 20px 0;">
<a href="http://{{ site_url }}/admin/programmer/callbackrequest/" class="btn"> <a href="http://{{ site_url }}/admin/programmer/callbackrequest/" class="btn">
📋 Управление заявками 📋 Управление заявками
</a> </a>
</div> </div>
<p><em>Не забудьте обработать все pending заявки!</em></p> <p><em>Не забудьте обработать все pending заявки!</em></p>
</div> </div>
<div class="footer"> <div class="footer">
<p>Ежедневная автоматическая сводка от системы сайта.</p> <p>Ежедневная автоматическая сводка от системы сайта.</p>
<p>Дата формирования: {{ today|date:"d.m.Y H:i" }}</p> <p>Дата формирования: {{ today|date:"d.m.Y H:i" }}</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,154 +1,154 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1 class="page-title">{{title}}</h1> <h1 class="page-title">{{title}}</h1>
<p class="page-subtitle">Профессиональный программист 1С с более чем 10-летним опытом</p> <p class="page-subtitle">Профессиональный программист 1С с более чем 10-летним опытом</p>
</div> </div>
<div class="content-card"> <div class="content-card">
<div class="about-header"> <div class="about-header">
<h2>Николай Сердюк</h2> <h2>Николай Сердюк</h2>
<p class="subtitle">Разработчик 1С</p> <p class="subtitle">Разработчик 1С</p>
</div> </div>
<div class="about-section"> <div class="about-section">
<h3>🚀 Опыт работы</h3> <h3>🚀 Опыт работы</h3>
<p class="card-subtitle">Более 10 лет успешной работы в разработке и сопровождении систем на платформе 1С</p> <p class="card-subtitle">Более 10 лет успешной работы в разработке и сопровождении систем на платформе 1С</p>
<div class="experience-item mt-3"> <div class="experience-item mt-3">
<h4>Основные направления:</h4> <h4>Основные направления:</h4>
<div class="skills-grid"> <div class="skills-grid">
<div class="skill-category"> <div class="skill-category">
<h4>💻 Разработка</h4> <h4>💻 Разработка</h4>
<ul> <ul>
<li>Разработка и доработка конфигураций 1С</li> <li>Разработка и доработка конфигураций 1С</li>
<li>Создание внешних обработок и отчетов</li> <li>Создание внешних обработок и отчетов</li>
<li>Кастомизация под бизнес-процессы</li> <li>Кастомизация под бизнес-процессы</li>
</ul> </ul>
</div> </div>
<div class="skill-category"> <div class="skill-category">
<h4>🔗 Интеграция</h4> <h4>🔗 Интеграция</h4>
<ul> <ul>
<li>Интеграция 1С с веб-сервисами</li> <li>Интеграция 1С с веб-сервисами</li>
<li>Связь с сайтами и мобильными приложениями</li> <li>Связь с сайтами и мобильными приложениями</li>
<li>API и веб-сервисы</li> <li>API и веб-сервисы</li>
</ul> </ul>
</div> </div>
<div class="skill-category"> <div class="skill-category">
<h4>⚡ Оптимизация</h4> <h4>⚡ Оптимизация</h4>
<ul> <ul>
<li>Оптимизация бизнес-процессов</li> <li>Оптимизация бизнес-процессов</li>
<li>Ускорение работы баз данных</li> <li>Ускорение работы баз данных</li>
<li>Автоматизация рутинных операций</li> <li>Автоматизация рутинных операций</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="about-section"> <div class="about-section">
<h3>🛠 Технологии и навыки</h3> <h3>🛠 Технологии и навыки</h3>
<div class="skills-grid"> <div class="skills-grid">
<div class="skill-category"> <div class="skill-category">
<h4>🎯 1С Разработка</h4> <h4>🎯 1С Разработка</h4>
<ul> <ul>
<li>1С:Предприятие 8.3</li> <li>1С:Предприятие 8.3</li>
<li>Управление торговлей</li> <li>Управление торговлей</li>
<li>Бухгалтерия предприятия</li> <li>Бухгалтерия предприятия</li>
<li>Зарплата и управление персоналом</li> <li>Зарплата и управление персоналом</li>
<li>Внешние обработки и отчеты</li> <li>Внешние обработки и отчеты</li>
</ul> </ul>
</div> </div>
<div class="skill-category"> <div class="skill-category">
<h4>🔧 Дополнительные технологии</h4> <h4>🔧 Дополнительные технологии</h4>
<ul> <ul>
<li>SQL и оптимизация запросов</li> <li>SQL и оптимизация запросов</li>
<li>Веб-сервисы и API</li> <li>Веб-сервисы и API</li>
<li>XML, JSON, REST</li> <li>XML, JSON, REST</li>
<li>Системное администрирование</li> <li>Системное администрирование</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div class="about-section"> <div class="about-section">
<h3>📈 Проекты и достижения</h3> <h3>📈 Проекты и достижения</h3>
<p class="card-subtitle">Успешно реализовал более 50 проектов различной сложности</p> <p class="card-subtitle">Успешно реализовал более 50 проектов различной сложности</p>
<div class="skills-grid mt-3"> <div class="skills-grid mt-3">
<div class="skill-category"> <div class="skill-category">
<h4>🏆 Ключевые проекты</h4> <h4>🏆 Ключевые проекты</h4>
<ul> <ul>
<li>Автоматизация учетных систем для предприятий</li> <li>Автоматизация учетных систем для предприятий</li>
<li>Интеграция 1С с сайтами и мобильными приложениями</li> <li>Интеграция 1С с сайтами и мобильными приложениями</li>
<li>Разработка кастомизированных отчетов и дашбордов</li> <li>Разработка кастомизированных отчетов и дашбордов</li>
<li>Оптимизация производительности баз данных</li> <li>Оптимизация производительности баз данных</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div class="about-section"> <div class="about-section">
<h3>📞 Контакты</h3> <h3>📞 Контакты</h3>
<div class="contacts"> <div class="contacts">
<div class="skills-grid"> <div class="skills-grid">
<div class="skill-category"> <div class="skill-category">
<h4>📧 Электронная почта</h4> <h4>📧 Электронная почта</h4>
<p><strong>{{ CONTACT_EMAIL }}</strong></p> <p><strong>{{ CONTACT_EMAIL }}</strong></p>
</div> </div>
<div class="skill-category"> <div class="skill-category">
<h4>📱 Телефон</h4> <h4>📱 Телефон</h4>
<p><strong>{{ CONTACT_PHONE }}</strong></p> <p><strong>{{ CONTACT_PHONE }}</strong></p>
</div> </div>
<div class="skill-category"> <div class="skill-category">
<h4>💬 Telegram</h4> <h4>💬 Telegram</h4>
<p><strong><a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="display: inline-flex; padding: 0.5rem 1rem;">@odinesina_prog</a></strong></p> <p><strong><a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="display: inline-flex; padding: 0.5rem 1rem;">@odinesina_prog</a></strong></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center mt-4"> <div class="text-center mt-4">
<div class="card-actions"> <div class="card-actions">
<a href="{% url 'solution' %}" class="btn btn-primary">📂 Посмотреть мои проекты</a> <a href="{% url 'solution' %}" class="btn btn-primary">📂 Посмотреть мои проекты</a>
<a href="{% url 'recall' %}" class="btn btn-secondary">⭐ Отзывы клиентов</a> <a href="{% url 'recall' %}" class="btn btn-secondary">⭐ Отзывы клиентов</a>
</div> </div>
</div> </div>
</div> </div>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Service", "@type": "Service",
"serviceType": "1С программирование", "serviceType": "1С программирование",
"provider": { "provider": {
"@type": "Person", "@type": "Person",
"name": "Николай Сердюк" "name": "Николай Сердюк"
}, },
"areaServed": "Россия", "areaServed": "Россия",
"hasOfferCatalog": { "hasOfferCatalog": {
"@type": "OfferCatalog", "@type": "OfferCatalog",
"name": "Услуги программиста 1С", "name": "Услуги программиста 1С",
"itemListElement": [ "itemListElement": [
{ {
"@type": "Offer", "@type": "Offer",
"itemOffered": { "itemOffered": {
"@type": "Service", "@type": "Service",
"name": "Разработка конфигураций 1С" "name": "Разработка конфигураций 1С"
} }
}, },
{ {
"@type": "Offer", "@type": "Offer",
"itemOffered": { "itemOffered": {
"@type": "Service", "@type": "Service",
"name": "Интеграция 1С с веб-сервисами" "name": "Интеграция 1С с веб-сервисами"
} }
} }
] ]
} }
} }
</script> </script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}

View File

@ -1,72 +1,72 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% load static %} {% load static %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'programmer/css/competence.css' %}"> <link rel="stylesheet" href="{% static 'programmer/css/competence.css' %}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Компетенции</h1> <h1 class="page-title">Компетенции</h1>
<p class="page-subtitle">Профессиональные навыки и опыт</p> <p class="page-subtitle">Профессиональные навыки и опыт</p>
</div> </div>
<div class="competence-grid"> <div class="competence-grid">
{% for p in posts %} {% for p in posts %}
<div class="modern-card fade-in"> <div class="modern-card fade-in">
<div class="competence-item"> <div class="competence-item">
{% if p.photo %} {% if p.photo %}
<div class="competence-scan-wrapper"> <div class="competence-scan-wrapper">
<div class="competence-scan-container"> <div class="competence-scan-container">
<img src="{{ p.photo.url }}" <img src="{{ p.photo.url }}"
alt="Сертификат 1С: {{ p.title }} - {{ p.content|striptags }}" alt="Сертификат 1С: {{ p.title }} - {{ p.content|striptags }}"
class="competence-scan" class="competence-scan"
onclick="openCompetenceModal('{{ p.photo.url }}', '{{ p.title }}')"> onclick="openCompetenceModal('{{ p.photo.url }}', '{{ p.title }}')">
<div class="scan-hint"> <div class="scan-hint">
<span class="scan-zoom-icon">🔍</span> <span class="scan-zoom-icon">🔍</span>
<span class="scan-text">Нажмите для увеличения</span> <span class="scan-text">Нажмите для увеличения</span>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="competence-content"> <div class="competence-content">
<h2 class="competence-title">{{ p.title }}</h2> <h2 class="competence-title">{{ p.title }}</h2>
<div class="competence-description"> <div class="competence-description">
{{ p.content|linebreaks }} {{ p.content|linebreaks }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if not posts %} {% if not posts %}
<div class="modern-card text-center fade-in"> <div class="modern-card text-center fade-in">
<h3>📚 Информация о компетенциях</h3> <h3>📚 Информация о компетенциях</h3>
<p class="card-subtitle">Раздел находится в разработке</p> <p class="card-subtitle">Раздел находится в разработке</p>
<div class="card-actions justify-center"> <div class="card-actions justify-center">
<a href="{% url 'solution' %}" class="btn btn-primary">Посмотреть проекты</a> <a href="{% url 'solution' %}" class="btn btn-primary">Посмотреть проекты</a>
<a href="{% url 'about' %}" class="btn btn-secondary">Обо мне</a> <a href="{% url 'about' %}" class="btn btn-secondary">Обо мне</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Модальное окно для увеличенного просмотра --> <!-- Модальное окно для увеличенного просмотра -->
<div id="competenceModal" class="modal competence-modal"> <div id="competenceModal" class="modal competence-modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 id="competenceModalTitle">Компетенция</h3> <h3 id="competenceModalTitle">Компетенция</h3>
<button class="modal-close" onclick="closeCompetenceModal()">&times;</button> <button class="modal-close" onclick="closeCompetenceModal()">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<img class="modal-image" id="competenceModalImage" alt="Увеличенное изображение компетенции"> <img class="modal-image" id="competenceModalImage" alt="Увеличенное изображение компетенции">
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{% static 'programmer/js/competence.js' %}"></script> <script src="{% static 'programmer/js/competence.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,13 +1,13 @@
/* TEAM */ /* TEAM */
Developer: Николай Сердюк Developer: Николай Сердюк
Site: https://nikdizell.ru Site: https://nikdizell.ru
Email: {{ CONTACT_EMAIL }} Email: {{ CONTACT_EMAIL }}
/* THANKS */ /* THANKS */
Django Framework Django Framework
Bootstrap Bootstrap
/* SITE */ /* SITE */
Last update: 2025 Last update: 2025
Language: Russian Language: Russian
Doctype: HTML5 Doctype: HTML5

View File

@ -1,103 +1,103 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% block content %} {% block content %}
<div class="hero-section fade-in"> <div class="hero-section fade-in">
<h1 class="hero-title">🚀 Добро пожаловать!</h1> <h1 class="hero-title">🚀 Добро пожаловать!</h1>
<p class="hero-subtitle">Я профессиональный программист 1С с опытом создания эффективных бизнес-решений</p> <p class="hero-subtitle">Я профессиональный программист 1С с опытом создания эффективных бизнес-решений</p>
</div> </div>
<div class="grid grid-2"> <div class="grid grid-2">
{% autoescape off %} {% autoescape off %}
{% for p in posts %} {% for p in posts %}
<div class="modern-card fade-in {% cycle '' 'secondary' %}"> <div class="modern-card fade-in {% cycle '' 'secondary' %}">
<div class="card-header"> <div class="card-header">
<h2 class="card-title">{{p.title}}</h2> <h2 class="card-title">{{p.title}}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
{{p.content}} {{p.content}}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button onclick="openModal()" class="btn btn-primary">🎯 Получить консультацию</button> <button onclick="openModal()" class="btn btn-primary">🎯 Получить консультацию</button>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% endautoescape %} {% endautoescape %}
</div> </div>
<!-- Модальное окно формы --> <!-- Модальное окно формы -->
<div id="callbackModal" class="modal"> <div id="callbackModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3>📞 Заявка на консультацию</h3> <h3>📞 Заявка на консультацию</h3>
<button class="modal-close" onclick="closeModal()">&times;</button> <button class="modal-close" onclick="closeModal()">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="post" action="{% url 'callback' %}" id="callbackForm"> <form method="post" action="{% url 'callback' %}" id="callbackForm">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="id_name">Имя *</label> <label for="id_name">Имя *</label>
{{ form.name }} {{ form.name }}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="id_phone">Телефон *</label> <label for="id_phone">Телефон *</label>
{{ form.phone }} {{ form.phone }}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="id_email">Электронная почта</label> <label for="id_email">Электронная почта</label>
{{ form.email }} {{ form.email }}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="id_question">Ваш вопрос</label> <label for="id_question">Ваш вопрос</label>
{{ form.question }} {{ form.question }}
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary" style="width: 100%;"> <button type="submit" class="btn btn-primary" style="width: 100%;">
📨 Отправить заявку 📨 Отправить заявку
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% if not posts %} {% if not posts %}
<div class="modern-card text-center fade-in"> <div class="modern-card text-center fade-in">
<h3>🚀 Контент скоро появится</h3> <h3>🚀 Контент скоро появится</h3>
<p class="card-subtitle">Мы готовим для вас интересные материалы и кейсы</p> <p class="card-subtitle">Мы готовим для вас интересные материалы и кейсы</p>
<div class="card-actions justify-center"> <div class="card-actions justify-center">
<button onclick="openModal()" class="btn btn-primary">🎯 Получить консультацию</button> <button onclick="openModal()" class="btn btn-primary">🎯 Получить консультацию</button>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<script> <script>
function openModal() { function openModal() {
document.getElementById('callbackModal').style.display = 'block'; document.getElementById('callbackModal').style.display = 'block';
} }
function closeModal() { function closeModal() {
document.getElementById('callbackModal').style.display = 'none'; document.getElementById('callbackModal').style.display = 'none';
} }
// Закрытие модального окна при клике вне его // Закрытие модального окна при клике вне его
window.onclick = function(event) { window.onclick = function(event) {
const modal = document.getElementById('callbackModal'); const modal = document.getElementById('callbackModal');
if (event.target === modal) { if (event.target === modal) {
closeModal(); closeModal();
} }
} }
// Закрытие по ESC // Закрытие по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeModal(); closeModal();
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,174 +1,174 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% load static %} {% load static %}
{% load seo_tags %} {% load seo_tags %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}"> <link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Отзывы клиентов</h1> <h1 class="page-title">Отзывы клиентов</h1>
<p class="page-subtitle">Реальные отзывы о работе программиста 1С</p> <p class="page-subtitle">Реальные отзывы о работе программиста 1С</p>
</div> </div>
<div class="recall-grid"> <div class="recall-grid">
{% for p in posts %} {% for p in posts %}
<div class="modern-card fade-in"> <div class="modern-card fade-in">
<!-- Добавляем микроразметку для отзыва --> <!-- Добавляем микроразметку для отзыва -->
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Review", "@type": "Review",
"itemReviewed": { "itemReviewed": {
"@type": "Service", "@type": "Service",
"name": "Услуги программиста 1С" "name": "Услуги программиста 1С"
}, },
"author": { "author": {
"@type": "Organization", "@type": "Organization",
"name": "{{ p.title }}" "name": "{{ p.title }}"
}, },
"reviewRating": { "reviewRating": {
"@type": "Rating", "@type": "Rating",
"ratingValue": "5", "ratingValue": "5",
"bestRating": "5" "bestRating": "5"
}, },
"datePublished": "{{ p.time_create|date:'Y-m-d' }}", "datePublished": "{{ p.time_create|date:'Y-m-d' }}",
"description": "{{ p.content|striptags|truncatewords:50 }}" "description": "{{ p.content|striptags|truncatewords:50 }}"
} }
</script> </script>
<div class="recall-item"> <div class="recall-item">
<div class="recall-header"> <div class="recall-header">
<div class="recall-info"> <div class="recall-info">
<h2 class="recall-title">{{ p.title }}</h2> <h2 class="recall-title">{{ p.title }}</h2>
{% if p.time_create %} {% if p.time_create %}
<!-- <div class="recall-meta">--> <!-- <div class="recall-meta">-->
<!-- <span class="recall-date">{{ p.time_create|date:"d.m.Y" }}</span>--> <!-- <span class="recall-date">{{ p.time_create|date:"d.m.Y" }}</span>-->
<!-- </div>--> <!-- </div>-->
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="recall-content"> <div class="recall-content">
{% if p.scan %} {% if p.scan %}
<div class="recall-scan-wrapper"> <div class="recall-scan-wrapper">
<div class="recall-scan-container"> <div class="recall-scan-container">
<img src="{{ p.scan.url }}" <img src="{{ p.scan.url }}"
alt="Отзыв от {{ p.title }}" alt="Отзыв от {{ p.title }}"
class="recall-scan" class="recall-scan"
onclick="openModal('{{ p.scan.url }}', '{{ p.title }}')"> onclick="openModal('{{ p.scan.url }}', '{{ p.title }}')">
<div class="scan-hint"> <div class="scan-hint">
<span class="scan-zoom-icon">🔍</span> <span class="scan-zoom-icon">🔍</span>
<span class="scan-text">Нажмите для увеличения</span> <span class="scan-text">Нажмите для увеличения</span>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="recall-text"> <div class="recall-text">
{{ p.content|linebreaks }} {{ p.content|linebreaks }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if not posts %} {% if not posts %}
<div class="modern-card text-center fade-in"> <div class="modern-card text-center fade-in">
<h3>💬 Отзывы клиентов</h3> <h3>💬 Отзывы клиентов</h3>
<p class="card-subtitle">Здесь будут отображаться отзывы от довольных клиентов</p> <p class="card-subtitle">Здесь будут отображаться отзывы от довольных клиентов</p>
<div class="card-actions justify-center"> <div class="card-actions justify-center">
<a href="{% url 'solution' %}" class="btn btn-primary">Посмотреть проекты</a> <a href="{% url 'solution' %}" class="btn btn-primary">Посмотреть проекты</a>
<a href="{% url 'about' %}" class="btn btn-secondary">Связаться со мной</a> <a href="{% url 'about' %}" class="btn btn-secondary">Связаться со мной</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Модальное окно для увеличенного просмотра --> <!-- Модальное окно для увеличенного просмотра -->
<div id="imageModal" class="modal"> <div id="imageModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 id="modalTitle">Отзыв</h3> <h3 id="modalTitle">Отзыв</h3>
<button class="modal-close" onclick="closeModal()">&times;</button> <button class="modal-close" onclick="closeModal()">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<img class="modal-image" id="modalImage"> <img class="modal-image" id="modalImage">
</div> </div>
</div> </div>
</div> </div>
<script> <script>
function openModal(imageUrl, title) { function openModal(imageUrl, title) {
console.log('Opening modal with:', imageUrl); console.log('Opening modal with:', imageUrl);
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
if (modal && modalImg) { if (modal && modalImg) {
modal.style.display = "block"; modal.style.display = "block";
modalImg.src = imageUrl; modalImg.src = imageUrl;
if (title && modalTitle) { if (title && modalTitle) {
modalTitle.textContent = title; modalTitle.textContent = title;
} }
// Добавляем класс для анимации // Добавляем класс для анимации
setTimeout(() => { setTimeout(() => {
modal.classList.add('active'); modal.classList.add('active');
}, 10); }, 10);
} }
} }
function closeModal() { function closeModal() {
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
if (modal) { if (modal) {
modal.classList.remove('active'); modal.classList.remove('active');
setTimeout(() => { setTimeout(() => {
modal.style.display = "none"; modal.style.display = "none";
}, 300); }, 300);
} }
} }
// Закрытие модального окна при клике вне изображения // Закрытие модального окна при клике вне изображения
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
if (event.target === modal) { if (event.target === modal) {
closeModal(); closeModal();
} }
}); });
// Закрытие по ESC // Закрытие по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeModal(); closeModal();
} }
}); });
// Адаптация размера изображения в модальном окне // Адаптация размера изображения в модальном окне
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
if (modalImg && modalImg.src) { if (modalImg && modalImg.src) {
adjustModalImageSize(); adjustModalImageSize();
} }
}); });
function adjustModalImageSize() { function adjustModalImageSize() {
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
const modalContent = document.querySelector('.modal-content'); const modalContent = document.querySelector('.modal-content');
if (modalImg && modalContent) { if (modalImg && modalContent) {
const maxWidth = window.innerWidth * 0.9; const maxWidth = window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.8; const maxHeight = window.innerHeight * 0.8;
modalImg.style.maxWidth = `${maxWidth}px`; modalImg.style.maxWidth = `${maxWidth}px`;
modalImg.style.maxHeight = `${maxHeight}px`; modalImg.style.maxHeight = `${maxHeight}px`;
} }
} }
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{% static 'programmer/js/recall.js' %}"></script> <script src="{% static 'programmer/js/recall.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,19 +1,19 @@
User-agent: * User-agent: *
Allow: / Allow: /
# Основной сайт # Основной сайт
Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml
# Запрещаем служебные разделы # Запрещаем служебные разделы
Disallow: /admin/ Disallow: /admin/
Disallow: /media/cache/ Disallow: /media/cache/
Disallow: /static/admin/ Disallow: /static/admin/
Disallow: /callback/ Disallow: /callback/
Disallow: /api/ Disallow: /api/
# Разрешаем индексацию статических файлов # Разрешаем индексацию статических файлов
Allow: /static/ Allow: /static/
Allow: /media/ Allow: /media/
# Указываем главное зеркало # Указываем главное зеркало
Host: {{ request.scheme }}://{{ request.get_host }} Host: {{ request.scheme }}://{{ request.get_host }}

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"> xmlns:xhtml="http://www.w3.org/1999/xhtml">
{% for url in urlset %} {% for url in urlset %}
<url> <url>
<loc>{{ url.location }}</loc> <loc>{{ url.location }}</loc>
{% if url.lastmod %}<lastmod>{{ url.lastmod|date:"Y-m-d" }}</lastmod>{% endif %} {% if url.lastmod %}<lastmod>{{ url.lastmod|date:"Y-m-d" }}</lastmod>{% endif %}
{% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %} {% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %}
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %} {% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %}
</url> </url>
{% endfor %} {% endfor %}
</urlset> </urlset>

View File

@ -1,96 +1,96 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% load static %} {% load static %}
{% load seo_tags %} {% load seo_tags %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'programmer/css/solution-accordion.css' %}"> <link rel="stylesheet" href="{% static 'programmer/css/solution-accordion.css' %}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Проекты автоматизации 1С</h1> <h1 class="page-title">Проекты автоматизации 1С</h1>
<p class="page-subtitle">Реализованные решения и кейсы по автоматизации бизнес-процессов</p> <p class="page-subtitle">Реализованные решения и кейсы по автоматизации бизнес-процессов</p>
</div> </div>
<div class="improved-list"> <div class="improved-list">
{% autoescape off %} {% autoescape off %}
{% for p in posts %} {% for p in posts %}
<li class="modern-card fade-in"> <li class="modern-card fade-in">
<div class="content-card"> <div class="content-card">
<h2>{{p.title}}</h2> <h2>{{p.title}}</h2>
<!-- Добавляем микроразметку для проекта --> <!-- Добавляем микроразметку для проекта -->
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "CreativeWork", "@type": "CreativeWork",
"name": "{{ p.title }}", "name": "{{ p.title }}",
"description": "{{ p.description|striptags|truncatewords:30 }}", "description": "{{ p.description|striptags|truncatewords:30 }}",
"author": { "author": {
"@type": "Person", "@type": "Person",
"name": "Николай Сердюк" "name": "Николай Сердюк"
}, },
"datePublished": "{{ p.time_create|date:'Y-m-d' }}" "datePublished": "{{ p.time_create|date:'Y-m-d' }}"
} }
</script> </script>
<div class="solution-accordion"> <div class="solution-accordion">
<div class="accordion-item"> <div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)"> <div class="accordion-header" onclick="toggleAccordion(this)">
<strong>📋 Описание задачи</strong> <strong>📋 Описание задачи</strong>
<span class="accordion-icon"></span> <span class="accordion-icon"></span>
</div> </div>
<div class="accordion-content"> <div class="accordion-content">
{{p.description}} {{p.description}}
</div> </div>
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)"> <div class="accordion-header" onclick="toggleAccordion(this)">
<strong>🔧 Описание решения</strong> <strong>🔧 Описание решения</strong>
<span class="accordion-icon"></span> <span class="accordion-icon"></span>
</div> </div>
<div class="accordion-content"> <div class="accordion-content">
{{p.implementation}} {{p.implementation}}
</div> </div>
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)"> <div class="accordion-header" onclick="toggleAccordion(this)">
<strong>✅ Результат</strong> <strong>✅ Результат</strong>
<span class="accordion-icon"></span> <span class="accordion-icon"></span>
</div> </div>
<div class="accordion-content"> <div class="accordion-content">
{{p.closing}} {{p.closing}}
</div> </div>
</div> </div>
</div> </div>
<div class="article-panel"> <div class="article-panel">
<p class="first">Опубликовано: {{p.time_create|date:"d.m.Y"}}</p> <p class="first">Опубликовано: {{p.time_create|date:"d.m.Y"}}</p>
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="modern-card text-center fade-in"> <div class="modern-card text-center fade-in">
<h3>🚀 Проекты в разработке</h3> <h3>🚀 Проекты в разработке</h3>
<p class="card-subtitle">Скоро здесь появятся новые кейсы автоматизации</p> <p class="card-subtitle">Скоро здесь появятся новые кейсы автоматизации</p>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
{% endautoescape %} {% endautoescape %}
</div> </div>
{% if not posts %} {% if not posts %}
<div class="content-card text-center"> <div class="content-card text-center">
<h3>Примеры решений скоро появятся</h3> <h3>Примеры решений скоро появятся</h3>
<p>Мы готовим для вас интересные кейсы и решения</p> <p>Мы готовим для вас интересные кейсы и решения</p>
</div> </div>
{% endif %} {% endif %}
<!-- Подключаем внешний скрипт --> <!-- Подключаем внешний скрипт -->
<script src="{% static 'programmer/js/solution-accordion.js' %}"></script> <script src="{% static 'programmer/js/solution-accordion.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,14 +1,14 @@
from django import template from django import template
from ..models import CallbackRequest from ..models import CallbackRequest
register = template.Library() register = template.Library()
@register.simple_tag @register.simple_tag
def get_unread_callbacks(): def get_unread_callbacks():
return CallbackRequest.objects.filter(is_read=False).count() return CallbackRequest.objects.filter(is_read=False).count()
@register.simple_tag @register.simple_tag
def get_today_callbacks(): def get_today_callbacks():
from django.utils import timezone from django.utils import timezone
today = timezone.now().date() today = timezone.now().date()
return CallbackRequest.objects.filter(time_create__date=today).count() return CallbackRequest.objects.filter(time_create__date=today).count()

View File

@ -1,21 +1,21 @@
from django import template from django import template
from django.utils.html import strip_tags from django.utils.html import strip_tags
register = template.Library() register = template.Library()
@register.simple_tag @register.simple_tag
def generate_meta_description(obj, default=""): def generate_meta_description(obj, default=""):
"""Генерирует meta description для объектов""" """Генерирует meta description для объектов"""
if hasattr(obj, 'get_seo_description'): if hasattr(obj, 'get_seo_description'):
return obj.get_seo_description() return obj.get_seo_description()
elif hasattr(obj, 'content'): elif hasattr(obj, 'content'):
clean_content = strip_tags(obj.content)[:160] clean_content = strip_tags(obj.content)[:160]
return clean_content + '...' if len(clean_content) > 160 else clean_content return clean_content + '...' if len(clean_content) > 160 else clean_content
return default return default
@register.simple_tag @register.simple_tag
def generate_meta_keywords(obj, default=""): def generate_meta_keywords(obj, default=""):
"""Генерирует meta keywords для объектов""" """Генерирует meta keywords для объектов"""
if hasattr(obj, 'get_meta_keywords'): if hasattr(obj, 'get_meta_keywords'):
return ', '.join(obj.get_meta_keywords()) return ', '.join(obj.get_meta_keywords())
return default return default

View File

@ -1,3 +1,3 @@
from django.test import TestCase from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,27 +1,27 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from .views import * from .views import *
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
from .sitemaps import sitemaps from .sitemaps import sitemaps
urlpatterns = [ urlpatterns = [
path('', index, name='home'), path('', index, name='home'),
path('about/', about, name='about'), path('about/', about, name='about'),
path('solutions/', solution, name='solution'), path('solutions/', solution, name='solution'),
path('competence/', ability, name='ability'), path('competence/', ability, name='ability'),
path('recall/', recall, name='recall'), path('recall/', recall, name='recall'),
path('post/<int:post_id>/', show_post, name='post'), path('post/<int:post_id>/', show_post, name='post'),
path('callback/', callback_request, name='callback'), path('callback/', callback_request, name='callback'),
path('admin/statistics/', statistics_view, name='statistics'), path('admin/statistics/', statistics_view, name='statistics'),
# Sitemap # Sitemap
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, path('sitemap.xml', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'), name='django.contrib.sitemaps.views.sitemap'),
path('robots.txt', robots_txt, name='robots'), path('robots.txt', robots_txt, name='robots'),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,65 +1,65 @@
# programmer/utils/email_notifications.py # programmer/utils/email_notifications.py
from django.core.mail import send_mail, EmailMultiAlternatives from django.core.mail import send_mail, EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.conf import settings from django.conf import settings
from django.utils.html import strip_tags from django.utils.html import strip_tags
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def send_callback_notification(callback_request): def send_callback_notification(callback_request):
""" """
Отправляет уведомление о новой заявке на обратный звонок Отправляет уведомление о новой заявке на обратный звонок
""" """
try: try:
subject = f'🚨 Новая заявка на обратный звонок от {callback_request.name}' subject = f'🚨 Новая заявка на обратный звонок от {callback_request.name}'
# HTML версия письма # HTML версия письма
html_message = render_to_string('emails/callback_notification.html', { html_message = render_to_string('emails/callback_notification.html', {
'callback': callback_request, 'callback': callback_request,
'site_url': settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else 'localhost', 'site_url': settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else 'localhost',
}) })
# Текстовая версия письма # Текстовая версия письма
plain_message = strip_tags(html_message) plain_message = strip_tags(html_message)
# Проверяем настройки email # Проверяем настройки email
if not all([settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD]): if not all([settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD]):
logger.error("Email settings are not configured properly") logger.error("Email settings are not configured properly")
return False return False
# Отправляем email # Отправляем email
send_mail( send_mail(
subject=subject, subject=subject,
message=plain_message, message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=settings.ADMIN_EMAILS, recipient_list=settings.ADMIN_EMAILS,
html_message=html_message, html_message=html_message,
fail_silently=False, fail_silently=False,
) )
logger.info(f"Email notification sent successfully for callback #{callback_request.id}") logger.info(f"Email notification sent successfully for callback #{callback_request.id}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error sending email notification: {e}") logger.error(f"Error sending email notification: {e}")
return False return False
def send_test_email(): def send_test_email():
""" """
Функция для тестирования отправки email Функция для тестирования отправки email
""" """
try: try:
send_mail( send_mail(
subject='📧 Test Email from Django', subject='📧 Test Email from Django',
message='This is a test email from your Django application.', message='This is a test email from your Django application.',
from_email=settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=settings.ADMIN_EMAILS, recipient_list=settings.ADMIN_EMAILS,
fail_silently=False, fail_silently=False,
) )
return True return True
except Exception as e: except Exception as e:
logger.error(f"Test email failed: {e}") logger.error(f"Test email failed: {e}")
return False return False

View File

@ -1,259 +1,259 @@
from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponse, HttpResponseNotFound
from .models import * from .models import *
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from .models import CallbackRequest # Импортируем из models, а не forms from .models import CallbackRequest # Импортируем из models, а не forms
from .forms import CallbackForm from .forms import CallbackForm
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from .models import PageView, Visitor from .models import PageView, Visitor
from django.db.models import Count from django.db.models import Count
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
menu = [ menu = [
{'title': "Главная", 'url_name': 'home'}, {'title': "Главная", 'url_name': 'home'},
{'title': "Проекты", 'url_name': 'solution'}, {'title': "Проекты", 'url_name': 'solution'},
{'title': "Компетенции", 'url_name': 'ability'}, {'title': "Компетенции", 'url_name': 'ability'},
{'title': "Отзывы", 'url_name': 'recall'}, {'title': "Отзывы", 'url_name': 'recall'},
{'title': "Обо мне", 'url_name': 'about'} {'title': "Обо мне", 'url_name': 'about'}
] ]
# === ДОБАВЬТЕ ЭТИ ФУНКЦИИ ЗДЕСЬ === # === ДОБАВЬТЕ ЭТИ ФУНКЦИИ ЗДЕСЬ ===
def get_client_ip(request): def get_client_ip(request):
"""Получаем реальный IP клиента""" """Получаем реальный IP клиента"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for: if x_forwarded_for:
ip = x_forwarded_for.split(',')[0] ip = x_forwarded_for.split(',')[0]
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')
return ip return ip
def should_track_request(request): def should_track_request(request):
"""Определяем, нужно ли отслеживать запрос""" """Определяем, нужно ли отслеживать запрос"""
client_ip = get_client_ip(request) client_ip = get_client_ip(request)
path = request.path path = request.path
# Игнорируемые пути (Nextcloud специфичные) # Игнорируемые пути (Nextcloud специфичные)
nextcloud_paths = [ nextcloud_paths = [
'/index.php', '/index.php',
'/status.php', '/status.php',
'/cron', '/cron',
'/remote.php', '/remote.php',
'/ocs', '/ocs',
'/apps/', '/apps/',
'/custom_apps/', '/custom_apps/',
] ]
# Игнорируемые IP (Docker сети) # Игнорируемые IP (Docker сети)
docker_ips = [ docker_ips = [
'192.168.64.1', '192.168.64.1',
'192.168.65.1', '192.168.65.1',
'172.17.0.1', '172.17.0.1',
'172.18.0.1', '172.18.0.1',
'172.19.0.1', '172.19.0.1',
] ]
# Игнорируем статические файлы и админку # Игнорируем статические файлы и админку
if path.startswith('/static/') or path.startswith('/admin/'): if path.startswith('/static/') or path.startswith('/admin/'):
return False return False
# Не отслеживаем Nextcloud и Docker запросы # Не отслеживаем Nextcloud и Docker запросы
if any(path.startswith(p) for p in nextcloud_paths): if any(path.startswith(p) for p in nextcloud_paths):
return False return False
if client_ip in docker_ips: if client_ip in docker_ips:
return False return False
return True return True
def track_page_view(request): def track_page_view(request):
"""Основная функция отслеживания просмотров""" """Основная функция отслеживания просмотров"""
if not should_track_request(request): if not should_track_request(request):
return return
try: try:
PageView.objects.create( PageView.objects.create(
url=request.path, url=request.path,
ip_address=get_client_ip(request), ip_address=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500], user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
referer=request.META.get('HTTP_REFERER', '')[:500], referer=request.META.get('HTTP_REFERER', '')[:500],
) )
except Exception as e: except Exception as e:
print(f"Error tracking page: {e}") print(f"Error tracking page: {e}")
def track_view(view_func): def track_view(view_func):
"""Декоратор для отслеживания просмотров страниц""" """Декоратор для отслеживания просмотров страниц"""
from functools import wraps from functools import wraps
@wraps(view_func) @wraps(view_func)
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
# Отслеживаем просмотр перед выполнением view # Отслеживаем просмотр перед выполнением view
track_page_view(request) track_page_view(request)
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
return _wrapped_view return _wrapped_view
@track_view @track_view
def index(request): def index(request):
posts = Home.objects.filter(is_published=True) posts = Home.objects.filter(is_published=True)
context = { context = {
'posts': posts, 'posts': posts,
'menu': menu, 'menu': menu,
'title': "Программист 1С Николай Сердюк - разработка и сопровождение", 'title': "Программист 1С Николай Сердюк - разработка и сопровождение",
'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С.", 'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С.",
'meta_keywords': "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, интеграция 1С, доработка 1С, 1С предприятие 8.3", 'meta_keywords': "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, интеграция 1С, доработка 1С, 1С предприятие 8.3",
'form': CallbackForm() 'form': CallbackForm()
} }
return render(request, 'programmer/index.html', context=context) return render(request, 'programmer/index.html', context=context)
@track_view @track_view
def about(request): def about(request):
context = { context = {
'menu': menu, 'menu': menu,
'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С", 'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С",
'meta_description': "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7.", 'meta_description': "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7.",
'meta_keywords': "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, интеграция 1С, сертифицированный 1С, миграция 1С 7.7" 'meta_keywords': "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, интеграция 1С, сертифицированный 1С, миграция 1С 7.7"
} }
return render(request, 'programmer/about.html', context=context) return render(request, 'programmer/about.html', context=context)
@track_view @track_view
def solution(request): def solution(request):
posts = Solution.objects.filter(is_published=True) posts = Solution.objects.filter(is_published=True)
context = { context = {
'posts': posts, 'posts': posts,
'menu': menu, 'menu': menu,
'meta_description': "Реализованные проекты по автоматизации 1С: складской учет с ТСД, интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы.", 'meta_description': "Реализованные проекты по автоматизации 1С: складской учет с ТСД, интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы.",
'meta_keywords': "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, миграция 1С 7.7, кейсы 1С, примеры работ 1С", 'meta_keywords': "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, миграция 1С 7.7, кейсы 1С, примеры работ 1С",
'title': "Проекты автоматизации 1С | Реализованные кейсы и решения", 'title': "Проекты автоматизации 1С | Реализованные кейсы и решения",
} }
return render(request, 'programmer/solution.html', context=context) return render(request, 'programmer/solution.html', context=context)
@track_view @track_view
def ability(request): def ability(request):
posts = Competence.objects.filter(is_published=True) posts = Competence.objects.filter(is_published=True)
context = { context = {
'posts': posts, 'posts': posts,
'menu': menu, 'menu': menu,
'title': "Сертификаты и компетенции 1С | Программист 1С Николай Сердюк", 'title': "Сертификаты и компетенции 1С | Программист 1С Николай Сердюк",
'meta_description': "Сертификаты 1С: Профессионал по платформе 8.3 и БП 3.0. Подтвержденная квалификация программиста 1С с сертификатами фирмы 1С.", 'meta_description': "Сертификаты 1С: Профессионал по платформе 8.3 и БП 3.0. Подтвержденная квалификация программиста 1С с сертификатами фирмы 1С.",
'meta_keywords': "сертификаты 1С, 1С профессионал, компетенции 1С, квалификация программиста 1С, сертифицированный специалист 1С" 'meta_keywords': "сертификаты 1С, 1С профессионал, компетенции 1С, квалификация программиста 1С, сертифицированный специалист 1С"
} }
return render(request, 'programmer/competence.html', context=context) return render(request, 'programmer/competence.html', context=context)
@track_view @track_view
def recall(request): def recall(request):
posts = Recall.objects.filter(is_published=True) posts = Recall.objects.filter(is_published=True)
context = { context = {
'posts': posts, 'posts': posts,
'menu': menu, 'menu': menu,
'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы", 'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы",
'meta_description': "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. Отзывы от ООО «РОВЕН-Регионы» и других компаний.", 'meta_description': "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. Отзывы от ООО «РОВЕН-Регионы» и других компаний.",
'meta_keywords': "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, реальные кейсы 1С, отзыв ООО РОВЕН" 'meta_keywords': "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, реальные кейсы 1С, отзыв ООО РОВЕН"
} }
return render(request, 'programmer/recall.html', context=context) return render(request, 'programmer/recall.html', context=context)
def show_post(request, post_id): def show_post(request, post_id):
return HttpResponse(f"Отображение № {post_id}") return HttpResponse(f"Отображение № {post_id}")
def pageNotFound(request, exception): def pageNotFound(request, exception):
return HttpResponseNotFound('<h1>Страница не найдена</h1>') return HttpResponseNotFound('<h1>Страница не найдена</h1>')
def callback_request(request): def callback_request(request):
if request.method == 'POST': if request.method == 'POST':
form = CallbackForm(request.POST) form = CallbackForm(request.POST)
if form.is_valid(): if form.is_valid():
# Сохраняем заявку через форму # Сохраняем заявку через форму
form.save() form.save()
messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.') messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.')
return redirect('home') return redirect('home')
else: else:
# Если форма невалидна, показываем ошибки # Если форма невалидна, показываем ошибки
for field, errors in form.errors.items(): for field, errors in form.errors.items():
for error in errors: for error in errors:
messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}') messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}')
return redirect('home') return redirect('home')
# Если GET запрос, просто показываем главную страницу # Если GET запрос, просто показываем главную страницу
return redirect('home') return redirect('home')
def is_admin(user): def is_admin(user):
return user.is_staff return user.is_staff
def is_staff(user): def is_staff(user):
return user.is_staff return user.is_staff
@login_required @login_required
@user_passes_test(is_staff) @user_passes_test(is_staff)
def statistics_view(request): def statistics_view(request):
today = timezone.now().date() today = timezone.now().date()
week_ago = today - timedelta(days=7) week_ago = today - timedelta(days=7)
# Статистика за сегодня # Статистика за сегодня
today_views = PageView.objects.filter( today_views = PageView.objects.filter(
timestamp__date=today timestamp__date=today
).count() ).count()
# Статистика за неделю # Статистика за неделю
weekly_views = PageView.objects.filter( weekly_views = PageView.objects.filter(
timestamp__date__gte=week_ago timestamp__date__gte=week_ago
).count() ).count()
# Всего просмотров # Всего просмотров
total_views = PageView.objects.count() total_views = PageView.objects.count()
# Популярные страницы за неделю # Популярные страницы за неделю
popular_pages = PageView.objects.filter( popular_pages = PageView.objects.filter(
timestamp__date__gte=week_ago timestamp__date__gte=week_ago
).values('url').annotate( ).values('url').annotate(
views=Count('id') views=Count('id')
).order_by('-views')[:10] ).order_by('-views')[:10]
# Уникальные посетители за неделю # Уникальные посетители за неделю
unique_visitors = Visitor.objects.filter( unique_visitors = Visitor.objects.filter(
last_visit__date__gte=week_ago last_visit__date__gte=week_ago
).count() ).count()
# Последние посещения # Последние посещения
recent_views = PageView.objects.select_related().order_by('-timestamp')[:20] recent_views = PageView.objects.select_related().order_by('-timestamp')[:20]
today = timezone.now().date() today = timezone.now().date()
total_callbacks = CallbackRequest.objects.count() total_callbacks = CallbackRequest.objects.count()
today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count() today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count()
unread_callbacks = CallbackRequest.objects.filter(is_read=False).count() unread_callbacks = CallbackRequest.objects.filter(is_read=False).count()
context = { context = {
'today_views': today_views, 'today_views': today_views,
'weekly_views': weekly_views, 'weekly_views': weekly_views,
'total_views': total_views, 'total_views': total_views,
'unique_visitors': unique_visitors, 'unique_visitors': unique_visitors,
'popular_pages': popular_pages, 'popular_pages': popular_pages,
'recent_views': recent_views, 'recent_views': recent_views,
'total_callbacks': total_callbacks, 'total_callbacks': total_callbacks,
'today_callbacks': today_callbacks, 'today_callbacks': today_callbacks,
'unread_callbacks': unread_callbacks, 'unread_callbacks': unread_callbacks,
} }
return render(request, 'admin/statistics.html', context) return render(request, 'admin/statistics.html', context)
@require_GET @require_GET
def robots_txt(request): def robots_txt(request):
return render(request, 'robots.txt', content_type='text/plain') return render(request, 'robots.txt', content_type='text/plain')

View File

@ -1,212 +1,212 @@
""" """
Django settings for OneCprogsite project. Django settings for OneCprogsite project.
Generated by 'django-admin startproject' using Django 4.2.7. Generated by 'django-admin startproject' using Django 4.2.7.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/ https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/ https://docs.djangoproject.com/en/4.2/ref/settings/
""" """
import os.path import os.path
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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' SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm'
# Безопасность cookies для HTTPS # Безопасность cookies для HTTPS
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS
SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_SAMESITE = 'Lax'
# Если используете другие cookies # Если используете другие cookies
LANGUAGE_COOKIE_SECURE = True LANGUAGE_COOKIE_SECURE = True
LANGUAGE_COOKIE_HTTPONLY = True LANGUAGE_COOKIE_HTTPONLY = True
LANGUAGE_COOKIE_SAMESITE = 'Lax' LANGUAGE_COOKIE_SAMESITE = 'Lax'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# Или разрешить конкретные домены (Django 4.0+) # Или разрешить конкретные домены (Django 4.0+)
X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru' X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru'
# ОБЯЗАТЕЛЬНО укажите ваши домены # ОБЯЗАТЕЛЬНО укажите ваши домены
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'nikdizell.ru', 'nikdizell.ru',
'www.nikdizell.ru', 'www.nikdizell.ru',
'localhost', 'localhost',
'127.0.0.1', '127.0.0.1',
'192.168.31.88' # Добавьте IP сервера '192.168.31.88' # Добавьте IP сервера
] ]
# Важно для работы за прокси # Важно для работы за прокси
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True SECURE_SSL_REDIRECT = True
# Дополнительная безопасность # Дополнительная безопасность
SECURE_BROWSER_XSS_FILTER = True SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True SECURE_HSTS_PRELOAD = True
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'https://nikdizell.ru', 'https://nikdizell.ru',
'https://www.nikdizell.ru', 'https://www.nikdizell.ru',
] ]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'programmer.apps.ProgrammerConfig', 'programmer.apps.ProgrammerConfig',
'django_bootstrap5', 'django_bootstrap5',
'django_extensions', 'django_extensions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.sitemaps', 'django.contrib.sitemaps',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'programmer.middleware.PageViewMiddleware', 'programmer.middleware.PageViewMiddleware',
] ]
ROOT_URLCONF = 'OneCprogsite.urls' ROOT_URLCONF = 'OneCprogsite.urls'
# Кастомный middleware для CSP # Кастомный middleware для CSP
class CSPMiddleware: class CSPMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
response = self.get_response(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" 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 return response
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'programmer.context_processors.menu_processor', 'programmer.context_processors.menu_processor',
'programmer.context_processors.contact_info', 'programmer.context_processors.contact_info',
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'OneCprogsite.wsgi.application' WSGI_APPLICATION = 'OneCprogsite.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'App', 'NAME': 'App',
'USER': 'postgres', 'USER': 'postgres',
'PASSWORD': 'NikDi94Zell', 'PASSWORD': 'NikDi94Zell',
'HOST': 'postgres', 'HOST': 'postgres',
'PORT': 5432, 'PORT': 5432,
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'ru' LANGUAGE_CODE = 'ru'
TIME_ZONE = 'Europe/Moscow' TIME_ZONE = 'Europe/Moscow'
USE_I18N = True USE_I18N = True
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/ # https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [] STATICFILES_DIRS = []
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
# Настройки email # Настройки email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru # EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru
# EMAIL_PORT = 587 # EMAIL_PORT = 587
# EMAIL_USE_TLS = True # EMAIL_USE_TLS = True
# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru') # EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru')
# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc') # EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc')
# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
# SERVER_EMAIL = EMAIL_HOST_USER # SERVER_EMAIL = EMAIL_HOST_USER
EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com') EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER
# Email для уведомлений (можно указать несколько через запятую) # Email для уведомлений (можно указать несколько через запятую)
# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',') # ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',')
ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',') ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',')

View File

@ -1,38 +1,38 @@
/** /**
* @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter * @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter
* @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a> * @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a>
* @license MIT | GPL | Apache 2.0, see LICENSE.txt * @license MIT | GPL | Apache 2.0, see LICENSE.txt
* @see https://github.com/dyve/jquery-autocomplete * @see https://github.com/dyve/jquery-autocomplete
*/ */
.acResults { .acResults {
padding: 0px; padding: 0px;
border: 1px solid WindowFrame; border: 1px solid WindowFrame;
background-color: Window; background-color: Window;
overflow: hidden; overflow: hidden;
} }
.acResults ul { .acResults ul {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
list-style-position: outside; list-style-position: outside;
list-style: none; list-style: none;
} }
.acResults ul li { .acResults ul li {
margin: 0px; margin: 0px;
padding: 2px 5px; padding: 2px 5px;
cursor: pointer; cursor: pointer;
display: block; display: block;
font: menu; font: menu;
font-size: 12px; font-size: 12px;
overflow: hidden; overflow: hidden;
} }
.acLoading { .acLoading {
background : url('../img/indicator.gif') right center no-repeat; background : url('../img/indicator.gif') right center no-repeat;
} }
.acSelect { .acSelect {
background-color: Highlight; background-color: Highlight;
color: HighlightText; color: HighlightText;
} }

View File

@ -1,116 +1,116 @@
/** /**
* Ajax Queue Plugin * Ajax Queue Plugin
*/ */
/** /**
<script> <script>
$(function(){ $(function(){
jQuery.ajaxQueue({ jQuery.ajaxQueue({
url: "test.php", url: "test.php",
success: function(html){ jQuery("ul").append(html); } success: function(html){ jQuery("ul").append(html); }
}); });
jQuery.ajaxQueue({ jQuery.ajaxQueue({
url: "test.php", url: "test.php",
success: function(html){ jQuery("ul").append(html); } success: function(html){ jQuery("ul").append(html); }
}); });
jQuery.ajaxSync({ jQuery.ajaxSync({
url: "test.php", url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); } success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
}); });
jQuery.ajaxSync({ jQuery.ajaxSync({
url: "test.php", url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); } success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
}); });
}); });
</script> </script>
<ul style="position: absolute; top: 5px; right: 5px;"></ul> <ul style="position: absolute; top: 5px; right: 5px;"></ul>
*/ */
/* /*
* Queued Ajax requests. * Queued Ajax requests.
* A new Ajax request won't be started until the previous queued * A new Ajax request won't be started until the previous queued
* request has finished. * request has finished.
*/ */
/* /*
* Synced Ajax requests. * Synced Ajax requests.
* The Ajax request will happen as soon as you call this method, but * The Ajax request will happen as soon as you call this method, but
* the callbacks (success/error/complete) won't fire until all previous * the callbacks (success/error/complete) won't fire until all previous
* synced requests have been completed. * synced requests have been completed.
*/ */
(function(jQuery) { (function(jQuery) {
var ajax = jQuery.ajax; var ajax = jQuery.ajax;
var pendingRequests = {}; var pendingRequests = {};
var synced = []; var synced = [];
var syncedData = []; var syncedData = [];
jQuery.ajax = function(settings) { jQuery.ajax = function(settings) {
// create settings for compatibility with ajaxSetup // create settings for compatibility with ajaxSetup
settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings)); settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));
var port = settings.port; var port = settings.port;
switch(settings.mode) { switch(settings.mode) {
case "abort": case "abort":
if ( pendingRequests[port] ) { if ( pendingRequests[port] ) {
pendingRequests[port].abort(); pendingRequests[port].abort();
} }
return pendingRequests[port] = ajax.apply(this, arguments); return pendingRequests[port] = ajax.apply(this, arguments);
case "queue": case "queue":
var _old = settings.complete; var _old = settings.complete;
settings.complete = function(){ settings.complete = function(){
if ( _old ) if ( _old )
_old.apply( this, arguments ); _old.apply( this, arguments );
jQuery([ajax]).dequeue("ajax" + port );; jQuery([ajax]).dequeue("ajax" + port );;
}; };
jQuery([ ajax ]).queue("ajax" + port, function(){ jQuery([ ajax ]).queue("ajax" + port, function(){
ajax( settings ); ajax( settings );
}); });
return; return;
case "sync": case "sync":
var pos = synced.length; var pos = synced.length;
synced[ pos ] = { synced[ pos ] = {
error: settings.error, error: settings.error,
success: settings.success, success: settings.success,
complete: settings.complete, complete: settings.complete,
done: false done: false
}; };
syncedData[ pos ] = { syncedData[ pos ] = {
error: [], error: [],
success: [], success: [],
complete: [] complete: []
}; };
settings.error = function(){ syncedData[ pos ].error = arguments; }; settings.error = function(){ syncedData[ pos ].error = arguments; };
settings.success = function(){ syncedData[ pos ].success = arguments; }; settings.success = function(){ syncedData[ pos ].success = arguments; };
settings.complete = function(){ settings.complete = function(){
syncedData[ pos ].complete = arguments; syncedData[ pos ].complete = arguments;
synced[ pos ].done = true; synced[ pos ].done = true;
if ( pos == 0 || !synced[ pos-1 ] ) if ( pos == 0 || !synced[ pos-1 ] )
for ( var i = pos; i < synced.length && synced[i].done; i++ ) { for ( var i = pos; i < synced.length && synced[i].done; i++ ) {
if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error ); if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );
if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success ); if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );
if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete ); if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );
synced[i] = null; synced[i] = null;
syncedData[i] = null; syncedData[i] = null;
} }
}; };
} }
return ajax.apply(this, arguments); return ajax.apply(this, arguments);
}; };
})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')
? django.jQuery ? django.jQuery
: jQuery : jQuery
); );

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +1,39 @@
/*! Copyright (c) 2010 Brandon Aaron (http://brandon.aaron.sh/) /*! Copyright (c) 2010 Brandon Aaron (http://brandon.aaron.sh/)
* Licensed under the MIT License (LICENSE.txt). * Licensed under the MIT License (LICENSE.txt).
* *
* Version 2.1.2 * Version 2.1.2
*/ */
(function($){ (function($){
$.fn.bgiframe = ($.browser.msie && /msie 6\.0/i.test(navigator.userAgent) ? function(s) { $.fn.bgiframe = ($.browser.msie && /msie 6\.0/i.test(navigator.userAgent) ? function(s) {
s = $.extend({ s = $.extend({
top : 'auto', // auto == .currentStyle.borderTopWidth top : 'auto', // auto == .currentStyle.borderTopWidth
left : 'auto', // auto == .currentStyle.borderLeftWidth left : 'auto', // auto == .currentStyle.borderLeftWidth
width : 'auto', // auto == offsetWidth width : 'auto', // auto == offsetWidth
height : 'auto', // auto == offsetHeight height : 'auto', // auto == offsetHeight
opacity : true, opacity : true,
src : 'javascript:false;' src : 'javascript:false;'
}, s); }, s);
var html = '<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+ var html = '<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+
'style="display:block;position:absolute;z-index:-1;'+ 'style="display:block;position:absolute;z-index:-1;'+
(s.opacity !== false?'filter:Alpha(Opacity=\'0\');':'')+ (s.opacity !== false?'filter:Alpha(Opacity=\'0\');':'')+
'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+ 'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+
'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+ 'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+
'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+ 'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+
'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+ 'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+
'"/>'; '"/>';
return this.each(function() { return this.each(function() {
if ( $(this).children('iframe.bgiframe').length === 0 ) if ( $(this).children('iframe.bgiframe').length === 0 )
this.insertBefore( document.createElement(html), this.firstChild ); this.insertBefore( document.createElement(html), this.firstChild );
}); });
} : function() { return this; }); } : function() { return this; });
// old alias // old alias
$.fn.bgIframe = $.fn.bgiframe; $.fn.bgIframe = $.fn.bgiframe;
function prop(n) { function prop(n) {
return n && n.constructor === Number ? n + 'px' : n; return n && n.constructor === Number ? n + 'px' : n;
} }
})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') ? django.jQuery : jQuery); })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') ? django.jQuery : jQuery);

File diff suppressed because one or more lines are too long

View File

@ -1,299 +1,299 @@
/* competence.css - Стили для страницы компетенций */ /* competence.css - Стили для страницы компетенций */
/* Основные стили для страницы компетенций */ /* Основные стили для страницы компетенций */
.competence-item { .competence-item {
display: flex; display: flex;
gap: 2rem; gap: 2rem;
align-items: flex-start; align-items: flex-start;
padding: 2rem; padding: 2rem;
background: var(--bg-card); background: var(--bg-card);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
border-left: 4px solid var(--secondary); border-left: 4px solid var(--secondary);
transition: var(--transition); transition: var(--transition);
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
} }
.competence-item:hover { .competence-item:hover {
transform: translateX(8px); transform: translateX(8px);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
border-color: var(--primary-light); border-color: var(--primary-light);
} }
.competence-scan-wrapper { .competence-scan-wrapper {
flex-shrink: 0; flex-shrink: 0;
} }
.competence-scan-container { .competence-scan-container {
width: 280px; width: 280px;
cursor: pointer; cursor: pointer;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
transition: var(--transition); transition: var(--transition);
position: relative; position: relative;
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.competence-scan-container:hover { .competence-scan-container:hover {
transform: translateY(-4px) scale(1.02); transform: translateY(-4px) scale(1.02);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
border-color: var(--primary); border-color: var(--primary);
} }
.competence-scan { .competence-scan {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
transition: var(--transition); transition: var(--transition);
} }
.competence-content { .competence-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.competence-title { .competence-title {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.competence-description { .competence-description {
line-height: 1.7; line-height: 1.7;
font-size: 1.05rem; font-size: 1.05rem;
color: var(--text-primary); color: var(--text-primary);
} }
.competence-description p { .competence-description p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.competence-description p:last-child { .competence-description p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.scan-hint { .scan-hint {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8)); background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white; color: white;
padding: 1rem; padding: 1rem;
text-align: center; text-align: center;
opacity: 0; opacity: 0;
transition: var(--transition); transition: var(--transition);
transform: translateY(10px); transform: translateY(10px);
} }
.competence-scan-container:hover .scan-hint { .competence-scan-container:hover .scan-hint {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
/* Стили для модального окна с изображением компетенций */ /* Стили для модального окна с изображением компетенций */
.modal.competence-modal { .modal.competence-modal {
background-color: rgba(15, 19, 31, 0.95); background-color: rgba(15, 19, 31, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
max-width: 95vw; max-width: 95vw;
max-height: 95vh; max-height: 95vh;
margin: 2% auto; margin: 2% auto;
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
background: var(--bg-card); background: var(--bg-card);
border-bottom: 2px solid var(--border-light); border-bottom: 2px solid var(--border-light);
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
} }
.modal.competence-modal .modal-header h3 { .modal.competence-modal .modal-header h3 {
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.modal.competence-modal .modal-body { .modal.competence-modal .modal-body {
padding: 1rem; padding: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
} }
.modal-image { .modal-image {
max-width: 90vw; max-width: 90vw;
max-height: 80vh; max-height: 80vh;
width: auto; width: auto;
height: auto; height: auto;
display: block; display: block;
margin: 0 auto; margin: 0 auto;
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Анимации для модального окна */ /* Анимации для модального окна */
.modal.competence-modal { .modal.competence-modal {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
opacity: 0; opacity: 0;
} }
.modal.competence-modal.active { .modal.competence-modal.active {
opacity: 1; opacity: 1;
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
transform: scale(0.7); transform: scale(0.7);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.modal.competence-modal.active .modal-content { .modal.competence-modal.active .modal-content {
transform: scale(1); transform: scale(1);
} }
/* Улучшенные тени и границы */ /* Улучшенные тени и границы */
.competence-scan-container { .competence-scan-container {
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
.competence-scan-container:hover { .competence-scan-container:hover {
border-color: var(--primary-light); border-color: var(--primary-light);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
} }
/* Адаптивность для мобильных устройств */ /* Адаптивность для мобильных устройств */
@media (max-width: 768px) { @media (max-width: 768px) {
.competence-item { .competence-item {
flex-direction: column; flex-direction: column;
padding: 1.5rem; padding: 1.5rem;
gap: 1.5rem; gap: 1.5rem;
} }
.competence-scan-container { .competence-scan-container {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
} }
.competence-title { .competence-title {
font-size: 1.375rem; font-size: 1.375rem;
text-align: center; text-align: center;
} }
.modal-image { .modal-image {
max-width: 95vw; max-width: 95vw;
max-height: 70vh; max-height: 70vh;
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
margin: 10% auto; margin: 10% auto;
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.competence-item { .competence-item {
padding: 1.25rem; padding: 1.25rem;
} }
.competence-scan-container { .competence-scan-container {
max-width: 100%; max-width: 100%;
} }
.competence-title { .competence-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
.modal.competence-modal .modal-content { .modal.competence-modal .modal-content {
margin: 5% auto; margin: 5% auto;
max-width: 98vw; max-width: 98vw;
} }
.modal.competence-modal .modal-body { .modal.competence-modal .modal-body {
padding: 0.5rem; padding: 0.5rem;
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
padding: 1rem; padding: 1rem;
} }
.modal.competence-modal .modal-header h3 { .modal.competence-modal .modal-header h3 {
font-size: 1.1rem; font-size: 1.1rem;
} }
} }
/* Стили для светлой темы */ /* Стили для светлой темы */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
.modal.competence-modal { .modal.competence-modal {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
} }
.modal.competence-modal .modal-header { .modal.competence-modal .modal-header {
background: var(--bg-primary); background: var(--bg-primary);
} }
.scan-hint { .scan-hint {
background: linear-gradient(transparent, rgba(0,0,0,0.7)); background: linear-gradient(transparent, rgba(0,0,0,0.7));
} }
.competence-item { .competence-item {
background: var(--bg-primary); background: var(--bg-primary);
} }
.competence-description { .competence-description {
color: var(--text-primary); color: var(--text-primary);
} }
} }
/* Улучшенные стили для сетки компетенций */ /* Улучшенные стили для сетки компетенций */
.competence-grid { .competence-grid {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
} }
.competence-grid .modern-card { .competence-grid .modern-card {
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
.competence-grid .modern-card::before { .competence-grid .modern-card::before {
height: 4px; height: 4px;
background: var(--gradient-secondary); background: var(--gradient-secondary);
} }
/* Анимации появления */ /* Анимации появления */
.fade-in { .fade-in {
animation: fadeInUp 0.8s ease-out; animation: fadeInUp 0.8s ease-out;
} }
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(40px); transform: translateY(40px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }

View File

@ -1,228 +1,228 @@
/* recall.css - Стили для страницы отзывов */ /* recall.css - Стили для страницы отзывов */
/* Основные стили для страницы отзывов */ /* Основные стили для страницы отзывов */
.recall-item { .recall-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.recall-content { .recall-content {
display: flex; display: flex;
gap: 2rem; gap: 2rem;
align-items: flex-start; align-items: flex-start;
} }
.recall-scan-wrapper { .recall-scan-wrapper {
flex-shrink: 0; flex-shrink: 0;
} }
.recall-scan-container { .recall-scan-container {
width: 280px; width: 280px;
cursor: pointer; cursor: pointer;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
transition: var(--transition); transition: var(--transition);
position: relative; position: relative;
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.recall-scan-container:hover { .recall-scan-container:hover {
transform: translateY(-4px) scale(1.02); transform: translateY(-4px) scale(1.02);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
border-color: var(--primary); border-color: var(--primary);
} }
.recall-scan { .recall-scan {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
transition: var(--transition); transition: var(--transition);
} }
.recall-text { .recall-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
line-height: 1.7; line-height: 1.7;
font-size: 1.05rem; font-size: 1.05rem;
color: var(--text-primary); color: var(--text-primary);
} }
.recall-text p { .recall-text p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.recall-text p:last-child { .recall-text p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.scan-hint { .scan-hint {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8)); background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white; color: white;
padding: 1rem; padding: 1rem;
text-align: center; text-align: center;
opacity: 0; opacity: 0;
transition: var(--transition); transition: var(--transition);
transform: translateY(10px); transform: translateY(10px);
} }
.recall-scan-container:hover .scan-hint { .recall-scan-container:hover .scan-hint {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
/* Стили для модального окна с изображением */ /* Стили для модального окна с изображением */
.modal.image-modal { .modal.image-modal {
background-color: rgba(15, 19, 31, 0.95); background-color: rgba(15, 19, 31, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
max-width: 95vw; max-width: 95vw;
max-height: 95vh; max-height: 95vh;
margin: 2% auto; margin: 2% auto;
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
background: var(--bg-card); background: var(--bg-card);
border-bottom: 2px solid var(--border-light); border-bottom: 2px solid var(--border-light);
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
} }
.modal.image-modal .modal-header h3 { .modal.image-modal .modal-header h3 {
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.modal.image-modal .modal-body { .modal.image-modal .modal-body {
padding: 1rem; padding: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
} }
.modal-image { .modal-image {
max-width: 90vw; max-width: 90vw;
max-height: 80vh; max-height: 80vh;
width: auto; width: auto;
height: auto; height: auto;
display: block; display: block;
margin: 0 auto; margin: 0 auto;
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Анимации для модального окна */ /* Анимации для модального окна */
.modal.image-modal { .modal.image-modal {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
opacity: 0; opacity: 0;
} }
.modal.image-modal.active { .modal.image-modal.active {
opacity: 1; opacity: 1;
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
transform: scale(0.7); transform: scale(0.7);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.modal.image-modal.active .modal-content { .modal.image-modal.active .modal-content {
transform: scale(1); transform: scale(1);
} }
/* Улучшенные тени и границы */ /* Улучшенные тени и границы */
.recall-scan-container { .recall-scan-container {
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
.recall-scan-container:hover { .recall-scan-container:hover {
border-color: var(--primary-light); border-color: var(--primary-light);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
} }
/* Адаптивность для мобильных устройств */ /* Адаптивность для мобильных устройств */
@media (max-width: 768px) { @media (max-width: 768px) {
.recall-content { .recall-content {
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.recall-scan-container { .recall-scan-container {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
} }
.recall-scan-wrapper { .recall-scan-wrapper {
order: -1; order: -1;
} }
.modal-image { .modal-image {
max-width: 95vw; max-width: 95vw;
max-height: 70vh; max-height: 70vh;
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
margin: 10% auto; margin: 10% auto;
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.recall-scan-container { .recall-scan-container {
max-width: 100%; max-width: 100%;
} }
.modal.image-modal .modal-content { .modal.image-modal .modal-content {
margin: 5% auto; margin: 5% auto;
max-width: 98vw; max-width: 98vw;
} }
.modal.image-modal .modal-body { .modal.image-modal .modal-body {
padding: 0.5rem; padding: 0.5rem;
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
padding: 1rem; padding: 1rem;
} }
.modal.image-modal .modal-header h3 { .modal.image-modal .modal-header h3 {
font-size: 1.1rem; font-size: 1.1rem;
} }
} }
/* Стили для светлой темы */ /* Стили для светлой темы */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
.modal.image-modal { .modal.image-modal {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
} }
.modal.image-modal .modal-header { .modal.image-modal .modal-header {
background: var(--bg-primary); background: var(--bg-primary);
} }
.scan-hint { .scan-hint {
background: linear-gradient(transparent, rgba(0,0,0,0.7)); background: linear-gradient(transparent, rgba(0,0,0,0.7));
} }
} }

View File

@ -1,67 +1,67 @@
/* solution-accordion.css */ /* solution-accordion.css */
.solution-accordion { .solution-accordion {
margin: 2rem 0; margin: 2rem 0;
} }
.accordion-item { .accordion-item {
background: var(--bg-primary); background: var(--bg-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
margin-bottom: 1rem; margin-bottom: 1rem;
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
overflow: hidden; overflow: hidden;
transition: var(--transition); transition: var(--transition);
} }
.accordion-item:hover { .accordion-item:hover {
border-color: var(--primary-light); border-color: var(--primary-light);
} }
.accordion-header { .accordion-header {
padding: 1.5rem; padding: 1.5rem;
background: var(--gradient-primary); background: var(--gradient-primary);
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 1.125rem; font-size: 1.125rem;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
transition: var(--transition); transition: var(--transition);
user-select: none; user-select: none;
} }
.accordion-header:hover { .accordion-header:hover {
background: var(--primary-dark); background: var(--primary-dark);
} }
.accordion-content { .accordion-content {
padding: 0; padding: 0;
background: var(--bg-card); background: var(--bg-card);
line-height: 1.7; line-height: 1.7;
color: var(--text-secondary); color: var(--text-secondary);
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.accordion-content.active { .accordion-content.active {
padding: 1.5rem; padding: 1.5rem;
max-height: 5000px; max-height: 5000px;
} }
.accordion-icon { .accordion-icon {
transition: transform 0.3s ease; transition: transform 0.3s ease;
font-size: 0.8em; font-size: 0.8em;
} }
.accordion-header.active .accordion-icon { .accordion-header.active .accordion-icon {
transform: rotate(180deg); transform: rotate(180deg);
} }
.accordion-content p { .accordion-content p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.accordion-content p:last-child { .accordion-content p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +1,73 @@
// competence.js - Скрипты для страницы компетенций // competence.js - Скрипты для страницы компетенций
function openCompetenceModal(imageUrl, title) { function openCompetenceModal(imageUrl, title) {
console.log('Opening competence modal with:', imageUrl); console.log('Opening competence modal with:', imageUrl);
const modal = document.getElementById('competenceModal'); const modal = document.getElementById('competenceModal');
const modalImg = document.getElementById('competenceModalImage'); const modalImg = document.getElementById('competenceModalImage');
const modalTitle = document.getElementById('competenceModalTitle'); const modalTitle = document.getElementById('competenceModalTitle');
if (modal && modalImg) { if (modal && modalImg) {
modal.style.display = "block"; modal.style.display = "block";
modalImg.src = imageUrl; modalImg.src = imageUrl;
if (title && modalTitle) { if (title && modalTitle) {
modalTitle.textContent = title; modalTitle.textContent = title;
} }
// Добавляем класс для анимации // Добавляем класс для анимации
setTimeout(() => { setTimeout(() => {
modal.classList.add('active'); modal.classList.add('active');
}, 10); }, 10);
// Подстраиваем размер изображения // Подстраиваем размер изображения
adjustCompetenceModalImageSize(); adjustCompetenceModalImageSize();
} }
} }
function closeCompetenceModal() { function closeCompetenceModal() {
const modal = document.getElementById('competenceModal'); const modal = document.getElementById('competenceModal');
if (modal) { if (modal) {
modal.classList.remove('active'); modal.classList.remove('active');
setTimeout(() => { setTimeout(() => {
modal.style.display = "none"; modal.style.display = "none";
}, 300); }, 300);
} }
} }
function adjustCompetenceModalImageSize() { function adjustCompetenceModalImageSize() {
const modalImg = document.getElementById('competenceModalImage'); const modalImg = document.getElementById('competenceModalImage');
if (modalImg) { if (modalImg) {
const maxWidth = window.innerWidth * 0.9; const maxWidth = window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.8; const maxHeight = window.innerHeight * 0.8;
modalImg.style.maxWidth = `${maxWidth}px`; modalImg.style.maxWidth = `${maxWidth}px`;
modalImg.style.maxHeight = `${maxHeight}px`; modalImg.style.maxHeight = `${maxHeight}px`;
} }
} }
// Инициализация после загрузки DOM // Инициализация после загрузки DOM
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Закрытие модального окна при клике вне изображения // Закрытие модального окна при клике вне изображения
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const modal = document.getElementById('competenceModal'); const modal = document.getElementById('competenceModal');
if (event.target === modal) { if (event.target === modal) {
closeCompetenceModal(); closeCompetenceModal();
} }
}); });
// Закрытие по ESC // Закрытие по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeCompetenceModal(); closeCompetenceModal();
} }
}); });
// Адаптация размера изображения при изменении размера окна // Адаптация размера изображения при изменении размера окна
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
const modalImg = document.getElementById('competenceModalImage'); const modalImg = document.getElementById('competenceModalImage');
if (modalImg && modalImg.src) { if (modalImg && modalImg.src) {
adjustCompetenceModalImageSize(); adjustCompetenceModalImageSize();
} }
}); });
console.log('Competence page scripts initialized'); console.log('Competence page scripts initialized');
}); });

View File

@ -1,94 +1,94 @@
// Mobile Menu Script // Mobile Menu Script
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('Mobile menu script loaded'); // Для отладки console.log('Mobile menu script loaded'); // Для отладки
const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenuClose = document.getElementById('mobileMenuClose'); const mobileMenuClose = document.getElementById('mobileMenuClose');
const mobileMenuOverlay = document.getElementById('mobileMenuOverlay'); const mobileMenuOverlay = document.getElementById('mobileMenuOverlay');
const mobileMenu = document.getElementById('mobileMenu'); const mobileMenu = document.getElementById('mobileMenu');
const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); const mobileThemeToggle = document.getElementById('mobile-theme-toggle');
const mainThemeToggle = document.getElementById('theme-toggle'); const mainThemeToggle = document.getElementById('theme-toggle');
// Проверяем, что элементы существуют // Проверяем, что элементы существуют
if (!mobileMenuBtn || !mobileMenu) { if (!mobileMenuBtn || !mobileMenu) {
console.error('Mobile menu elements not found'); console.error('Mobile menu elements not found');
return; return;
} }
console.log('Mobile menu elements found:', { console.log('Mobile menu elements found:', {
mobileMenuBtn, mobileMenuBtn,
mobileMenuClose, mobileMenuClose,
mobileMenuOverlay, mobileMenuOverlay,
mobileMenu, mobileMenu,
mobileThemeToggle, mobileThemeToggle,
mainThemeToggle mainThemeToggle
}); });
// Открытие мобильного меню // Открытие мобильного меню
mobileMenuBtn.addEventListener('click', function() { mobileMenuBtn.addEventListener('click', function() {
console.log('Opening mobile menu'); console.log('Opening mobile menu');
mobileMenu.classList.add('active'); mobileMenu.classList.add('active');
mobileMenuOverlay.style.display = 'block'; mobileMenuOverlay.style.display = 'block';
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
}); });
// Закрытие мобильного меню // Закрытие мобильного меню
function closeMobileMenu() { function closeMobileMenu() {
console.log('Closing mobile menu'); console.log('Closing mobile menu');
mobileMenu.classList.remove('active'); mobileMenu.classList.remove('active');
mobileMenuOverlay.style.display = 'none'; mobileMenuOverlay.style.display = 'none';
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
if (mobileMenuClose) { if (mobileMenuClose) {
mobileMenuClose.addEventListener('click', closeMobileMenu); mobileMenuClose.addEventListener('click', closeMobileMenu);
} }
if (mobileMenuOverlay) { if (mobileMenuOverlay) {
mobileMenuOverlay.addEventListener('click', closeMobileMenu); mobileMenuOverlay.addEventListener('click', closeMobileMenu);
} }
// Закрытие меню при клике на ссылку // Закрытие меню при клике на ссылку
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link'); const mobileNavLinks = document.querySelectorAll('.mobile-nav-link');
mobileNavLinks.forEach(link => { mobileNavLinks.forEach(link => {
link.addEventListener('click', closeMobileMenu); link.addEventListener('click', closeMobileMenu);
}); });
// Синхронизация переключателей темы // Синхронизация переключателей темы
function syncThemeToggles() { function syncThemeToggles() {
if (mobileThemeToggle && mainThemeToggle) { if (mobileThemeToggle && mainThemeToggle) {
mobileThemeToggle.checked = mainThemeToggle.checked; mobileThemeToggle.checked = mainThemeToggle.checked;
} }
} }
if (mainThemeToggle) { if (mainThemeToggle) {
mainThemeToggle.addEventListener('change', function() { mainThemeToggle.addEventListener('change', function() {
console.log('Main theme toggle changed:', this.checked); console.log('Main theme toggle changed:', this.checked);
syncThemeToggles(); syncThemeToggles();
}); });
} }
if (mobileThemeToggle) { if (mobileThemeToggle) {
mobileThemeToggle.addEventListener('change', function() { mobileThemeToggle.addEventListener('change', function() {
console.log('Mobile theme toggle changed:', this.checked); console.log('Mobile theme toggle changed:', this.checked);
if (mainThemeToggle) { if (mainThemeToggle) {
mainThemeToggle.checked = this.checked; mainThemeToggle.checked = this.checked;
// Триггерим событие change // Триггерим событие change
const event = new Event('change'); const event = new Event('change');
mainThemeToggle.dispatchEvent(event); mainThemeToggle.dispatchEvent(event);
} }
}); });
} }
// Инициализация синхронизации // Инициализация синхронизации
syncThemeToggles(); syncThemeToggles();
// Закрытие меню по ESC // Закрытие меню по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeMobileMenu(); closeMobileMenu();
} }
}); });
console.log('Mobile menu script initialized successfully'); console.log('Mobile menu script initialized successfully');
}); });

View File

@ -1,74 +1,74 @@
// recall.js - Скрипты для страницы отзывов // recall.js - Скрипты для страницы отзывов
function openModal(imageUrl, title) { function openModal(imageUrl, title) {
console.log('Opening modal with:', imageUrl); console.log('Opening modal with:', imageUrl);
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
if (modal && modalImg) { if (modal && modalImg) {
modal.style.display = "block"; modal.style.display = "block";
modalImg.src = imageUrl; modalImg.src = imageUrl;
if (title && modalTitle) { if (title && modalTitle) {
modalTitle.textContent = title; modalTitle.textContent = title;
} }
// Добавляем класс для анимации // Добавляем класс для анимации
setTimeout(() => { setTimeout(() => {
modal.classList.add('active'); modal.classList.add('active');
}, 10); }, 10);
// Подстраиваем размер изображения // Подстраиваем размер изображения
adjustModalImageSize(); adjustModalImageSize();
} }
} }
function closeModal() { function closeModal() {
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
if (modal) { if (modal) {
modal.classList.remove('active'); modal.classList.remove('active');
setTimeout(() => { setTimeout(() => {
modal.style.display = "none"; modal.style.display = "none";
}, 300); }, 300);
} }
} }
function adjustModalImageSize() { function adjustModalImageSize() {
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
const modalContent = document.querySelector('.modal-content'); const modalContent = document.querySelector('.modal-content');
if (modalImg && modalContent) { if (modalImg && modalContent) {
const maxWidth = window.innerWidth * 0.9; const maxWidth = window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.8; const maxHeight = window.innerHeight * 0.8;
modalImg.style.maxWidth = `${maxWidth}px`; modalImg.style.maxWidth = `${maxWidth}px`;
modalImg.style.maxHeight = `${maxHeight}px`; modalImg.style.maxHeight = `${maxHeight}px`;
} }
} }
// Инициализация после загрузки DOM // Инициализация после загрузки DOM
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Закрытие модального окна при клике вне изображения // Закрытие модального окна при клике вне изображения
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const modal = document.getElementById('imageModal'); const modal = document.getElementById('imageModal');
if (event.target === modal) { if (event.target === modal) {
closeModal(); closeModal();
} }
}); });
// Закрытие по ESC // Закрытие по ESC
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeModal(); closeModal();
} }
}); });
// Адаптация размера изображения при изменении размера окна // Адаптация размера изображения при изменении размера окна
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
const modalImg = document.getElementById('modalImage'); const modalImg = document.getElementById('modalImage');
if (modalImg && modalImg.src) { if (modalImg && modalImg.src) {
adjustModalImageSize(); adjustModalImageSize();
} }
}); });
console.log('Recall page scripts initialized'); console.log('Recall page scripts initialized');
}); });

View File

@ -1,46 +1,46 @@
// solution-accordion.js // solution-accordion.js
function toggleAccordion(header) { function toggleAccordion(header) {
const content = header.nextElementSibling; const content = header.nextElementSibling;
const icon = header.querySelector('.accordion-icon'); const icon = header.querySelector('.accordion-icon');
// Переключаем только текущий аккордеон // Переключаем только текущий аккордеон
header.classList.toggle('active'); header.classList.toggle('active');
content.classList.toggle('active'); content.classList.toggle('active');
icon.style.transform = header.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)'; icon.style.transform = header.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)';
} }
// Функция для открытия всех аккордеонов // Функция для открытия всех аккордеонов
function expandAll() { function expandAll() {
document.querySelectorAll('.accordion-content').forEach(content => { document.querySelectorAll('.accordion-content').forEach(content => {
content.classList.add('active'); content.classList.add('active');
}); });
document.querySelectorAll('.accordion-header').forEach(header => { document.querySelectorAll('.accordion-header').forEach(header => {
header.classList.add('active'); header.classList.add('active');
}); });
document.querySelectorAll('.accordion-icon').forEach(icon => { document.querySelectorAll('.accordion-icon').forEach(icon => {
icon.style.transform = 'rotate(180deg)'; icon.style.transform = 'rotate(180deg)';
}); });
} }
// Функция для закрытия всех аккордеонов // Функция для закрытия всех аккордеонов
function collapseAll() { function collapseAll() {
document.querySelectorAll('.accordion-content').forEach(content => { document.querySelectorAll('.accordion-content').forEach(content => {
content.classList.remove('active'); content.classList.remove('active');
}); });
document.querySelectorAll('.accordion-header').forEach(header => { document.querySelectorAll('.accordion-header').forEach(header => {
header.classList.remove('active'); header.classList.remove('active');
}); });
document.querySelectorAll('.accordion-icon').forEach(icon => { document.querySelectorAll('.accordion-icon').forEach(icon => {
icon.style.transform = 'rotate(0deg)'; icon.style.transform = 'rotate(0deg)';
}); });
} }
// Автоматически открываем первый аккордеон в каждой карточке при загрузке // Автоматически открываем первый аккордеон в каждой карточке при загрузке
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.content-card').forEach(card => { document.querySelectorAll('.content-card').forEach(card => {
const firstAccordion = card.querySelector('.accordion-header'); const firstAccordion = card.querySelector('.accordion-header');
if (firstAccordion) { if (firstAccordion) {
toggleAccordion(firstAccordion); toggleAccordion(firstAccordion);
} }
}); });
}); });

View File

@ -1,68 +1,68 @@
// Theme Switcher Script // Theme Switcher Script
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); const mobileThemeToggle = document.getElementById('mobile-theme-toggle');
const themeCSS = document.getElementById('theme-css'); const themeCSS = document.getElementById('theme-css');
// Проверяем сохраненную тему в localStorage // Проверяем сохраненную тему в localStorage
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
// Устанавливаем светлую тему по умолчанию // Устанавливаем светлую тему по умолчанию
if (savedTheme === 'dark') { if (savedTheme === 'dark') {
switchToDarkTheme(); switchToDarkTheme();
} else { } else {
switchToLightTheme(); // Светлая тема по умолчанию switchToLightTheme(); // Светлая тема по умолчанию
} }
// Обработчик переключения темы для десктопного переключателя // Обработчик переключения темы для десктопного переключателя
if (themeToggle) { if (themeToggle) {
themeToggle.addEventListener('change', function() { themeToggle.addEventListener('change', function() {
if (this.checked) { if (this.checked) {
switchToLightTheme(); switchToLightTheme();
} else { } else {
switchToDarkTheme(); switchToDarkTheme();
} }
}); });
} }
// Обработчик переключения темы для мобильного переключателя // Обработчик переключения темы для мобильного переключателя
if (mobileThemeToggle) { if (mobileThemeToggle) {
mobileThemeToggle.addEventListener('change', function() { mobileThemeToggle.addEventListener('change', function() {
if (this.checked) { if (this.checked) {
switchToLightTheme(); switchToLightTheme();
} else { } else {
switchToDarkTheme(); switchToDarkTheme();
} }
// Синхронизируем оба переключателя // Синхронизируем оба переключателя
if (themeToggle) { if (themeToggle) {
themeToggle.checked = this.checked; themeToggle.checked = this.checked;
} }
}); });
} }
function switchToLightTheme() { function switchToLightTheme() {
themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css');
if (themeToggle) themeToggle.checked = true; if (themeToggle) themeToggle.checked = true;
if (mobileThemeToggle) mobileThemeToggle.checked = true; if (mobileThemeToggle) mobileThemeToggle.checked = true;
localStorage.setItem('theme', 'light'); localStorage.setItem('theme', 'light');
} }
function switchToDarkTheme() { function switchToDarkTheme() {
themeCSS.href = themeCSS.href.replace('styles_w.css', 'styles_dark.css'); themeCSS.href = themeCSS.href.replace('styles_w.css', 'styles_dark.css');
if (themeToggle) themeToggle.checked = false; if (themeToggle) themeToggle.checked = false;
if (mobileThemeToggle) mobileThemeToggle.checked = false; if (mobileThemeToggle) mobileThemeToggle.checked = false;
localStorage.setItem('theme', 'dark'); localStorage.setItem('theme', 'dark');
} }
// Синхронизация переключателей при загрузке // Синхронизация переключателей при загрузке
if (themeToggle && mobileThemeToggle) { if (themeToggle && mobileThemeToggle) {
mobileThemeToggle.checked = themeToggle.checked; mobileThemeToggle.checked = themeToggle.checked;
} }
// Обработка ошибок загрузки CSS // Обработка ошибок загрузки CSS
themeCSS.onerror = function() { themeCSS.onerror = function() {
console.error('Ошибка загрузки CSS файла темы'); console.error('Ошибка загрузки CSS файла темы');
// Восстанавливаем светлую тему по умолчанию при ошибке // Восстанавливаем светлую тему по умолчанию при ошибке
themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css');
}; };
}); });

View File

@ -1,39 +1,39 @@
""" """
URL configuration for OneCprogsite project. URL configuration for OneCprogsite project.
The `urlpatterns` list routes URLs to views. For more information please see: The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/ https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples: Examples:
Function views Function views
1. Add an import: from my_app import views 1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home') 2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views Class-based views
1. Add an import: from other_app.views import Home 1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from OneCprogsite import settings from OneCprogsite import settings
from programmer.views import * from programmer.views import *
from django.urls import path, include from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('programmer.urls')), path('', include('programmer.urls')),
# path('', index, name='home'), # path('', index, name='home'),
# path('about/', about, name='about'), # path('about/', about, name='about'),
# path('solution/', solution, name='solution'), # path('solution/', solution, name='solution'),
# path('ability/', ability, name='ability'), # path('ability/', ability, name='ability'),
# path('recall/', recall, name='recall'), # path('recall/', recall, name='recall'),
# path('post/<int:post_id>', show_post, name='post'), # path('post/<int:post_id>', show_post, name='post'),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
handler404 = pageNotFound handler404 = pageNotFound

View File

@ -1,16 +1,16 @@
""" """
WSGI config for OneCprogsite project. WSGI config for OneCprogsite project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
""" """
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings')
application = get_wsgi_application() application = get_wsgi_application()

0
programmer/__init__.py Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More