86 lines
4.0 KiB
Python
86 lines
4.0 KiB
Python
"""Mass-замена старых Joomla-кросс-ссылок в HTML-контенте.
|
|
|
|
Пока команда чинит только внутренние ссылки на статьи. Ссылки на картинки и
|
|
прочие медиа остаются без изменений.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
|
|
from web.legacy_links import iter_legacy_link_matches, replace_legacy_links
|
|
from web.models import TbContent
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
'Находит и заменяет старые Joomla-кросс-ссылки в HTML-контенте на '
|
|
'текущие Django-URL. Ссылки на медиа пока не трогает.'
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
'--apply',
|
|
action='store_true',
|
|
help='Сохранить изменения в базе. Без флага команда работает в режиме dry-run.',
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
apply_changes = options['apply']
|
|
# Сначала один раз собираем карту `id -> slug`, чтобы не делать лишние
|
|
# запросы к базе в цикле по каждому контенту.
|
|
content_by_id = dict(
|
|
TbContent.objects.values_list('id', 'szContentSlug')
|
|
)
|
|
# Обрабатываем только HTML-поля, где реально встречаются старые ссылки.
|
|
fields = ('szContentIntro', 'szContentBody', 'szContentHead')
|
|
pattern_counter = Counter()
|
|
updated_objects = 0
|
|
updated_fields = 0
|
|
|
|
for content in TbContent.objects.all().iterator():
|
|
field_updates: dict[str, str] = {}
|
|
object_matches = []
|
|
|
|
for field_name in fields:
|
|
text = getattr(content, field_name) or ''
|
|
if not text:
|
|
continue
|
|
# Быстрая проверка: если в тексте нет legacy-ссылок, не тратим
|
|
# время на полноценную замену.
|
|
if not any(match for match in iter_legacy_link_matches(text)):
|
|
continue
|
|
|
|
new_text, matches = replace_legacy_links(text, content_by_id)
|
|
if matches and new_text != text:
|
|
field_updates[field_name] = new_text
|
|
object_matches.extend(matches)
|
|
pattern_counter.update(match.pattern_name for match in matches)
|
|
|
|
if not field_updates:
|
|
continue
|
|
|
|
updated_objects += 1
|
|
updated_fields += len(field_updates)
|
|
self.stdout.write(
|
|
f'#{content.pk}: {len(object_matches)} замен(ы) в полях {", ".join(field_updates)}'
|
|
)
|
|
for match in object_matches[:5]:
|
|
self.stdout.write(f' - {match.pattern_name}: {match.old_url} -> {match.new_url}')
|
|
if len(object_matches) > 5:
|
|
self.stdout.write(f' ... ещё {len(object_matches) - 5} замен(ы)')
|
|
|
|
if apply_changes:
|
|
# Записываем только те поля, которые действительно изменились.
|
|
with transaction.atomic():
|
|
TbContent.objects.filter(pk=content.pk).update(**field_updates)
|
|
|
|
self.stdout.write(self.style.SUCCESS(f'Затронуто объектов: {updated_objects}'))
|
|
self.stdout.write(self.style.SUCCESS(f'Затронуто полей: {updated_fields}'))
|
|
self.stdout.write(self.style.SUCCESS(f'Сводка по шаблонам: {dict(pattern_counter)}'))
|
|
if not apply_changes:
|
|
self.stdout.write(self.style.WARNING('Это dry-run. Для записи в БД добавь флаг --apply.'))
|
|
|