add: автоматическое создание slug с помощью pytils и чистка от html-мнемоник.
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -5,6 +5,16 @@
|
|||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
||||||
и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
|
и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [Unreleased] — 2025–02–13
|
||||||
|
|
||||||
|
### Добавлено
|
||||||
|
- Поле `updated_at` (_Дата обновления_) в модели, админке, блогах, страницах и `sitemaps.xml` для улучшения SEO, GEO и LLMO.
|
||||||
|
- README.md с описанием проекта онлайн-типографа, его особенностей, технического стека и инструкциями по установке и запуску.
|
||||||
|
|
||||||
|
### Исправлено
|
||||||
|
- Исправлены ошибки в шаблоне 'post_list.html' (и дизайн в целом).
|
||||||
|
- Формирование `slag` из `title` при сохранении поста или страницы с использованием библиотеки `pytils` для транслитерации с очистикой от HTML-мнемоник и создания URL-дружественных строк.
|
||||||
|
|
||||||
## [0.2.4] - 2025-02-12
|
## [0.2.4] - 2025-02-12
|
||||||
|
|
||||||
### Добавлено
|
### Добавлено
|
||||||
|
|||||||
18
etpgrf_site/blog/migrations/0005_alter_post_slug.py
Normal file
18
etpgrf_site/blog/migrations/0005_alter_post_slug.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-02-12 16:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('blog', '0004_post_updated_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='post',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(blank=True, help_text='Уникальная часть адреса. Оставьте пустым для автогенерации.', max_length=255, unique=True, verbose_name='URL (slug)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.text import slugify
|
||||||
|
import html
|
||||||
|
# Попробуем импортировать pytils, если он есть
|
||||||
|
try:
|
||||||
|
from pytils.translit import slugify as pytils_slugify
|
||||||
|
except ImportError:
|
||||||
|
pytils_slugify = None
|
||||||
|
|
||||||
class PostType(models.TextChoices):
|
class PostType(models.TextChoices):
|
||||||
BLOG = 'B', 'Пост в блог'
|
BLOG = 'B', 'Пост в блог'
|
||||||
@@ -19,7 +26,8 @@ class Post(models.Model):
|
|||||||
verbose_name="URL (slug)",
|
verbose_name="URL (slug)",
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text="Уникальная часть адреса. Используйте латиницу, цифры и дефис. Например: my-new-post"
|
blank=True, # Разрешаем оставлять пустым в админке (заполнится в save)
|
||||||
|
help_text="Уникальная часть адреса. Оставьте пустым для автогенерации."
|
||||||
)
|
)
|
||||||
|
|
||||||
post_type = models.CharField(
|
post_type = models.CharField(
|
||||||
@@ -44,17 +52,21 @@ class Post(models.Model):
|
|||||||
help_text="Дата, которая будет отображаться в блоге. Можно запланировать на будущее."
|
help_text="Дата, которая будет отображаться в блоге. Можно запланировать на будущее."
|
||||||
)
|
)
|
||||||
updated_at = models.DateTimeField(
|
updated_at = models.DateTimeField(
|
||||||
"Дата обновления",
|
verbose_name="Дата обновления",
|
||||||
auto_now=True,
|
auto_now=True,
|
||||||
help_text="Автоматически обновляется при каждом сохранении."
|
help_text="Автоматически обновляется при каждом сохранении."
|
||||||
)
|
)
|
||||||
|
|
||||||
content = models.TextField(
|
content = models.TextField(
|
||||||
verbose_name="Контент",
|
verbose_name="Контент",
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
help_text="Основной текст публикации. Поддерживает HTML."
|
help_text="Основной текст публикации. Поддерживает HTML."
|
||||||
)
|
)
|
||||||
excerpt = models.TextField(
|
excerpt = models.TextField(
|
||||||
verbose_name="Краткое описание (тизер)",
|
verbose_name="Краткое описание (тизер)",
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
help_text="Отображается в списке постов. Если оставить пустым, будет взято начало контента."
|
help_text="Отображается в списке постов. Если оставить пустым, будет взято начало контента."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,5 +114,27 @@ class Post(models.Model):
|
|||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
if self.post_type == PostType.PAGE:
|
if self.post_type == PostType.PAGE:
|
||||||
|
# Страницы живут в корневом urls.py без namespace
|
||||||
return reverse('page_detail', kwargs={'slug': self.slug})
|
return reverse('page_detail', kwargs={'slug': self.slug})
|
||||||
return reverse('post_detail', kwargs={'slug': self.slug})
|
# Посты живут в приложении blog с namespace 'blog'
|
||||||
|
return reverse('blog:post_detail', kwargs={'slug': self.slug})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Если слаг не заполнен, генерируем его из заголовка
|
||||||
|
if not self.slug:
|
||||||
|
# 1. Декодируем HTML-сущности ( -> " ")
|
||||||
|
clean_title = html.unescape(self.title)
|
||||||
|
# 2. Генерируем базовый слаг
|
||||||
|
if pytils_slugify:
|
||||||
|
base_slug = pytils_slugify(clean_title)
|
||||||
|
else:
|
||||||
|
base_slug = slugify(clean_title)
|
||||||
|
|
||||||
|
# 3. Уникализация
|
||||||
|
self.slug = base_slug
|
||||||
|
counter = 1
|
||||||
|
while Post.objects.filter(slug=self.slug).exclude(pk=self.pk).exists():
|
||||||
|
self.slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
13
poetry.lock
generated
13
poetry.lock
generated
@@ -381,6 +381,17 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytils"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "Russian-specific string utils"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pytils-0.4.4-py3-none-any.whl", hash = "sha256:e54c16465a5fdb65d414e2da8045e6cc6de79889acda6143dcef2e1e86a1a840"},
|
||||||
|
{file = "pytils-0.4.4.tar.gz", hash = "sha256:9992a96caad57daa211584df1da4fd825f11e836d3ed93011785f1d02ab6f0ca"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "2026.1.15"
|
version = "2026.1.15"
|
||||||
@@ -572,4 +583,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.13"
|
python-versions = "^3.13"
|
||||||
content-hash = "9610a92fa47d1bd0849512ae842b0fdd68dc06d9917ab676cf5d8f6521700837"
|
content-hash = "ce33b38ff06b069d35d46c795c2a5f81c0907f288bb662a001ab740760cc90b2"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ etpgrf = "0.1.4"
|
|||||||
# regex = "^2023.12" # etpgrf подтянет как зависимость
|
# regex = "^2023.12" # etpgrf подтянет как зависимость
|
||||||
# beautifulsoup4 = "^4.10.0" # etpgrf подтянет как зависимость
|
# beautifulsoup4 = "^4.10.0" # etpgrf подтянет как зависимость
|
||||||
pillow = "^12.1.0"
|
pillow = "^12.1.0"
|
||||||
|
pytils = "^0.4.4"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
Reference in New Issue
Block a user