Оптимизация блога с помощью клауд

This commit is contained in:
NikDizell 2026-03-08 01:53:36 +03:00
parent d048510f68
commit c95f27696b
6 changed files with 67 additions and 82 deletions

View File

@ -65,6 +65,8 @@ class Article(models.Model):
return f"{self.title} | Блог программиста 1С" return f"{self.title} | Блог программиста 1С"
def get_seo_description(self): def get_seo_description(self):
if self.meta_description:
return self.meta_description
clean = self.content[:160].replace("\n", " ").strip() clean = self.content[:160].replace("\n", " ").strip()
return f"{clean}..." return f"{clean}..."

View File

@ -1,5 +1,5 @@
from typing import Optional, List 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 django.core.cache import cache
from .models import Article, Category, Comment 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') qs = Article.objects.filter(is_published=True).select_related('category')
if category_slug: if category_slug:
qs = qs.filter(category__slug=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 минут cache.set(cache_key, articles, 300) # 5 минут
return articles return articles
@ -27,8 +27,7 @@ def get_article_by_slug(slug: str) -> Optional[Article]:
def increment_article_views(article: Article) -> None: def increment_article_views(article: Article) -> None:
"""Увеличивает счётчик просмотров статьи.""" """Увеличивает счётчик просмотров статьи."""
article.views_count += 1 Article.objects.filter(pk=article.pk).update(views_count=F('views_count') + 1)
article.save(update_fields=['views_count'])
def add_comment_to_article(article: Article, data: dict) -> Comment: def add_comment_to_article(article: Article, data: dict) -> Comment:
"""Создание комментария к статье (без модерации).""" """Создание комментария к статье (без модерации)."""

View File

@ -59,6 +59,24 @@
{% endif %} {% endif %}
</div> </div>
<!-- Скрипт для модального окна -->
<script src="{% static 'programmer/js/recall.js' %}"></script>
<!-- Модальное окно для увеличения изображений -->
<div id="imageModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Просмотр изображения</h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<img class="modal-image" id="modalImage" alt="">
</div>
</div>
</div>
{% endblock %}
{% block extra_js %} {% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
@ -77,21 +95,3 @@
}); });
</script> </script>
{% endblock %} {% endblock %}
<!-- Скрипт для модального окна -->
<script src="{% static 'programmer/js/recall.js' %}"></script>
<!-- Модальное окно для увеличения изображений -->
<div id="imageModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Просмотр изображения</h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<img class="modal-image" id="modalImage" alt="">
</div>
</div>
</div>
{% endblock %}

View File

@ -47,5 +47,5 @@ class ArticleListViewTest(TestCase):
def test_service_returns_published(self): def test_service_returns_published(self):
articles = get_published_articles() articles = get_published_articles()
self.assertEqual(articles.count(), 15) self.assertEqual(len(articles), 15)

View File

@ -55,101 +55,85 @@ class ArticleListView(MenuContextMixin, BreadcrumbMixin, ListView):
).first() ).first()
return context return context
@method_decorator(cache_page(60 * 5)) # кэширование страницы на 5 минут
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView, CreateView): class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
"""Детальная страница статьи + форма комментария""" """Детальная страница статьи с формой комментария."""
model = Article model = Article
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html'
context_object_name = 'article' 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): def get_breadcrumbs(self):
article = self.get_object() article = self.object # already set by the time breadcrumbs are called
return [ return [
{'title': 'Статьи', 'url_name': 'blog:article_list'}, {'title': 'Статьи', 'url_name': 'blog:article_list'},
{ {
'title': article.category.name, 'title': article.category.name,
'url_name': 'blog:category_detail', 'url_name': 'blog:category_detail',
'category_slug': article.category.slug, 'category_slug': article.category.slug,
# 'url_args': [article.category.slug]
}, },
{'title': article.title, 'url_name': None}, {'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): def get(self, request, *args, **kwargs):
# Увеличиваем счётчик просмотров при GET запросе """Устанавливаем self.object один раз, увеличиваем счётчик просмотров."""
self.object = self.get_object() self.object = self.get_object()
increment_article_views(self.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): def post(self, request, *args, **kwargs):
# Проверяем, авторизован ли пользователь """Обработка формы комментария."""
self.object = self.get_object()
if not request.user.is_authenticated: if not request.user.is_authenticated:
messages.error(request, "❌ Только авторизованные пользователи могут оставлять комментарии.") messages.error(request, "❌ Только авторизованные пользователи могут оставлять комментарии.")
return redirect(request.path) # redirect('login') или redirect(request.path) чтобы остаться на странице return redirect(request.path)
self.object = self.get_object() form = CommentForm(request.POST, request=request)
form = self.get_form()
if form.is_valid(): if form.is_valid():
return self.form_valid(form) add_comment_to_article(
else:
return self.form_invalid(form)
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, article=self.object,
data=form.cleaned_data data=form.cleaned_data
) )
messages.success(self.request, "Ваш комментарий отправлен на модерацию.") messages.success(request, "✅ Ваш комментарий отправлен на модерацию.")
return redirect(self.object.get_absolute_url()) 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Используем form из kwargs если передана (например при ошибке валидации)
if 'form' not in kwargs:
context['form'] = CommentForm(request=self.request)
context.update({ context.update({
'title': "Статьи | Инструкции", 'moderated_comments': self.object.comments.filter(is_moderated=True),
'meta_description': (
"Инструкции по работе в 1С и бизнес-решения"
"инструкции, статьи помощь 1С."
),
'meta_keywords': (
"проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, "
"миграция 1С 7.7, кейсы 1С, примеры работ 1С"
),
'user_is_authenticated': self.request.user.is_authenticated, '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 return context
@method_decorator(staff_member_required, name='dispatch') @method_decorator(staff_member_required, name='dispatch')
class ArticleDraftPreviewView(DetailView): class ArticleDraftPreviewView(MenuContextMixin, BreadcrumbMixin, DetailView):
model = Article model = Article
template_name = 'blog/article_detail.html' # используем тот же шаблон template_name = 'blog/article_detail.html' # используем тот же шаблон
context_object_name = 'article' context_object_name = 'article'

View File

@ -122,7 +122,7 @@ class SolutionListView(BaseListView, BreadcrumbMixin):
model = Solution model = Solution
template_name = 'programmer/solution.html' template_name = 'programmer/solution.html'
context_object_name = 'posts' context_object_name = 'posts'
paginate_by = 1 # Количество проектов на странице paginate_by = 5 # Количество проектов на странице
cards_template = 'programmer/includes/project_cards.html' cards_template = 'programmer/includes/project_cards.html'
ordering = ['-time_create'] ordering = ['-time_create']