fix: seo и пустое состояние тегов
This commit is contained in:
64
cadpoint/web/sitemaps.py
Normal file
64
cadpoint/web/sitemaps.py
Normal 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
|
||||
|
||||
@@ -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>Привет мир</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')
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user