from django.http import HttpResponse, HttpResponseNotFound, HttpRequest from .models import * from django.shortcuts import render, redirect from django.contrib import messages from .models import Home, Solution, Recall, PageView, Visitor, CallbackRequest from .forms import CallbackForm from django.utils import timezone from datetime import timedelta from .models import PageView, Visitor from django.db.models import Count, QuerySet from django.contrib.auth.decorators import login_required, user_passes_test from django.views.decorators.http import require_GET, require_POST from django.views.generic import TemplateView, ListView from .mixins import PageViewTrackingMixin, MenuContextMixin from .services import get_published_queryset, track_page_view from typing import Any, Dict, Type # menu = [ # {'title': "Главная", 'url_name': 'home'}, # {'title': "Проекты", 'url_name': 'solution'}, # # {'title': "Компетенции", 'url_name': 'ability'}, # {'title': "Отзывы", 'url_name': 'recall'}, # {'title': "Статьи", 'url_name': 'blog'}, # {'title': "Обо мне", 'url_name': 'about'} # ] class BasePageView(PageViewTrackingMixin, MenuContextMixin, TemplateView): """ Базовый класс для всех страниц сайта. Включает отслеживание просмотров и контекст меню. """ extra_context = {} class BaseListView(PageViewTrackingMixin, MenuContextMixin, ListView): """ Базовый класс для всех страниц со списками объектов. Включает отслеживание просмотров и контекст меню. """ extra_context = {} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(self.extra_context) return context class HomePageView(BasePageView): """Главная страница с формой обратной связи.""" template_name = 'programmer/index.html' def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) # Получаем контент для главной страницы posts = get_published_queryset(Home) context.update({ 'posts': posts, 'form': CallbackForm(), 'title': "Программист 1С Николай Сердюк - разработка и сопровождение", 'meta_description': ( "Профессиональный программист 1С с более чем 10-летним опытом. " "Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С." ), 'meta_keywords': ( "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, " "интеграция 1С, доработка 1С, 1С предприятие 8.3" ), }) return context class AboutPageView(BasePageView): """Страница 'О себе'.""" template_name = 'programmer/about.html' def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context.update({ 'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С", 'meta_description': ( "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. " "Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7." ), 'meta_keywords': ( "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, " "интеграция 1С, сертифицированный 1С, миграция 1С 7.7" ), }) return context class SolutionListView(BaseListView): """Список проектов с пагинацией.""" model = Solution template_name = 'programmer/solution.html' context_object_name = 'posts' paginate_by = 1 # Количество проектов на странице ordering = ['-time_create'] def get_queryset(self) -> QuerySet: """Возвращает только опубликованные проекты.""" return get_published_queryset(self.model, order_by='-time_create') def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context.update({ 'title': "Проекты автоматизации 1С | Реализованные кейсы и решения", 'meta_description': ( "Реализованные проекты по автоматизации 1С: складской учет с ТСД, " "интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы." ), 'meta_keywords': ( "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, " "миграция 1С 7.7, кейсы 1С, примеры работ 1С" ), }) return context class RecallListView(BaseListView): """Список отзывов с пагинацией.""" model = Recall template_name = 'programmer/recall.html' context_object_name = 'posts' paginate_by = 5 ordering = ['-time_create'] def get_queryset(self) -> QuerySet: """Возвращает только опубликованные отзывы.""" return get_published_queryset(self.model, order_by='-time_create') def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context.update({ 'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы", 'meta_description': ( "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. " "Отзывы от ООО «РОВЕН-Регионы» и других компаний." ), 'meta_keywords': ( "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, " "реальные кейсы 1С, отзыв ООО РОВЕН" ), }) return context @require_POST def callback_request(request: HttpRequest) -> HttpResponse: """ Обработка заявки на обратный звонок. Принимает только POST запросы. """ form = CallbackForm(request.POST) if form.is_valid(): # Сохраняем заявку callback = form.save() # TODO: В будущем можно добавить асинхронную отправку email через Celery # from .tasks import send_callback_email # send_callback_email.delay(callback.id) messages.success( request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.' ) else: # Собираем все ошибки формы for field, errors in form.errors.items(): field_label = form.fields[field].label if field in form.fields else field for error in errors: messages.error( request, f'❌ Ошибка в поле "{field_label}": {error}' ) return redirect('home') def show_post(request: HttpRequest, post_id: int) -> HttpResponse: """ Временная функция для отображения детальной страницы поста. TODO: Реализовать полноценный DetailView для каждой модели. """ return HttpResponse(f"Отображение поста № {post_id}") def pageNotFound(request: HttpRequest, exception: Exception) -> HttpResponseNotFound: """Обработчик 404 ошибки.""" return HttpResponseNotFound('

