Последние измения на прод
This commit is contained in:
parent
27ac664e56
commit
c947dc980a
@ -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()
|
||||||
|
|||||||
@ -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(',')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 = 'Программисты'
|
||||||
|
|||||||
@ -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'),
|
||||||
}
|
}
|
||||||
@ -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': 'Ваш вопрос'
|
||||||
}
|
}
|
||||||
@ -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('Ежедневная сводка не отправлена (нет данных или ошибка)')
|
||||||
)
|
)
|
||||||
@ -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.')
|
||||||
)
|
)
|
||||||
@ -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!')
|
||||||
|
|||||||
@ -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
|
||||||
@ -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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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='Программист'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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='Фото'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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': 'Компетенции'},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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='Ваш вопрос'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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')],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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='Прочитано'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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='Уведомление отправлено'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
@ -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');
|
||||||
});
|
});
|
||||||
@ -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');
|
||||||
});
|
});
|
||||||
@ -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');
|
||||||
});
|
});
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -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');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -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>
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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
@ -1,8 +1,8 @@
|
|||||||
{% extends 'programmer/base.html' %}
|
{% extends 'programmer/base.html' %}
|
||||||
{% load django_bootstrap5 %}
|
{% load django_bootstrap5 %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -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()">×</button>
|
<button class="modal-close" onclick="closeCompetenceModal()">×</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 %}
|
||||||
@ -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
|
||||||
@ -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()">×</button>
|
<button class="modal-close" onclick="closeModal()">×</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 %}
|
||||||
@ -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()">×</button>
|
<button class="modal-close" onclick="closeModal()">×</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 %}
|
||||||
@ -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 }}
|
||||||
@ -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>
|
||||||
@ -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 %}
|
||||||
@ -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()
|
||||||
@ -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
|
||||||
@ -1,3 +1,3 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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(',')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
@ -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');
|
||||||
});
|
});
|
||||||
@ -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');
|
||||||
});
|
});
|
||||||
@ -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');
|
||||||
});
|
});
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -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');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
0
programmer/__init__.py
Normal file
169
programmer/admin.py
Normal file
169
programmer/admin.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.urls import path
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
|
class ProgrammerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
class RecallAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'description', 'implementation')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CallbackRequest)
|
||||||
|
class CallbackAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge')
|
||||||
|
list_display_links = ('name', 'phone')
|
||||||
|
list_editable = ('is_processed', 'is_read')
|
||||||
|
list_filter = ('time_create', 'is_processed', 'is_read')
|
||||||
|
search_fields = ('name', 'phone', 'email')
|
||||||
|
readonly_fields = ('time_create',)
|
||||||
|
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed']
|
||||||
|
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
|
||||||
|
|
||||||
|
def resend_notification(self, request, queryset):
|
||||||
|
from .utils.email_notifications import send_callback_notification
|
||||||
|
count = 0
|
||||||
|
for callback in queryset:
|
||||||
|
success = send_callback_notification(callback)
|
||||||
|
if success:
|
||||||
|
count += 1
|
||||||
|
self.message_user(request, f'Уведомления отправлены для {count} заявок')
|
||||||
|
|
||||||
|
resend_notification.short_description = "Переотправить email уведомления"
|
||||||
|
|
||||||
|
def new_badge(self, obj):
|
||||||
|
if not obj.is_read:
|
||||||
|
return format_html('<span style="color: red; font-weight: bold;">🆕 НОВАЯ</span>')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
new_badge.short_description = 'Статус'
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
# Показываем количество непрочитанных в заголовке
|
||||||
|
unread_count = CallbackRequest.objects.filter(is_read=False).count()
|
||||||
|
if unread_count > 0:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'У вас {unread_count} непрочитанных заявок!',
|
||||||
|
messages.WARNING
|
||||||
|
)
|
||||||
|
return super().get_queryset(request)
|
||||||
|
|
||||||
|
def mark_as_read(self, request, queryset):
|
||||||
|
updated = queryset.update(is_read=True)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
|
||||||
|
|
||||||
|
mark_as_read.short_description = "Отметить как прочитанные"
|
||||||
|
|
||||||
|
def mark_as_unread(self, request, queryset):
|
||||||
|
updated = queryset.update(is_read=False)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как непрочитанные')
|
||||||
|
|
||||||
|
mark_as_unread.short_description = "Отметить как непрочитанные"
|
||||||
|
|
||||||
|
def mark_as_processed(self, request, queryset):
|
||||||
|
updated = queryset.update(is_processed=True)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как обработанные')
|
||||||
|
|
||||||
|
mark_as_processed.short_description = "Отметить как обработанные"
|
||||||
|
|
||||||
|
# Добавляем кастомное представление для статистики
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def callback_stats(self, request):
|
||||||
|
today = timezone.now().date()
|
||||||
|
week_ago = today - timezone.timedelta(days=7)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total': CallbackRequest.objects.count(),
|
||||||
|
'today': CallbackRequest.objects.filter(time_create__date=today).count(),
|
||||||
|
'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(),
|
||||||
|
'unread': CallbackRequest.objects.filter(is_read=False).count(),
|
||||||
|
'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**self.admin_site.each_context(request),
|
||||||
|
'title': 'Статистика заявок',
|
||||||
|
'stats': stats,
|
||||||
|
}
|
||||||
|
return render(request, 'admin/callback_stats.html', context)
|
||||||
|
|
||||||
|
class ProgrammerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class RecallAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'description', 'implementation')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class HomeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
@admin.register(PageView)
|
||||||
|
class PageViewAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['url', 'timestamp', 'ip_address']
|
||||||
|
list_filter = ['timestamp', 'url']
|
||||||
|
search_fields = ['url', 'ip_address']
|
||||||
|
date_hierarchy = 'timestamp'
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).order_by('-timestamp')
|
||||||
|
|
||||||
|
admin.site.register(Competence, ProgrammerAdmin)
|
||||||
|
admin.site.register(Recall, RecallAdmin)
|
||||||
|
admin.site.register(Solution, SolutionAdmin)
|
||||||
|
admin.site.register(Home, HomeAdmin)
|
||||||
7
programmer/apps.py
Normal file
7
programmer/apps.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProgrammerConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'programmer'
|
||||||
|
verbose_name = 'Программисты'
|
||||||
12
programmer/context_processors.py
Normal file
12
programmer/context_processors.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from .views import menu
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def menu_processor(request):
|
||||||
|
return {'menu': menu}
|
||||||
|
|
||||||
|
def contact_info(request):
|
||||||
|
return {
|
||||||
|
'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'),
|
||||||
|
'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'),
|
||||||
|
}
|
||||||
33
programmer/forms.py
Normal file
33
programmer/forms.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import CallbackRequest
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CallbackRequest
|
||||||
|
fields = ['name', 'phone', 'email', 'question']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-input',
|
||||||
|
'placeholder': 'Ваше имя'
|
||||||
|
}),
|
||||||
|
'phone': forms.TextInput(attrs={
|
||||||
|
'class': 'form-input',
|
||||||
|
'placeholder': '+7 (___) ___-__-__'
|
||||||
|
}),
|
||||||
|
'email': forms.EmailInput(attrs={
|
||||||
|
'class': 'form-input',
|
||||||
|
'placeholder': 'your@email.com'
|
||||||
|
}),
|
||||||
|
'question': forms.Textarea(attrs={
|
||||||
|
'class': 'form-textarea',
|
||||||
|
'placeholder': 'Опишите ваш вопрос или задачу...',
|
||||||
|
'rows': 4
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'name': 'Имя',
|
||||||
|
'phone': 'Телефон',
|
||||||
|
'email': 'Электронная почта',
|
||||||
|
'question': 'Ваш вопрос'
|
||||||
|
}
|
||||||
0
programmer/management/__init__.py
Normal file
0
programmer/management/__init__.py
Normal file
0
programmer/management/commands/__init__.py
Normal file
0
programmer/management/commands/__init__.py
Normal file
33
programmer/management/commands/send_daily_summary.py
Normal file
33
programmer/management/commands/send_daily_summary.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# programmer/management/commands/send_daily_summary.py
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from programmer.utils.email_notifications import send_daily_summary
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Отправляет ежедневную сводку по заявкам на email'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--test',
|
||||||
|
action='store_true',
|
||||||
|
help='Тестовая отправка (не учитывает реальные данные)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options['test']:
|
||||||
|
self.stdout.write(self.style.WARNING('Тестовая отправка ежедневной сводки...'))
|
||||||
|
# Здесь можно добавить тестовые данные
|
||||||
|
else:
|
||||||
|
self.stdout.write('Отправка ежедневной сводки...')
|
||||||
|
|
||||||
|
success = send_daily_summary()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Ежедневная сводка отправлена успешно! Время: {timezone.now()}')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING('Ежедневная сводка не отправлена (нет данных или ошибка)')
|
||||||
|
)
|
||||||
29
programmer/management/commands/test_email.py
Normal file
29
programmer/management/commands/test_email.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# programmer/management/commands/test_email.py
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from programmer.utils.email_notifications import send_test_email
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Test email configuration'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write("Testing email configuration...")
|
||||||
|
|
||||||
|
# Проверяем настройки
|
||||||
|
self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}")
|
||||||
|
self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}")
|
||||||
|
self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}")
|
||||||
|
self.stdout.write(f"ADMIN_EMAILS: {settings.ADMIN_EMAILS}")
|
||||||
|
|
||||||
|
# Тестируем отправку
|
||||||
|
success = send_test_email()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS('✅ Test email sent successfully!')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR('❌ Failed to send test email. Check your email settings.')
|
||||||
|
)
|
||||||
27
programmer/management/commands/test_sitemap.py
Normal file
27
programmer/management/commands/test_sitemap.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.sitemaps import Sitemap
|
||||||
|
from django.urls import reverse
|
||||||
|
from programmer.models import Home, Solution, Competence, Recall
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Test sitemap generation'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
from programmer.sitemaps import sitemaps
|
||||||
|
|
||||||
|
self.stdout.write('Testing sitemap generation...')
|
||||||
|
|
||||||
|
for name, sitemap in sitemaps.items():
|
||||||
|
self.stdout.write(f'\n{name}:')
|
||||||
|
items = sitemap().items()
|
||||||
|
self.stdout.write(f' Items found: {len(items)}')
|
||||||
|
|
||||||
|
for item in items[:3]: # Показываем первые 3 элемента
|
||||||
|
try:
|
||||||
|
url = sitemap().location(item)
|
||||||
|
self.stdout.write(f' - {url}')
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(f' - Error: {e}')
|
||||||
|
|
||||||
|
self.stdout.write('\nSitemap test completed!')
|
||||||
54
programmer/middleware.py
Normal file
54
programmer/middleware.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from .models import PageView, Visitor
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
|
||||||
|
class PageViewMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
# Игнорируем статические файлы и админку
|
||||||
|
if not request.path.startswith('/static/') and not request.path.startswith('/admin/'):
|
||||||
|
self.track_page_view(request)
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def track_page_view(self, request):
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# Сохраняем просмотр страницы
|
||||||
|
PageView.objects.create(
|
||||||
|
url=request.path,
|
||||||
|
ip_address=self.get_client_ip(request),
|
||||||
|
user_agent=request.META.get('HTTP_USER_AGENT', ''),
|
||||||
|
referer=request.META.get('HTTP_REFERER', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем статистику посетителя
|
||||||
|
ip = self.get_client_ip(request)
|
||||||
|
visitor, created = Visitor.objects.get_or_create(
|
||||||
|
ip_address=ip,
|
||||||
|
defaults={
|
||||||
|
'first_visit': timezone.now(),
|
||||||
|
'last_visit': timezone.now()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
visitor.last_visit = timezone.now()
|
||||||
|
visitor.visit_count += 1
|
||||||
|
visitor.save()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Логируем ошибку, но не прерываем выполнение
|
||||||
|
print(f"Error tracking page view: {e}")
|
||||||
|
|
||||||
|
def get_client_ip(self, request):
|
||||||
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||||
|
if x_forwarded_for:
|
||||||
|
ip = x_forwarded_for.split(',')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
return ip
|
||||||
26
programmer/migrations/0001_initial.py
Normal file
26
programmer/migrations/0001_initial.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-23 12:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Competence',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('content', models.TextField(blank=True)),
|
||||||
|
('photo', models.ImageField(upload_to='photos/%Y/%m/%d/')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True)),
|
||||||
|
('is_publiched', models.BooleanField(default=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 08:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='competence',
|
||||||
|
options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компитенция', 'verbose_name_plural': 'Компитенции'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Компетенция'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='is_publiched',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Опубликован'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='photo',
|
||||||
|
field=models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='time_create',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='time_update',
|
||||||
|
field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competence',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(max_length=255, verbose_name='Программист'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 11:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0002_alter_competence_options_alter_competence_content_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Recall',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Организация')),
|
||||||
|
('content', models.TextField(blank=True, verbose_name='Отзыв')),
|
||||||
|
('photo', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото')),
|
||||||
|
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
|
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
|
||||||
|
('is_published', models.BooleanField(default=True, verbose_name='Опубликован')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Отзыв',
|
||||||
|
'verbose_name_plural': 'Отзывы',
|
||||||
|
'ordering': ['time_create', 'title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='competence',
|
||||||
|
old_name='is_publiched',
|
||||||
|
new_name='is_published',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
programmer/migrations/0004_rename_photo_recall_scan.py
Normal file
18
programmer/migrations/0004_rename_photo_recall_scan.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-11-24 12:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0003_recall_rename_is_publiched_competence_is_published'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='recall',
|
||||||
|
old_name='photo',
|
||||||
|
new_name='scan',
|
||||||
|
),
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user