diff --git a/blog/models.py b/blog/models.py index c60bec1..a315695 100644 --- a/blog/models.py +++ b/blog/models.py @@ -65,6 +65,8 @@ class Article(models.Model): return f"{self.title} | Блог программиста 1С" def get_seo_description(self): + if self.meta_description: + return self.meta_description clean = self.content[:160].replace("\n", " ").strip() return f"{clean}..." diff --git a/blog/services.py b/blog/services.py index d781a84..7b3e1d2 100644 --- a/blog/services.py +++ b/blog/services.py @@ -1,5 +1,5 @@ from typing import Optional, List -from django.db.models import QuerySet +from django.db.models import F, QuerySet from django.core.cache import cache from .models import Article, Category, Comment @@ -14,7 +14,7 @@ def get_published_articles(category_slug: Optional[str] = None) -> QuerySet[Arti qs = Article.objects.filter(is_published=True).select_related('category') if category_slug: qs = qs.filter(category__slug=category_slug) - articles = qs.order_by('-time_create') + articles = list(qs.order_by('-time_create')) cache.set(cache_key, articles, 300) # 5 минут return articles @@ -27,8 +27,7 @@ def get_article_by_slug(slug: str) -> Optional[Article]: def increment_article_views(article: Article) -> None: """Увеличивает счётчик просмотров статьи.""" - article.views_count += 1 - article.save(update_fields=['views_count']) + Article.objects.filter(pk=article.pk).update(views_count=F('views_count') + 1) def add_comment_to_article(article: Article, data: dict) -> Comment: """Создание комментария к статье (без модерации).""" diff --git a/blog/templates/blog/article_detail.html b/blog/templates/blog/article_detail.html index 2f7ab68..030adea 100644 --- a/blog/templates/blog/article_detail.html +++ b/blog/templates/blog/article_detail.html @@ -59,6 +59,24 @@ {% endif %} + + + + + + +{% endblock %} + {% block extra_js %} {% endblock %} - - - - - - - -{% endblock %} \ No newline at end of file diff --git a/blog/tests.py b/blog/tests.py index 7c4cfda..f390377 100644 --- a/blog/tests.py +++ b/blog/tests.py @@ -47,5 +47,5 @@ class ArticleListViewTest(TestCase): def test_service_returns_published(self): articles = get_published_articles() - self.assertEqual(articles.count(), 15) + self.assertEqual(len(articles), 15) diff --git a/blog/views.py b/blog/views.py index 5b3b4ae..021605a 100644 --- a/blog/views.py +++ b/blog/views.py @@ -55,101 +55,85 @@ class ArticleListView(MenuContextMixin, BreadcrumbMixin, ListView): ).first() return context - @method_decorator(cache_page(60 * 5)) # кэширование страницы на 5 минут def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) -class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView, CreateView): - """Детальная страница статьи + форма комментария""" +class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView): + """Детальная страница статьи с формой комментария.""" model = Article template_name = 'blog/article_detail.html' context_object_name = 'article' - form_class = CommentForm + + def get_object(self, queryset=None): + """Получаем статью по slug, только опубликованные.""" + if not hasattr(self, '_article_cache'): + article = get_article_by_slug(self.kwargs['slug']) + if article is None: + raise Http404("Статья не найдена") + self._article_cache = article + return self._article_cache def get_breadcrumbs(self): - article = self.get_object() + article = self.object # already set by the time breadcrumbs are called return [ {'title': 'Статьи', 'url_name': 'blog:article_list'}, { 'title': article.category.name, 'url_name': 'blog:category_detail', 'category_slug': article.category.slug, - # 'url_args': [article.category.slug] }, {'title': article.title, 'url_name': None}, ] - def get_object(self, queryset=None): - article = get_article_by_slug(self.kwargs['slug']) - if article is None: - raise Http404("Статья не найдена") - return article - def get(self, request, *args, **kwargs): - # Увеличиваем счётчик просмотров при GET запросе + """Устанавливаем self.object один раз, увеличиваем счётчик просмотров.""" self.object = self.get_object() increment_article_views(self.object) - return super().get(request, *args, **kwargs) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) def post(self, request, *args, **kwargs): - # Проверяем, авторизован ли пользователь + """Обработка формы комментария.""" + self.object = self.get_object() + if not request.user.is_authenticated: messages.error(request, "❌ Только авторизованные пользователи могут оставлять комментарии.") - return redirect(request.path) # redirect('login') или redirect(request.path) чтобы остаться на странице + return redirect(request.path) - self.object = self.get_object() - form = self.get_form() + form = CommentForm(request.POST, request=request) if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) + add_comment_to_article( + article=self.object, + data=form.cleaned_data + ) + messages.success(request, "✅ Ваш комментарий отправлен на модерацию.") + return redirect(self.object.get_absolute_url()) - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['request'] = self.request - return kwargs - - def get_initial(self): - """Возвращает пустые начальные данные для формы комментария.""" - initial = super().get_initial() - initial['content'] = '' - return initial - - def form_valid(self, form): - # Для авторизованных пользователей гарантируем наличие имени и email - if self.request.user.is_authenticated: - form.cleaned_data['author_name'] = self.request.user.get_full_name() or self.request.user.username - form.cleaned_data['author_email'] = self.request.user.email - - comment = add_comment_to_article( - article=self.object, - data=form.cleaned_data - ) - messages.success(self.request, "Ваш комментарий отправлен на модерацию.") - return redirect(self.object.get_absolute_url()) + # Форма невалидна — показываем страницу снова с ошибками + context = self.get_context_data(object=self.object, form=form) + return self.render_to_response(context) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + + # Используем form из kwargs если передана (например при ошибке валидации) + if 'form' not in kwargs: + context['form'] = CommentForm(request=self.request) + context.update({ - 'title': "Статьи | Инструкции", - 'meta_description': ( - "Инструкции по работе в 1С и бизнес-решения" - "инструкции, статьи помощь 1С." - ), - 'meta_keywords': ( - "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, " - "миграция 1С 7.7, кейсы 1С, примеры работ 1С" - ), + 'moderated_comments': self.object.comments.filter(is_moderated=True), 'user_is_authenticated': self.request.user.is_authenticated, + 'title': self.object.get_seo_title(), + 'meta_description': self.object.get_seo_description(), + 'meta_keywords': ', '.join( + tag.name for tag in self.object.tags.all() + ), }) - # Добавляем только одобренные комментарии - context['moderated_comments'] = self.object.comments.filter(is_moderated=True) return context - @method_decorator(staff_member_required, name='dispatch') -class ArticleDraftPreviewView(DetailView): +class ArticleDraftPreviewView(MenuContextMixin, BreadcrumbMixin, DetailView): model = Article template_name = 'blog/article_detail.html' # используем тот же шаблон context_object_name = 'article' diff --git a/programmer/views.py b/programmer/views.py index fd1ff73..d00ab98 100644 --- a/programmer/views.py +++ b/programmer/views.py @@ -122,7 +122,7 @@ class SolutionListView(BaseListView, BreadcrumbMixin): model = Solution template_name = 'programmer/solution.html' context_object_name = 'posts' - paginate_by = 1 # Количество проектов на странице + paginate_by = 5 # Количество проектов на странице cards_template = 'programmer/includes/project_cards.html' ordering = ['-time_create'] @@ -273,7 +273,7 @@ class SolutionDetailView(MenuContextMixin, BreadcrumbMixin, DetailView): model = Solution template_name = 'programmer/solution_detail.html' context_object_name = 'solution' - # Отображаются только опубликованные объекты; для неопубликованных выводится ошибка 404 + # Отображаются только опубликованные объекты; для неопубликованных выводится ошибка 404 queryset = Solution.objects.filter(is_published=True) def get_breadcrumbs(self):