Skip to content

Commit 155a472

Browse files
committed
Create new spritesheet fields
1 parent 28d2ae5 commit 155a472

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Generated by Django 6.0.6 on 2026-07-01 11:30
2+
3+
import pgtrigger.compiler
4+
import pgtrigger.migrations
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('core', '0029_marker_insert_insert_marker_update_update'),
12+
]
13+
14+
operations = [
15+
pgtrigger.migrations.RemoveTrigger(
16+
model_name='object',
17+
name='insert_insert',
18+
),
19+
pgtrigger.migrations.RemoveTrigger(
20+
model_name='object',
21+
name='update_update',
22+
),
23+
migrations.AddField(
24+
model_name='object',
25+
name='spritesheet_file',
26+
field=models.FileField(blank=True, null=True, upload_to='objects/spritesheets/'),
27+
),
28+
migrations.AddField(
29+
model_name='object',
30+
name='spritesheet_metadata',
31+
field=models.FileField(blank=True, null=True, upload_to='objects/spritesheets/'),
32+
),
33+
migrations.AddField(
34+
model_name='objectevent',
35+
name='spritesheet_file',
36+
field=models.FileField(blank=True, null=True, upload_to='objects/spritesheets/'),
37+
),
38+
migrations.AddField(
39+
model_name='objectevent',
40+
name='spritesheet_metadata',
41+
field=models.FileField(blank=True, null=True, upload_to='objects/spritesheets/'),
42+
),
43+
pgtrigger.migrations.AddTrigger(
44+
model_name='object',
45+
trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "core_objectevent" ("audio_description", "author", "created", "file_extension", "file_name_original", "file_size", "id", "modified", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "sound_id", "source", "spritesheet_file", "spritesheet_metadata", "thumbnail", "title") VALUES (NEW."audio_description", NEW."author", NEW."created", NEW."file_extension", NEW."file_name_original", NEW."file_size", NEW."id", NEW."modified", NEW."owner_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."sound_id", NEW."source", NEW."spritesheet_file", NEW."spritesheet_metadata", NEW."thumbnail", NEW."title"); RETURN NULL;', hash='9961658bf4959d63d6a95cc2197b1650cacc1a3d', operation='INSERT', pgid='pgtrigger_insert_insert_25a7e', table='core_object', when='AFTER')),
46+
),
47+
pgtrigger.migrations.AddTrigger(
48+
model_name='object',
49+
trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "core_objectevent" ("audio_description", "author", "created", "file_extension", "file_name_original", "file_size", "id", "modified", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "sound_id", "source", "spritesheet_file", "spritesheet_metadata", "thumbnail", "title") VALUES (NEW."audio_description", NEW."author", NEW."created", NEW."file_extension", NEW."file_name_original", NEW."file_size", NEW."id", NEW."modified", NEW."owner_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."sound_id", NEW."source", NEW."spritesheet_file", NEW."spritesheet_metadata", NEW."thumbnail", NEW."title"); RETURN NULL;', hash='b5beaa6a354a1fe2c1a4a7de9f16c4d4b620801b', operation='UPDATE', pgid='pgtrigger_update_update_27852', table='core_object', when='AFTER')),
50+
),
51+
]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Data migration: generate spritesheets and metadata JSON files for all
3+
existing GIF Objects that don't have them yet.
4+
"""
5+
6+
import json
7+
import logging
8+
9+
from django.core.files.base import ContentFile
10+
from django.db import migrations
11+
from src.core.spritesheet_converter import gif_to_spritesheet
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def generate_spritesheets_for_existing_gifs(apps, schema_editor):
17+
# Import inline so that the migration doesn't break if the module is
18+
# moved or renamed later — the function body is self-contained.
19+
20+
21+
Object = apps.get_model("core", "Object")
22+
23+
for obj in Object.objects.filter(file_extension="gif").iterator():
24+
if not obj.source:
25+
logger.warning("Object %s: no source file, skipping.", obj.pk)
26+
continue
27+
28+
try:
29+
storage = obj.source.storage
30+
31+
with obj.source.open("rb") as f:
32+
png_bytes, metadata = gif_to_spritesheet(f)
33+
34+
base_name = obj.source.name.rsplit(".", 1)[0].split("/")[-1]
35+
36+
# Save spritesheet PNG
37+
spritesheet_path = f"objects/spritesheets/{base_name}_spritesheet.png"
38+
_save_to_storage(storage, spritesheet_path, png_bytes)
39+
obj.spritesheet_file.name = spritesheet_path
40+
41+
# Save metadata JSON
42+
metadata_path = f"objects/spritesheets/{base_name}_spritesheet.json"
43+
_save_to_storage(
44+
storage, metadata_path, json.dumps(metadata).encode("utf-8")
45+
)
46+
obj.spritesheet_metadata.name = metadata_path
47+
48+
obj.save()
49+
logger.info("Object %s (%s): spritesheet generated.", obj.pk, base_name)
50+
51+
except Exception as e:
52+
logger.error(
53+
"Object %s: failed to generate spritesheet: %s", obj.pk, e
54+
)
55+
continue
56+
57+
58+
def _save_to_storage(storage, path, content_bytes):
59+
"""Save content to storage, deleting existing file first for idempotency."""
60+
try:
61+
if storage.exists(path):
62+
storage.delete(path)
63+
except Exception:
64+
pass
65+
storage.save(path, ContentFile(content_bytes))
66+
67+
68+
class Migration(migrations.Migration):
69+
70+
dependencies = [
71+
("core", "0030_object_spritesheet_fields"),
72+
]
73+
74+
operations = [
75+
migrations.RunPython(
76+
generate_spritesheets_for_existing_gifs,
77+
reverse_code=migrations.RunPython.noop,
78+
),
79+
]

src/core/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,16 @@ class Object(TimeStampedModel, ContentMixin):
309309
blank=True,
310310
null=True,
311311
)
312+
spritesheet_file = models.FileField(
313+
upload_to="objects/spritesheets/",
314+
blank=True,
315+
null=True,
316+
)
317+
spritesheet_metadata = models.FileField(
318+
upload_to="objects/spritesheets/",
319+
blank=True,
320+
null=True,
321+
)
312322

313323
def __str__(self):
314324
return self.source.name
@@ -602,5 +612,9 @@ def remove_source_file(sender, instance, **kwargs):
602612
# source file we just removed. See #849.
603613
if instance.audio_description:
604614
instance.audio_description.delete(False)
615+
if instance.spritesheet_file:
616+
instance.spritesheet_file.delete(False)
617+
if instance.spritesheet_metadata:
618+
instance.spritesheet_metadata.delete(False)
605619
if isinstance(instance, Sound):
606620
instance.file.delete(False)

0 commit comments

Comments
 (0)