Страница не найдена

') def is_staff_user(user) -> bool: """Проверка, является ли пользователь сотрудником (для декоратора).""" return user.is_staff @login_required @user_passes_test(is_staff_user) def statistics_view(request: HttpRequest) -> HttpResponse: """ Статистика сайта для администраторов. TODO: Переделать на класс-based view с отдельным шаблоном. """ today = timezone.now().date() week_ago = today - timedelta(days=7) # Базовая статистика просмотров stats = { 'today_views': PageView.objects.filter(timestamp__date=today).count(), 'weekly_views': PageView.objects.filter(timestamp__date__gte=week_ago).count(), 'total_views': PageView.objects.count(), 'unique_visitors': Visitor.objects.filter(last_visit__date__gte=week_ago).count(), } # Популярные страницы за неделю stats['popular_pages'] = ( PageView.objects .filter(timestamp__date__gte=week_ago) .values('url') .annotate(views=Count('id')) .order_by('-views')[:10] ) # Последние посещения stats['recent_views'] = PageView.objects.order_by('-timestamp')[:20] # Статистика заявок stats.update({ 'total_callbacks': CallbackRequest.objects.count(), 'today_callbacks': CallbackRequest.objects.filter(time_create__date=today).count(), 'unread_callbacks': CallbackRequest.objects.filter(is_read=False).count(), }) return render(request, 'admin/statistics.html', stats) @require_GET def robots_txt(request: HttpRequest) -> HttpResponse: """Отдает robots.txt.""" return render(request, 'robots.txt', content_type='text/plain') def get_client_ip(request): """Получаем реальный IP клиента""" 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 def should_track_request(request): """Определяем, нужно ли отслеживать запрос""" client_ip = get_client_ip(request) path = request.path # Игнорируемые пути (Nextcloud специфичные) nextcloud_paths = [ '/index.php', '/status.php', '/cron', '/remote.php', '/ocs', '/apps/', '/custom_apps/', ] # Игнорируемые IP (Docker сети) docker_ips = [ '192.168.64.1', '192.168.65.1', '172.17.0.1', '172.18.0.1', '172.19.0.1', ] # Игнорируем статические файлы и админку if path.startswith('/static/') or path.startswith('/admin/'): return False # Не отслеживаем Nextcloud и Docker запросы if any(path.startswith(p) for p in nextcloud_paths): return False if client_ip in docker_ips: return False return True def track_page_view(request): """Основная функция отслеживания просмотров""" if not should_track_request(request): return try: PageView.objects.create( url=request.path, ip_address=get_client_ip(request), user_agent=request.META.get('HTTP_USER_AGENT', '')[:500], referer=request.META.get('HTTP_REFERER', '')[:500], ) except Exception as e: print(f"Error tracking page: {e}") def track_view(view_func): """Декоратор для отслеживания просмотров страниц""" from functools import wraps @wraps(view_func) def _wrapped_view(request, *args, **kwargs): # Отслеживаем просмотр перед выполнением view track_page_view(request) return view_func(request, *args, **kwargs) return _wrapped_view # @track_view # def index(request): # posts = Home.objects.filter(is_published=True) # context = { # 'posts': posts, # 'menu': menu, # 'title': "Программист 1С Николай Сердюк - разработка и сопровождение", # 'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С.", # 'meta_keywords': "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, интеграция 1С, доработка 1С, 1С предприятие 8.3", # 'form': CallbackForm() # } # return render(request, 'programmer/index.html', context=context) # @track_view # def about(request): # context = { # 'menu': menu, # 'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С", # 'meta_description': "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7.", # 'meta_keywords': "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, интеграция 1С, сертифицированный 1С, миграция 1С 7.7" # } # return render(request, 'programmer/about.html', context=context) # @track_view # def solution(request): # posts = Solution.objects.filter(is_published=True) # context = { # 'posts': posts, # 'menu': menu, # 'meta_description': "Реализованные проекты по автоматизации 1С: складской учет с ТСД, интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы.", # 'meta_keywords': "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, миграция 1С 7.7, кейсы 1С, примеры работ 1С", # 'title': "Проекты автоматизации 1С | Реализованные кейсы и решения", # } # return render(request, 'programmer/solution.html', context=context) # @track_view # def ability(request): # posts = Competence.objects.filter(is_published=True) # context = { # 'posts': posts, # 'menu': menu, # 'title': "Сертификаты и компетенции 1С | Программист 1С Николай Сердюк", # 'meta_description': "Сертификаты 1С: Профессионал по платформе 8.3 и БП 3.0. Подтвержденная квалификация программиста 1С с сертификатами фирмы 1С.", # 'meta_keywords': "сертификаты 1С, 1С профессионал, компетенции 1С, квалификация программиста 1С, сертифицированный специалист 1С" # } # return render(request, 'programmer/competence.html', context=context) # @track_view # def recall(request): # posts = Recall.objects.filter(is_published=True) # context = { # 'posts': posts, # 'menu': menu, # 'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы", # 'meta_description': "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. Отзывы от ООО «РОВЕН-Регионы» и других компаний.", # 'meta_keywords': "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, реальные кейсы 1С, отзыв ООО РОВЕН" # } # return render(request, 'programmer/recall.html', context=context) # def show_post(request, post_id): # return HttpResponse(f"Отображение № {post_id}") # def pageNotFound(request, exception): # return HttpResponseNotFound('

