Site/programmer/views.py

425 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponse, HttpResponseNotFound, HttpRequest, JsonResponse
from django.urls import reverse_lazy
from .models import *
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import CallbackForm, ProfileForm, UserEditForm, RegistrationForm
from django.utils import timezone
from datetime import timedelta
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, CreateView, UpdateView, DetailView
from .mixins import PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin
from .services import get_published_queryset, track_page_view
from typing import Any, Dict, Type
from django.template.loader import render_to_string
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
def render_to_response(self, context, **response_kwargs):
# Если это AJAX-запрос, возвращаем JSON с HTML фрагментом
if self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
# Определяем шаблон для карточек
cards_template = getattr(self, 'cards_template', None)
if not cards_template:
# Генерируем имя по умолчанию: app/includes/model_cards.html
cards_template = f"{self.model._meta.app_label}/includes/{self.model._meta.model_name}_cards.html"
html = render_to_string(
cards_template,
{self.context_object_name: context[self.context_object_name]},
request=self.request
)
return JsonResponse({
'html': html,
'has_next': context['page_obj'].has_next() if context.get('page_obj') else False
})
# Обычный запрос
return super().render_to_response(context, **response_kwargs)
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)
form = CallbackForm()
if self.request.user.is_authenticated:
if 'captcha' in form.fields:
del form.fields['captcha']
context.update({
'posts': posts,
'form': form,
'title': "Программист 1С Николай Сердюк - разработка и сопровождение",
'meta_description': (
"Профессиональный программист 1С с более чем 10-летним опытом. "
"Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С."
),
'meta_keywords': (
"программист 1С, разработка 1С, обновление 1С, сопровождение 1С, "
"интеграция 1С, доработка 1С, 1С предприятие 8.3"
),
})
return context
class AboutPageView(BasePageView, BreadcrumbMixin):
"""Страница 'О себе'."""
template_name = 'programmer/about.html'
def get_breadcrumbs(self):
return [{'title': 'Обо мне', 'url_name': None}]
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, BreadcrumbMixin):
"""Список проектов с пагинацией."""
model = Solution
template_name = 'programmer/solution.html'
context_object_name = 'posts'
paginate_by = 5 # Количество проектов на странице
cards_template = 'programmer/includes/project_cards.html'
ordering = ['-time_create']
def get_breadcrumbs(self):
return [{'title': 'Проекты', 'url_name': None}]
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, BreadcrumbMixin):
"""Список отзывов с пагинацией."""
model = Recall
template_name = 'programmer/recall.html'
context_object_name = 'posts'
paginate_by = 5
ordering = ['-time_create']
def get_breadcrumbs(self):
return [{'title': 'Отзывы', 'url_name': None}]
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
class ProfileView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin, TemplateView):
template_name = 'programmer/profile.html'
def get_breadcrumbs(self):
return [{'title': 'Профиль', 'url_name': None}]
class RegisterView(PageViewTrackingMixin, MenuContextMixin, SuccessMessageMixin, BreadcrumbMixin, CreateView):
"""Регистрация нового пользователя"""
template_name = 'programmer/register.html'
form_class = RegistrationForm
# success_url = reverse_lazy('profile')
success_message = '✅ Регистрация успешна!'
def get_breadcrumbs(self):
return [{'title': 'Регистрация', 'url_name': None}]
def get_success_url(self):
# Если есть параметр next, используем его
next_url = self.request.POST.get('next') or self.request.GET.get('next')
if next_url:
return next_url
return reverse_lazy('profile')
def form_valid(self, form):
# Сохраняем пользователя
response = super().form_valid(form)
# Автоматически логиним после регистрации
user = self.object
login(self.request, user)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['next'] = self.request.GET.get('next', '')
return context
class ProfileEditView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin, UpdateView):
model = Profile
form_class = ProfileForm
template_name = 'programmer/profile_edit.html'
success_url = reverse_lazy('profile')
def get_breadcrumbs(self):
return [
{'title': 'Профиль', 'url_name': 'profile'},
{'title': 'Редактирование', 'url_name': None},
]
def get_object(self, queryset=None):
# Возвращаем профиль текущего пользователя
return self.request.user.profile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Передаём формы в контекст
if 'user_form' not in context:
context['user_form'] = UserEditForm(instance=self.request.user)
if 'profile_form' not in context:
context['profile_form'] = ProfileForm(instance=self.request.user.profile)
return context
def post(self, request, *args, **kwargs):
user_form = UserEditForm(request.POST, instance=request.user)
profile_form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, '✅ Профиль успешно обновлён.')
return redirect('profile')
else:
# Если формы не валидны, показываем ошибки
context = self.get_context_data()
context['user_form'] = user_form
context['profile_form'] = profile_form
return self.render_to_response(context)
class PrivacyPolicyView(TemplateView, MenuContextMixin, BreadcrumbMixin):
template_name = 'programmer/privacy.html'
def get_breadcrumbs(self):
return [{'title': 'Политика конфиденциальности', 'url_name': None}]
class SolutionDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
model = Solution
template_name = 'programmer/solution_detail.html'
context_object_name = 'solution'
# Отображаются только опубликованные объекты; для неопубликованных выводится ошибка 404
queryset = Solution.objects.filter(is_published=True)
def get_breadcrumbs(self):
return [
{'title': 'Проекты', 'url_name': 'solution'},
{'title': self.object.title, 'url_name': None},
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.object.get_seo_title()
context['meta_description'] = self.object.get_seo_description()
return context
class RecallDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
model = Recall
template_name = 'programmer/recall_detail.html'
context_object_name = 'recall'
queryset = Recall.objects.filter(is_published=True)
def get_breadcrumbs(self):
return [
{'title': 'Отзывы', 'url_name': 'recall'},
{'title': self.object.title, 'url_name': None},
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.object.get_seo_title()
context['meta_description'] = self.object.get_seo_description()
return context
class CompetenceDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
model = Competence
template_name = 'programmer/competence_detail.html'
context_object_name = 'competence'
queryset = Competence.objects.filter(is_published=True)
def get_breadcrumbs(self):
return [
{'title': 'Компетенции', 'url_name': 'ability'},
{'title': self.object.title, 'url_name': None},
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
@require_POST
def callback_request(request: HttpRequest) -> HttpResponse:
"""
Обработка заявки на обратный звонок.
Принимает только POST запросы.
"""
form = CallbackForm(request.POST)
if request.user.is_authenticated:
if 'captcha' in form.fields:
del form.fields['captcha']
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 pageNotFound(request: HttpRequest, exception: Exception) -> HttpResponseNotFound:
"""Обработчик 404 ошибки."""
return HttpResponseNotFound('<h1>Страница не найдена</h1>')
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': stats})
@require_GET
def robots_txt(request: HttpRequest) -> HttpResponse:
"""Отдает robots.txt."""
return render(request, 'programmer/robots.txt', content_type='text/plain')
@require_GET
def yandex_html(request: HttpRequest) -> HttpResponse:
"""Отдает yandex_cdc16c33291495b9.html."""
return render(request, 'programmer/yandex_cdc16c33291495b9.html', content_type='text/html')