Оптимизация блога с помощью клауд
This commit is contained in:
parent
d048510f68
commit
c95f27696b
@ -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}..."
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
"""Создание комментария к статье (без модерации)."""
|
"""Создание комментария к статье (без модерации)."""
|
||||||
|
|||||||
@ -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()">×</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()">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<img class="modal-image" id="modalImage" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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']
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user