Страница не найдена

') # def callback_request(request): # if request.method == 'POST': # form = CallbackForm(request.POST) # if form.is_valid(): # # Сохраняем заявку через форму # form.save() # messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.') # return redirect('home') # else: # # Если форма невалидна, показываем ошибки # for field, errors in form.errors.items(): # for error in errors: # messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}') # return redirect('home') # # # Если GET запрос, просто показываем главную страницу # return redirect('home') # def is_admin(user): # return user.is_staff # def is_staff(user): # return user.is_staff # @login_required # @user_passes_test(is_staff) # def statistics_view(request): # today = timezone.now().date() # week_ago = today - timedelta(days=7) # # # Статистика за сегодня # today_views = PageView.objects.filter( # timestamp__date=today # ).count() # # # Статистика за неделю # weekly_views = PageView.objects.filter( # timestamp__date__gte=week_ago # ).count() # # # Всего просмотров # total_views = PageView.objects.count() # # # Популярные страницы за неделю # popular_pages = PageView.objects.filter( # timestamp__date__gte=week_ago # ).values('url').annotate( # views=Count('id') # ).order_by('-views')[:10] # # # Уникальные посетители за неделю # unique_visitors = Visitor.objects.filter( # last_visit__date__gte=week_ago # ).count() # # # Последние посещения # recent_views = PageView.objects.select_related().order_by('-timestamp')[:20] # # today = timezone.now().date() # total_callbacks = CallbackRequest.objects.count() # today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count() # unread_callbacks = CallbackRequest.objects.filter(is_read=False).count() # # context = { # 'today_views': today_views, # 'weekly_views': weekly_views, # 'total_views': total_views, # 'unique_visitors': unique_visitors, # 'popular_pages': popular_pages, # 'recent_views': recent_views, # 'total_callbacks': total_callbacks, # 'today_callbacks': today_callbacks, # 'unread_callbacks': unread_callbacks, # } # # return render(request, 'admin/statistics.html', context) # @require_GET # def robots_txt(request): # return render(request, 'robots.txt', content_type='text/plain')