fix: seo и пустое состояние тегов

This commit is contained in:
2026-04-12 15:57:37 +03:00
parent 6d8ccb5ceb
commit 3f72d2e963
12 changed files with 387 additions and 126 deletions

64
cadpoint/web/sitemaps.py Normal file
View File

@@ -0,0 +1,64 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from django.utils import timezone
from django.db.models import Q
from taggit.models import Tag
from web.models import TbContent
class CadpointSitemap(Sitemap):
"""Одна карта сайта для публичных страниц CADpoint."""
changefreq = 'weekly'
def items(self):
now_value = timezone.now()
content_items = list(
TbContent.objects.filter(
bContentPublish=True,
tdContentPublishUp__lte=now_value,
).filter(
Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=now_value)
).order_by('-tdContentPublishUp', 'id')
)
latest_content = content_items[0].dtContentTimeStamp if content_items else None
sitemap_items = [
{'kind': 'home', 'lastmod': latest_content},
{'kind': 'alltags', 'lastmod': latest_content},
]
tags = Tag.objects.filter(taggit_taggeditem_items__isnull=False).distinct().order_by('name')
sitemap_items.extend({'kind': 'tag', 'tag': tag} for tag in tags)
sitemap_items.extend({'kind': 'item', 'item': item} for item in content_items)
return sitemap_items
def location(self, item):
kind = item['kind']
if kind == 'home':
return '/'
if kind == 'alltags':
return reverse('web_alltags')
if kind == 'tag':
return f"/tag_{item['tag'].slug}"
return f"/item/{item['item'].id}-{item['item'].szContentSlug}"
def lastmod(self, item):
kind = item['kind']
if kind in {'home', 'alltags'}:
return item.get('lastmod')
if kind == 'item':
return item['item'].dtContentTimeStamp
return None
def priority(self, item):
kind = item['kind']
if kind == 'home':
return 1.0
if kind == 'alltags':
return 0.6
if kind == 'tag':
return 0.5
return 0.8

View File

