From 153ab191a5f898f5c67ccc09aa86fe6f5962bee1 Mon Sep 17 00:00:00 2001 From: erjemin Date: Thu, 3 Sep 2026 01:13:11 +0300 Subject: [PATCH] =?UTF-8?q?del:=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0=20(18)=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0004_alter_tbhypn0item_file_svg.py | 19 +++++++++ hypn0/hypn0_site/models.py | 41 +++++++++++++++++-- hypn0/hypn0_site/tests.py | 18 +++++--- 3 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 hypn0/hypn0_site/migrations/0004_alter_tbhypn0item_file_svg.py diff --git a/hypn0/hypn0_site/migrations/0004_alter_tbhypn0item_file_svg.py b/hypn0/hypn0_site/migrations/0004_alter_tbhypn0item_file_svg.py new file mode 100644 index 0000000..ddcf047 --- /dev/null +++ b/hypn0/hypn0_site/migrations/0004_alter_tbhypn0item_file_svg.py @@ -0,0 +1,19 @@ +# Generated by Django 6.1 on 2026-09-02 22:00 + +import hypn0_site.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hypn0_site', '0003_remove_tbhypn0item_i_file_size_alter_tbblogpost_slug_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='tbhypn0item', + name='file_svg', + field=models.FileField(help_text='Результирующий SVG-файл генерации', upload_to=hypn0_site.models.hypn0_svg_upload_to, verbose_name='SVG-файл'), + ), + ] diff --git a/hypn0/hypn0_site/models.py b/hypn0/hypn0_site/models.py index 4de68b4..b6a7d68 100644 --- a/hypn0/hypn0_site/models.py +++ b/hypn0/hypn0_site/models.py @@ -5,6 +5,17 @@ from hashids import Hashids from hypn0.settings import * from django.utils import timezone + +def hypn0_svg_upload_to(instance, filename: str) -> str: + """ + Генерирует путь и имя для сохраняемого SVG-файла картины: + svg/YYYY/MM/hypn0_{s_hash_id}.svg + """ + now = timezone.now() + hash_id = getattr(instance, "s_hash_id", "") or "temp" + return f"svg/{now.strftime('%Y/%m')}/hypn0_{hash_id}.svg" + + class TbHypn0Item(models.Model): """ Единая модель для генераций, публичных шеров и витрины галереи. @@ -57,7 +68,7 @@ class TbHypn0Item(models.Model): help_text="Заголовок для генерации. Включена HTML-типографика. 255 символов максимум.", ) file_svg = models.FileField( - upload_to="svg/%Y/%m/", + upload_to=hypn0_svg_upload_to, verbose_name="SVG-файл", help_text="Результирующий SVG-файл генерации", ) @@ -313,11 +324,35 @@ class TbHypn0Item(models.Model): super().save(*args, **kwargs) # 3. Формируем s_hash_id, если его еще нет + update_fields = [] if not self.s_hash_id: self.s_hash_id = Hashids(salt=HASHIDS_SALT, min_length=HASHIDS_MIN_LENGTH).encode(self.pk) - super().save(update_fields=['s_hash_id']) + update_fields.append("s_hash_id") - # 4. Регистрируем автора в TbVote (гарантированно после получения self.pk) + # 4. Приводим имя SVG-файла к стандартному виду с s_hash_id (svg/YYYY/MM/hypn0_{s_hash_id}.svg) + if self.file_svg and self.s_hash_id: + expected_filename = f"hypn0_{self.s_hash_id}.svg" + current_name = self.file_svg.name + if expected_filename not in current_name: + # Вычисляем целевой путь в хранилище + date_folder = self.d_created_at.strftime("%Y/%m") if self.d_created_at else timezone.now().strftime("%Y/%m") + target_name = f"svg/{date_folder}/{expected_filename}" + storage = self.file_svg.storage + if current_name and storage.exists(current_name) and current_name != target_name: + # Если целевой файл уже существовал, удаляем перед заменой + if storage.exists(target_name): + storage.delete(target_name) + # Перемещаем файл в хранилище + with storage.open(current_name, "rb") as source_file: + storage.save(target_name, source_file) + storage.delete(current_name) + self.file_svg.name = target_name + update_fields.append("file_svg") + + if update_fields: + super().save(update_fields=update_fields) + + # 5. Регистрируем автора в TbVote (гарантированно после получения self.pk) if is_new and author_fp: TbVote.objects.create( k_item=self, diff --git a/hypn0/hypn0_site/tests.py b/hypn0/hypn0_site/tests.py index 56c60c0..221c9fb 100644 --- a/hypn0/hypn0_site/tests.py +++ b/hypn0/hypn0_site/tests.py @@ -60,8 +60,11 @@ class HalftoneServiceTests(BaseMediaTestCase): self.img.putpixel((x, y), (0, 0, 0)) def test_encode_to_base36(self): - self.assertEqual(encode_to_base36(0), "a") - self.assertEqual(encode_to_base36(1), "b") + self.assertEqual(encode_to_base36(0), "H") + self.assertEqual(encode_to_base36(1), "Y") + self.assertEqual(encode_to_base36(2), "P") + self.assertEqual(encode_to_base36(3), "N") + self.assertEqual(encode_to_base36(4), "0") with self.assertRaises(ValueError): encode_to_base36(-1) @@ -112,14 +115,14 @@ class HalftoneServiceTests(BaseMediaTestCase): # Проверяем, что для вертикального изображения (100x300) сетка не раздувается tall_img = Image.new("RGB", (100, 300), color="black") svg_tall = generate_halftone_svg(tall_img, cols=30, max_radius=5) - # При cols=30 наибольшая сторона (высота) должна иметь 30 точек, а ширина 10 - # step = 5 * 2 + 2 = 12 -> width = 10 * 12 = 120, height = 30 * 12 = 360 - self.assertIn('viewBox="0 0 120 360"', svg_tall) + # При cols=30 наибольшая сторона (высота) имеет 30 точек, ширина 10 + # STEP = 20, margin = 4 -> width = 10 * 20 + 8 = 208, height = 30 * 20 + 8 = 608 + self.assertIn('viewBox="0 0 208 608"', svg_tall) # Для горизонтального изображения (300x100) wide_img = Image.new("RGB", (300, 100), color="black") svg_wide = generate_halftone_svg(wide_img, cols=30, max_radius=5) - self.assertIn('viewBox="0 0 360 120"', svg_wide) + self.assertIn('viewBox="0 0 608 208"', svg_wide) class HalftoneFormTests(BaseMediaTestCase): @@ -329,6 +332,9 @@ class PublishViewTests(BaseMediaTestCase): self.assertEqual(item.j_metadata["shape"], "diamond") self.assertEqual(item.j_metadata["cols"], 40) self.assertEqual(item.j_metadata["color"], "#10b981") + # Проверяем, что файл сохранен с именем, содержащим id-хэш + self.assertIn(f"hypn0_{item.s_hash_id}.svg", item.file_svg.name) + self.assertTrue(item.file_svg.storage.exists(item.file_svg.name)) # Проверяем авторский голос в TbVote self.assertEqual(TbVote.objects.count(), 1)