del: модели и админка (18) именование файлов
This commit is contained in:
@@ -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-файл'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -5,6 +5,17 @@ from hashids import Hashids
|
|||||||
from hypn0.settings import *
|
from hypn0.settings import *
|
||||||
from django.utils import timezone
|
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):
|
class TbHypn0Item(models.Model):
|
||||||
"""
|
"""
|
||||||
Единая модель для генераций, публичных шеров и витрины галереи.
|
Единая модель для генераций, публичных шеров и витрины галереи.
|
||||||
@@ -57,7 +68,7 @@ class TbHypn0Item(models.Model):
|
|||||||
help_text="Заголовок для генерации. Включена HTML-типографика. 255 символов максимум.",
|
help_text="Заголовок для генерации. Включена HTML-типографика. 255 символов максимум.",
|
||||||
)
|
)
|
||||||
file_svg = models.FileField(
|
file_svg = models.FileField(
|
||||||
upload_to="svg/%Y/%m/",
|
upload_to=hypn0_svg_upload_to,
|
||||||
verbose_name="SVG-файл",
|
verbose_name="SVG-файл",
|
||||||
help_text="Результирующий SVG-файл генерации",
|
help_text="Результирующий SVG-файл генерации",
|
||||||
)
|
)
|
||||||
@@ -313,11 +324,35 @@ class TbHypn0Item(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# 3. Формируем s_hash_id, если его еще нет
|
# 3. Формируем s_hash_id, если его еще нет
|
||||||
|
update_fields = []
|
||||||
if not self.s_hash_id:
|
if not self.s_hash_id:
|
||||||
self.s_hash_id = Hashids(salt=HASHIDS_SALT, min_length=HASHIDS_MIN_LENGTH).encode(self.pk)
|
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:
|
if is_new and author_fp:
|
||||||
TbVote.objects.create(
|
TbVote.objects.create(
|
||||||
k_item=self,
|
k_item=self,
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ class HalftoneServiceTests(BaseMediaTestCase):
|
|||||||
self.img.putpixel((x, y), (0, 0, 0))
|
self.img.putpixel((x, y), (0, 0, 0))
|
||||||
|
|
||||||
def test_encode_to_base36(self):
|
def test_encode_to_base36(self):
|
||||||
self.assertEqual(encode_to_base36(0), "a")
|
self.assertEqual(encode_to_base36(0), "H")
|
||||||
self.assertEqual(encode_to_base36(1), "b")
|
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):
|
with self.assertRaises(ValueError):
|
||||||
encode_to_base36(-1)
|
encode_to_base36(-1)
|
||||||
|
|
||||||
@@ -112,14 +115,14 @@ class HalftoneServiceTests(BaseMediaTestCase):
|
|||||||
# Проверяем, что для вертикального изображения (100x300) сетка не раздувается
|
# Проверяем, что для вертикального изображения (100x300) сетка не раздувается
|
||||||
tall_img = Image.new("RGB", (100, 300), color="black")
|
tall_img = Image.new("RGB", (100, 300), color="black")
|
||||||
svg_tall = generate_halftone_svg(tall_img, cols=30, max_radius=5)
|
svg_tall = generate_halftone_svg(tall_img, cols=30, max_radius=5)
|
||||||
# При cols=30 наибольшая сторона (высота) должна иметь 30 точек, а ширина 10
|
# При cols=30 наибольшая сторона (высота) имеет 30 точек, ширина 10
|
||||||
# step = 5 * 2 + 2 = 12 -> width = 10 * 12 = 120, height = 30 * 12 = 360
|
# STEP = 20, margin = 4 -> width = 10 * 20 + 8 = 208, height = 30 * 20 + 8 = 608
|
||||||
self.assertIn('viewBox="0 0 120 360"', svg_tall)
|
self.assertIn('viewBox="0 0 208 608"', svg_tall)
|
||||||
|
|
||||||
# Для горизонтального изображения (300x100)
|
# Для горизонтального изображения (300x100)
|
||||||
wide_img = Image.new("RGB", (300, 100), color="black")
|
wide_img = Image.new("RGB", (300, 100), color="black")
|
||||||
svg_wide = generate_halftone_svg(wide_img, cols=30, max_radius=5)
|
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):
|
class HalftoneFormTests(BaseMediaTestCase):
|
||||||
@@ -329,6 +332,9 @@ class PublishViewTests(BaseMediaTestCase):
|
|||||||
self.assertEqual(item.j_metadata["shape"], "diamond")
|
self.assertEqual(item.j_metadata["shape"], "diamond")
|
||||||
self.assertEqual(item.j_metadata["cols"], 40)
|
self.assertEqual(item.j_metadata["cols"], 40)
|
||||||
self.assertEqual(item.j_metadata["color"], "#10b981")
|
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
|
# Проверяем авторский голос в TbVote
|
||||||
self.assertEqual(TbVote.objects.count(), 1)
|
self.assertEqual(TbVote.objects.count(), 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user