@@ -82,6 +82,13 @@ class AdminTypographFormTests(SimpleTestCase):
'html',
)
for field_name in ('szContentKeywords', 'szContentDescription'):
self.assertIsInstance(form.fields[field_name].widget, Textarea)
self.assertNotEqual(
form.fields[field_name].widget.attrs.get('data-codemirror-editor'),
'1',
)
self.assertIn('codemirror/editor.js', str(form.media))
def test_tbcontent_model_has_no_btypograf_field(self):
@@ -181,6 +188,41 @@ class AllTagsPageTests(TestCase):
self.assertContains(response, '<b class="_tag">1</b>')
class TagEmptyStateTests(TestCase):
def setUp(self):
self.item = TbContent.objects.create(
szContentHead='Тест 1',
szContentIntro='Анонс 1',
szContentBody='Тело 1',
szContentSlug='test-1',
bContentPublish=True,
)
self.item.tags.add('alpha')
Tag.objects.create(name='lonely')
def test_tag_page_with_news_still_renders_entries(self):
response = self.client.get('/tag_alpha')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Тест 1')
self.assertNotContains(response, 'Новостей не найдено')
def test_tag_page_without_news_shows_empty_state(self):
response = self.client.get('/tag_lonely')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Новостей не найдено')
self.assertContains(response, 'По этому тегу пока нет опубликованных новостей.')
self.assertNotContains(response, 'Тест 1')
def test_tag_page_for_missing_slug_shows_missing_tag_message(self):
response = self.client.get('/tag_rebranded-tag')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Тег не найден')
self.assertContains(response, 'не найден или был переименован')
class TypographTests(TestCase):
def test_save_generates_slug_from_clean_text(self):
item = TbContent(szContentHead='<b>Привет&nbsp;мир</b>')
@@ -277,16 +319,107 @@ class TypographTests(TestCase):
szContentHead='Проверка просмотра',
szContentIntro='Короткий анонс',
szContentBody='Полный текст',
szContentSlug='proverka-prosmotra',
szContentSlug='real-slug',
bContentPublish=True,
)
timestamp_before = item.dtContentTimeStamp
response = self.client.get(f'/item/{item.id}-{item.szContentSlug}')
response = self.client.get(f'/item/{item.id}-wrong-slug')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<title>Проверка просмотра | CADpoint</title>')
self.assertContains(
response,
f'<link rel="canonical" href="http://testserver/item/{item.id}-{item.szContentSlug}" />',
)
self.assertNotContains(response, 'wrong-slug')
self.assertContains(response, '<script type="application/ld+json">')
self.assertContains(response, '"@type": "WebSite"')
self.assertContains(response, '"@type": "Article"')
self.assertRegex(
response.content.decode(),
r'"datePublished": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}[+-]\d{2}:\d{2}"',
)
self.assertRegex(
response.content.decode(),
r'"dateModified": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}[+-]\d{2}:\d{2}"',
)
item.refresh_from_db()
self.assertEqual(item.iContentHits, 1)
self.assertEqual(item.dtContentTimeStamp, timestamp_before)
def test_show_item_keywords_prioritize_explicit_seo_values(self):
item = TbContent.objects.create(
szContentHead='SEO ключи',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='seo-klyuchi',
szContentKeywords='ключ1, ключ2',
bContentPublish=True,
)
item.tags.add('alpha', 'beta')
response = self.client.get(f'/item/{item.id}-{item.szContentSlug}')
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<meta name="keywords" content="cadpoint, ключ1, ключ2, alpha, beta, новости" />',
)
self.assertNotContains(response, 'None')
def test_show_item_keywords_without_explicit_value_do_not_render_none(self):
item = TbContent.objects.create(
szContentHead='SEO пусто',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='seo-pusto',
szContentKeywords=None,
bContentPublish=True,
)
item.tags.add('alpha')
response = self.client.get(f'/item/{item.id}-{item.szContentSlug}')
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<meta name="keywords" content="cadpoint, alpha, новости" />',
)
self.assertNotContains(response, 'None')
class SitemapTests(TestCase):
def setUp(self):
self.published = TbContent.objects.create(
szContentHead='Опубликованная статья',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='opublikovannaya-statya',
bContentPublish=True,
)
self.published.tags.add('alpha')
TbContent.objects.create(
szContentHead='Скрытая статья',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='skrytaya-statya',
bContentPublish=False,
)
def test_sitemap_uses_django_framework_and_lists_public_pages(self):
response = self.client.get(reverse('web_sitemap'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<?xml version="1.0" encoding="UTF-8"?>', html=False)
self.assertContains(response, '<loc>http://testserver/</loc>', html=False)
self.assertContains(response, '<loc>http://testserver/alltags</loc>', html=False)
self.assertContains(
response,
f'<loc>http://testserver/item/{self.published.id}-{self.published.szContentSlug}</loc>',
html=False,
)
self.assertContains(response, '<loc>http://testserver/tag_alpha</loc>', html=False)
self.assertNotContains(response, 'skrytaya-statya')

View File

@@ -90,6 +90,8 @@ def index(request,
"""
template = "index.jinja2" # шаблон
to_template: dict[str, object] = {"COOKIES": check_cookies(request)}
empty_state_title = ""
empty_state_message = ""
page_number = max(int(ppage), 0)
now_value = timezone.now()
@@ -109,6 +111,7 @@ def index(request,
# Список тегов должен быть отсортированным для канонического URL.
return HttpResponseRedirect("tag_%s" % "_".join(sorted(selected_tags)))
content_qs = content_qs.filter(tags__slug__in=selected_tags).distinct()
to_template["SELECTED_TAGS"] = Tag.objects.filter(slug__in=selected_tags).order_by("slug")
to_template["TAGS_S"] = "/tag_" + slug_tags
to_template["TAGS_L"] = selected_tags
@@ -116,6 +119,26 @@ def index(request,
total_items = q_content.count()
total_page = max(math.ceil(total_items / settings.NUM_ITEMS_IN_PAGE) - 1, 0) if total_items else 0
if selected_tags:
existing_tags = set(Tag.objects.filter(slug__in=selected_tags).values_list("slug", flat=True))
missing_tags = [tag_slug for tag_slug in selected_tags if tag_slug not in existing_tags]
if missing_tags:
if len(missing_tags) == 1:
empty_state_title = "Тег не найден"
empty_state_message = f"Тег «{missing_tags[0]}» не найден или был переименован."
else:
empty_state_title = "Теги не найдены"
empty_state_message = f"Теги «{', '.join(missing_tags)}» не найдены или были переименованы."
elif not total_items:
if len(selected_tags) == 1:
empty_state_message = "По этому тегу пока нет опубликованных новостей."
else:
empty_state_message = "По выбранным тегам пока нет опубликованных новостей."
elif page_number > total_page:
empty_state_message = "На этой странице больше нет новостей. Откройте первую страницу ленты."
elif not total_items:
empty_state_message = "Пока здесь нет новостей."
q_content = q_content[page_number * settings.NUM_ITEMS_IN_PAGE:
page_number * settings.NUM_ITEMS_IN_PAGE+ settings.NUM_ITEMS_IN_PAGE]
@@ -138,6 +161,8 @@ def index(request,
to_template["TAGS_IN_PAGE"] = q_tags
to_template["PAGE_OF_LIST"] = page_number
to_template["TOTAL_PAGE"] = total_page
to_template["EMPTY_STATE_TITLE"] = empty_state_title
to_template["EMPTY_STATE_MESSAGE"] = empty_state_message
return render(request, template, to_template)
@@ -223,16 +248,3 @@ def show_item(request,
except (ValueError, AttributeError, TbContent.DoesNotExist, TbContent.MultipleObjectsReturned):
raise Http404("Контента с таким id не существует")
def sitemap(request):
template = "sitemap.jinja2" # шаблон
q_items = TbContent.objects.filter(
bContentPublish=True,
tdContentPublishUp__lte=timezone.now(),
).filter(
Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now())
).order_by("-tdContentPublishUp", "id").all()
to_template: dict[str, object] = {"ITEMS": q_items}
print(q_items)
response = render(request, template, to_template)
return response