mirror of
https://github.com/EDeev/deev.space.git
synced 2026-06-15 11:01:10 +03:00
494 lines
No EOL
18 KiB
Python
494 lines
No EOL
18 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
||
from django.http import JsonResponse
|
||
from django.views.generic import ListView, DetailView, TemplateView
|
||
from django.contrib.auth import login, authenticate, logout
|
||
from django.contrib.auth.decorators import login_required
|
||
from django.contrib import messages
|
||
from django.core.paginator import Paginator
|
||
from django.core.mail import send_mail
|
||
from django.conf import settings
|
||
from django.db.models import Q, Count
|
||
from django.views.decorators.http import require_POST, require_GET
|
||
from django.utils import timezone
|
||
import json
|
||
import logging
|
||
|
||
from .models import (
|
||
Article, Project, Skill, Comment, ArticleLike, CommentLike,
|
||
ContactMessage, Experience, Education, Category, SiteSettings
|
||
)
|
||
from .forms import RegisterForm, LoginForm, CommentForm, ContactForm
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_site_settings():
|
||
"""Получение настроек сайта."""
|
||
return SiteSettings.load()
|
||
|
||
|
||
class IndexView(TemplateView):
|
||
"""Главная страница."""
|
||
template_name = 'index.html'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
site_settings = get_site_settings()
|
||
|
||
context['site_settings'] = site_settings
|
||
context['skills'] = Skill.objects.all()
|
||
context['skills_by_category'] = self._get_skills_by_category()
|
||
context['recent_articles'] = Article.objects.filter(
|
||
is_published=True, is_achievement=False
|
||
).select_related('category')[:3]
|
||
context['featured_projects'] = Project.objects.filter(
|
||
is_visible=True
|
||
).order_by('order', '-date')[:6]
|
||
context['experiences'] = Experience.objects.all()[:2]
|
||
|
||
context['page_title'] = f'{site_settings.owner_name} — {site_settings.owner_title}'
|
||
context[
|
||
'page_description'] = site_settings.site_description or f'Персональный сайт {site_settings.owner_title.lower()} {site_settings.owner_name}'
|
||
|
||
return context
|
||
|
||
def _get_skills_by_category(self):
|
||
skills = Skill.objects.all()
|
||
categories = {}
|
||
for skill in skills:
|
||
cat_display = skill.get_category_display()
|
||
if cat_display not in categories:
|
||
categories[cat_display] = []
|
||
categories[cat_display].append(skill)
|
||
return categories
|
||
|
||
|
||
class AboutView(TemplateView):
|
||
"""Страница 'Обо мне'."""
|
||
template_name = 'about.html'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
site_settings = get_site_settings()
|
||
|
||
context['site_settings'] = site_settings
|
||
context['experiences'] = Experience.objects.all()
|
||
context['educations'] = Education.objects.all()
|
||
context['skills'] = Skill.objects.all()
|
||
context['skills_by_category'] = self._get_skills_by_category()
|
||
|
||
context['page_title'] = f'Обо мне — {site_settings.owner_name}'
|
||
context[
|
||
'page_description'] = f'Профессиональный путь, образование и навыки {site_settings.owner_title.lower()} {site_settings.owner_name}'
|
||
|
||
return context
|
||
|
||
def _get_skills_by_category(self):
|
||
skills = Skill.objects.all()
|
||
categories = {}
|
||
for skill in skills:
|
||
cat_display = skill.get_category_display()
|
||
if cat_display not in categories:
|
||
categories[cat_display] = []
|
||
categories[cat_display].append(skill)
|
||
return categories
|
||
|
||
|
||
class ProjectsView(ListView):
|
||
"""Страница проектов."""
|
||
model = Project
|
||
template_name = 'projects.html'
|
||
context_object_name = 'projects'
|
||
|
||
def get_queryset(self):
|
||
queryset = Project.objects.filter(is_visible=True)
|
||
tech_filter = self.request.GET.get('tech')
|
||
status_filter = self.request.GET.get('status')
|
||
|
||
if tech_filter:
|
||
queryset = queryset.filter(technologies__icontains=tech_filter)
|
||
|
||
# Исправление фильтрации по статусу
|
||
if status_filter:
|
||
if status_filter == 'in_development':
|
||
# Включаем оба статуса: in_development и beta
|
||
queryset = queryset.filter(status__in=['in_development', 'beta'])
|
||
else:
|
||
queryset = queryset.filter(status=status_filter)
|
||
|
||
return queryset.order_by('order', '-date')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
site_settings = get_site_settings()
|
||
|
||
# Получаем все видимые проекты для правильной фильтрации
|
||
base_queryset = Project.objects.filter(is_visible=True)
|
||
|
||
tech_filter = self.request.GET.get('tech')
|
||
status_filter = self.request.GET.get('status')
|
||
|
||
if tech_filter:
|
||
base_queryset = base_queryset.filter(technologies__icontains=tech_filter)
|
||
|
||
# Разделение по статусам с учетом фильтра
|
||
if status_filter == 'completed':
|
||
context['completed_projects'] = base_queryset.filter(status='completed').order_by('order', '-date')
|
||
context['dev_projects'] = Project.objects.none()
|
||
elif status_filter == 'in_development':
|
||
context['completed_projects'] = Project.objects.none()
|
||
context['dev_projects'] = base_queryset.filter(status__in=['in_development', 'beta']).order_by('order',
|
||
'-date')
|
||
else:
|
||
context['completed_projects'] = base_queryset.filter(status='completed').order_by('order', '-date')
|
||
context['dev_projects'] = base_queryset.filter(status__in=['in_development', 'beta']).order_by('order',
|
||
'-date')
|
||
|
||
# Все технологии для фильтра
|
||
all_techs = set()
|
||
for project in Project.objects.filter(is_visible=True):
|
||
all_techs.update(project.get_technologies_list())
|
||
context['all_technologies'] = sorted(all_techs)
|
||
|
||
context['current_tech'] = self.request.GET.get('tech', '')
|
||
context['current_status'] = self.request.GET.get('status', '')
|
||
|
||
context['page_title'] = f'Проекты — {site_settings.owner_name}'
|
||
context[
|
||
'page_description'] = 'Портфолио проектов: Telegram-боты, веб-приложения, базы данных и инструменты разработки'
|
||
|
||
return context
|
||
|
||
|
||
class BlogView(ListView):
|
||
"""Страница блога."""
|
||
model = Article
|
||
template_name = 'blog/blog.html'
|
||
context_object_name = 'articles'
|
||
paginate_by = 9
|
||
|
||
def get_queryset(self):
|
||
queryset = Article.objects.filter(
|
||
is_published=True, is_achievement=False
|
||
).select_related('category')
|
||
|
||
category_slug = self.kwargs.get('category_slug')
|
||
if category_slug:
|
||
queryset = queryset.filter(category__slug=category_slug)
|
||
|
||
search_query = self.request.GET.get('q')
|
||
if search_query:
|
||
queryset = queryset.filter(
|
||
Q(title__icontains=search_query) |
|
||
Q(excerpt__icontains=search_query) |
|
||
Q(post__icontains=search_query)
|
||
)
|
||
|
||
return queryset.order_by('-date')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
site_settings = get_site_settings()
|
||
|
||
context['categories'] = Category.objects.annotate(
|
||
count=Count('articles', filter=Q(articles__is_published=True, articles__is_achievement=False))
|
||
).filter(count__gt=0)
|
||
|
||
category_slug = self.kwargs.get('category_slug')
|
||
context['current_category'] = None
|
||
if category_slug:
|
||
context['current_category'] = get_object_or_404(Category, slug=category_slug)
|
||
|
||
context['search_query'] = self.request.GET.get('q', '')
|
||
context['total_articles'] = Article.objects.filter(is_published=True, is_achievement=False).count()
|
||
|
||
context['page_title'] = f'Блог — {site_settings.owner_name}'
|
||
context['page_description'] = 'Технические статьи, гайды и размышления о разработке'
|
||
|
||
return context
|
||
|
||
|
||
class ArticleDetailView(DetailView):
|
||
"""Страница отдельной статьи."""
|
||
model = Article
|
||
template_name = 'blog/article.html'
|
||
context_object_name = 'article'
|
||
slug_url_kwarg = 'slug'
|
||
|
||
def get_queryset(self):
|
||
return Article.objects.filter(is_published=True).select_related('category')
|
||
|
||
def get_object(self, queryset=None):
|
||
obj = super().get_object(queryset)
|
||
obj.views += 1
|
||
obj.save(update_fields=['views'])
|
||
return obj
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
article = self.object
|
||
|
||
context['comments'] = article.comments.filter(
|
||
is_approved=True, parent=None
|
||
).select_related('user').prefetch_related(
|
||
'replies__user', 'replies__replies__user'
|
||
).order_by('-created_at')
|
||
|
||
context['comment_form'] = CommentForm()
|
||
|
||
context['user_like'] = None
|
||
if self.request.user.is_authenticated:
|
||
like = ArticleLike.objects.filter(
|
||
article=article, user=self.request.user
|
||
).first()
|
||
context['user_like'] = like
|
||
|
||
context['related_articles'] = Article.objects.filter(
|
||
is_published=True, is_achievement=False, category=article.category
|
||
).exclude(pk=article.pk)[:3] if article.category else Article.objects.none()
|
||
|
||
context['page_title'] = f'{article.title} — Блог'
|
||
context['page_description'] = article.excerpt or article.post[:160]
|
||
context['og_image'] = article.img.url if article.img else None
|
||
|
||
return context
|
||
|
||
|
||
class AchievementsView(ListView):
|
||
"""Страница достижений."""
|
||
model = Article
|
||
template_name = 'achievements.html'
|
||
context_object_name = 'achievements'
|
||
|
||
def get_queryset(self):
|
||
return Article.objects.filter(
|
||
is_published=True, is_achievement=True
|
||
).order_by('-achievement_date', '-date')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
site_settings = get_site_settings()
|
||
|
||
context['page_title'] = f'Достижения — {site_settings.owner_name}'
|
||
context['page_description'] = 'Награды, сертификаты и профессиональные достижения'
|
||
|
||
return context
|
||
|
||
|
||
class ContactsView(TemplateView):
|
||
"""Страница контактов."""
|
||
template_name = 'contacts.html'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
site_settings = get_site_settings()
|
||
|
||
context['site_settings'] = site_settings
|
||
context['contact_form'] = ContactForm()
|
||
|
||
context['smartcaptcha_client_key'] = settings.SMARTCAPTCHA_CLIENT_KEY
|
||
|
||
context['page_title'] = f'Контакты — {site_settings.owner_name}'
|
||
context['page_description'] = 'Свяжитесь со мной: Telegram, email, социальные сети и форма обратной связи'
|
||
|
||
return context
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
form = ContactForm(request.POST)
|
||
if form.is_valid():
|
||
message = form.save()
|
||
|
||
try:
|
||
send_mail(
|
||
subject=f'[deev.space] Новое сообщение: {message.subject}',
|
||
message=f'От: {message.name} ({message.email})\n\nТема: {message.subject}\n\n{message.message}',
|
||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||
recipient_list=[settings.CONTACT_EMAIL],
|
||
fail_silently=True,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Ошибка отправки email: {e}')
|
||
|
||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||
return JsonResponse({'success': True, 'message': 'Сообщение успешно отправлено!'})
|
||
|
||
messages.success(request, 'Сообщение успешно отправлено!')
|
||
return redirect('contacts')
|
||
|
||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
|
||
|
||
context = self.get_context_data()
|
||
context['contact_form'] = form
|
||
return render(request, self.template_name, context)
|
||
|
||
|
||
def register_view(request):
|
||
"""Регистрация пользователя."""
|
||
if request.user.is_authenticated:
|
||
return redirect('index')
|
||
|
||
if request.method == 'POST':
|
||
form = RegisterForm(request.POST)
|
||
if form.is_valid():
|
||
user = form.save()
|
||
login(request, user)
|
||
messages.success(request, f'Добро пожаловать, {user.username}!')
|
||
|
||
next_url = request.GET.get('next', 'index')
|
||
return redirect(next_url)
|
||
else:
|
||
form = RegisterForm()
|
||
|
||
return render(request, 'auth/register.html', {
|
||
'form': form,
|
||
'page_title': 'Регистрация — deev.space'
|
||
})
|
||
|
||
|
||
def login_view(request):
|
||
"""Авторизация пользователя."""
|
||
if request.user.is_authenticated:
|
||
return redirect('index')
|
||
|
||
if request.method == 'POST':
|
||
form = LoginForm(request, data=request.POST)
|
||
if form.is_valid():
|
||
user = form.get_user()
|
||
login(request, user)
|
||
messages.success(request, f'С возвращением, {user.username}!')
|
||
|
||
next_url = request.GET.get('next', 'index')
|
||
return redirect(next_url)
|
||
else:
|
||
form = LoginForm()
|
||
|
||
return render(request, 'auth/login.html', {
|
||
'form': form,
|
||
'page_title': 'Вход — deev.space'
|
||
})
|
||
|
||
|
||
def logout_view(request):
|
||
"""Выход из системы."""
|
||
logout(request)
|
||
messages.info(request, 'Вы вышли из системы')
|
||
return redirect('index')
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
def add_comment(request, article_id):
|
||
"""Добавление комментария к статье."""
|
||
article = get_object_or_404(Article, id=article_id, is_published=True)
|
||
form = CommentForm(request.POST)
|
||
|
||
if form.is_valid():
|
||
comment = form.save(commit=False)
|
||
comment.article = article
|
||
comment.user = request.user
|
||
|
||
parent_id = request.POST.get('parent_id')
|
||
if parent_id:
|
||
parent = get_object_or_404(Comment, id=parent_id, article=article)
|
||
if parent.nesting_level < 3:
|
||
comment.parent = parent
|
||
|
||
comment.save()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'comment': {
|
||
'id': comment.id,
|
||
'user': comment.user.username,
|
||
'avatar_letter': comment.user.get_avatar_letter(),
|
||
'content': comment.content,
|
||
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
|
||
'likes': 0,
|
||
'dislikes': 0,
|
||
}
|
||
})
|
||
|
||
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
def toggle_article_like(request, article_id):
|
||
"""Лайк/дизлайк статьи."""
|
||
article = get_object_or_404(Article, id=article_id, is_published=True)
|
||
|
||
try:
|
||
data = json.loads(request.body)
|
||
is_like = data.get('is_like', True)
|
||
except json.JSONDecodeError:
|
||
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
||
|
||
like, created = ArticleLike.objects.get_or_create(
|
||
article=article, user=request.user,
|
||
defaults={'is_like': is_like}
|
||
)
|
||
|
||
user_vote = None
|
||
if not created:
|
||
if like.is_like == is_like:
|
||
like.delete()
|
||
else:
|
||
like.is_like = is_like
|
||
like.save()
|
||
user_vote = is_like
|
||
else:
|
||
user_vote = is_like
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'likes': article.likes_count,
|
||
'dislikes': article.dislikes_count,
|
||
'user_vote': user_vote
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
def toggle_comment_like(request, comment_id):
|
||
"""Лайк/дизлайк комментария."""
|
||
comment = get_object_or_404(Comment, id=comment_id, is_approved=True)
|
||
|
||
try:
|
||
data = json.loads(request.body)
|
||
is_like = data.get('is_like', True)
|
||
except json.JSONDecodeError:
|
||
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
||
|
||
like, created = CommentLike.objects.get_or_create(
|
||
comment=comment, user=request.user,
|
||
defaults={'is_like': is_like}
|
||
)
|
||
|
||
user_vote = None
|
||
if not created:
|
||
if like.is_like == is_like:
|
||
like.delete()
|
||
else:
|
||
like.is_like = is_like
|
||
like.save()
|
||
user_vote = is_like
|
||
else:
|
||
user_vote = is_like
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'likes': comment.likes_count,
|
||
'dislikes': comment.dislikes_count,
|
||
'user_vote': user_vote
|
||
})
|
||
|
||
|
||
def handler404(request, exception):
|
||
"""Обработчик ошибки 404."""
|
||
return render(request, 'errors/404.html', status=404)
|
||
|
||
|
||
def handler500(request):
|
||
"""Обработчик ошибки 500."""
|
||
return render(request, 'errors/500.html', status=500) |