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/),
|
||||
и этот проект придерживается [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
|
||||
|
||||
### Добавлено
|
||||
|
||||
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.utils import timezone
|
||||
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):
|
||||
BLOG = 'B', 'Пост в блог'
|
||||
@@ -19,7 +26,8 @@ class Post(models.Model):
|
||||
verbose_name="URL (slug)",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text="Уникальная часть адреса. Используйте латиницу, цифры и дефис. Например: my-new-post"
|
||||
blank=True, # Разрешаем оставлять пустым в админке (заполнится в save)
|
||||
help_text="Уникальная часть адреса. Оставьте пустым для автогенерации."
|
||||
)
|
||||
|
||||
post_type = models.CharField(
|
||||
@@ -44,17 +52,21 @@ class Post(models.Model):
|
||||
help_text="Дата, которая будет отображаться в блоге. Можно запланировать на будущее."
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
"Дата обновления",
|
||||
verbose_name="Дата обновления",
|
||||
auto_now=True,
|
||||
help_text="Автоматически обновляется при каждом сохранении."
|
||||
)
|
||||
|
||||
content = models.TextField(
|
||||
verbose_name="Контент",
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text="Основной текст публикации. Поддерживает HTML."
|
||||
)
|
||||
excerpt = models.TextField(
|
||||
verbose_name="Краткое описание (тизер)",
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text="Отображается в списке постов. Если оставить пустым, будет взято начало контента."
|
||||
)
|
||||
|
||||
@@ -102,5 +114,27 @@ class Post(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.post_type == PostType.PAGE:
|
||||
# Страницы живут в корневом urls.py без namespace
|
||||
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]
|
||||
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]]
|
||||
name = "regex"
|
||||
version = "2026.1.15"
|
||||
@@ -572,4 +583,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.13"
|
||||
content-hash = "9610a92fa47d1bd0849512ae842b0fdd68dc06d9917ab676cf5d8f6521700837"
|
||||
content-hash = "ce33b38ff06b069d35d46c795c2a5f81c0907f288bb662a001ab740760cc90b2"
|
||||
|
||||
@@ -16,6 +16,7 @@ etpgrf = "0.1.4"
|
||||
# regex = "^2023.12" # etpgrf подтянет как зависимость
|
||||
# beautifulsoup4 = "^4.10.0" # etpgrf подтянет как зависимость
|
||||
pillow = "^12.1.0"
|
||||
pytils = "^0.4.4"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
Reference in New Issue
Block a user