425 lines
17 KiB
Python
425 lines
17 KiB
Python
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):
|
||
"""Отдает robots.txt."""
|
||
return render(request, 'programmer/robots.txt', content_type='text/plain; charset=utf-8')
|
||
|
||
@require_GET
|
||
def yandex_html(request: HttpRequest) -> HttpResponse:
|
||
"""Отдает yandex_cdc16c33291495b9.html."""
|
||
return render(request, 'programmer/yandex_cdc16c33291495b9.html', content_type='text/html')
|