Skip to content

Commit 329ded9

Browse files
committed
refactor(migrations): rename reverse migration functions for clarity
1 parent 1845c51 commit 329ded9

File tree

3 files changed

+150
-11
lines changed

3 files changed

+150
-11
lines changed

src/backend/InvenTree/common/migrations/0041_migrate_company_images.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def forwards_migrate_company_images(apps, schema_editor):
4545

4646
new_img.save()
4747

48-
def reverse_migrate_inventree_images_to_company(apps, schema_editor):
48+
def reverse_images(apps, schema_editor):
4949
"""Reverse migration: move InvenTreeImage back to Company image field."""
5050
Company = apps.get_model('company', 'Company')
5151
InvenTreeImage = apps.get_model('common', 'InvenTreeImage')
@@ -82,7 +82,7 @@ class Migration(migrations.Migration):
8282
operations = [
8383
migrations.RunPython(
8484
forwards_migrate_company_images,
85-
reverse_migrate_inventree_images_to_company,
85+
reverse_code=reverse_images,
8686
),
8787

8888
]

src/backend/InvenTree/common/migrations/0042_migrate_part_images.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def forwards_migrate_part_images(apps, schema_editor):
6868
img.save()
6969

7070

71-
def reverse_migrate_inventree_images_to_part(apps, schema_editor):
71+
def reverse_images(apps, schema_editor):
7272
"""Reverse migration: move InvenTreeImage back to Part.image."""
7373
Part = apps.get_model('part', 'Part')
7474
InvenTreeImage = apps.get_model('common', 'InvenTreeImage')
@@ -88,14 +88,14 @@ def reverse_migrate_inventree_images_to_part(apps, schema_editor):
8888
class Migration(migrations.Migration):
8989

9090
dependencies = [
91-
('part', '0139_remove_bomitem_overage'),
91+
('part', '0142_remove_part_last_stocktake_remove_partstocktake_note_and_more'),
9292
('common', '0041_migrate_company_images'),
9393
('contenttypes', '0002_remove_content_type_name'),
9494
]
9595

9696
operations = [
9797
migrations.RunPython(
9898
forwards_migrate_part_images,
99-
reverse_migrate_inventree_images_to_part,
99+
reverse_code=reverse_images,
100100
),
101101
]

src/backend/InvenTree/common/test_migrations.py

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def test_company_image_migrated(self):
309309
self.assertTrue(
310310
inv_img.primary, f'Image for company {pk} not marked primary'
311311
)
312-
# NEW: Check that image file actually exists
312+
# Check that image file actually exists
313313
self.assertTrue(inv_img.image, f'Image field is empty for company {pk}')
314314
self.assertIsNotNone(
315315
inv_img.image.name, f'Image name is None for company {pk}'
@@ -372,11 +372,6 @@ def test_image_file_integrity(self):
372372
content = f.read()
373373
self.assertGreater(len(content), 0, 'Image file is empty')
374374

375-
def test_multiple_companies_same_image(self):
376-
"""Test deduplication when multiple companies share the same image filename."""
377-
# This would require modifying prepare() to create this scenario
378-
# or creating a separate test class
379-
380375
def test_content_type_exists(self):
381376
"""Verify that content types are properly set up."""
382377
ContentType = self.new_state.apps.get_model('contenttypes', 'contenttype')
@@ -422,6 +417,150 @@ def test_image_metadata_preserved(self):
422417
)
423418

424419

420+
class TestLegacyImageMigrationReverse(MigratorTestCase):
421+
"""Test that InvenTreeImage values are correctly migrated back to Company.image and Part.image."""
422+
423+
# Start from the new state (after migration) and migrate back
424+
migrate_from = [('common', '0042_migrate_part_images')]
425+
migrate_to = [('common', '0040_inventreeimage')]
426+
427+
@classmethod
428+
def setUpClass(cls):
429+
"""Set up the test class."""
430+
super().setUpClass()
431+
cls._temp_media = tempfile.mkdtemp(prefix='test_media_reverse_')
432+
cls._override = override_settings(MEDIA_ROOT=cls._temp_media)
433+
cls._override.enable()
434+
435+
@classmethod
436+
def tearDownClass(cls):
437+
"""Clean up after the test class."""
438+
super().tearDownClass()
439+
cls._override.disable()
440+
shutil.rmtree(cls._temp_media, ignore_errors=True)
441+
442+
def prepare(self):
443+
"""Populate the 'new' database state (with InvenTreeImage model)."""
444+
# Get models from the NEW state (after forward migration)
445+
Company = self.old_state.apps.get_model('company', 'company')
446+
Part = self.old_state.apps.get_model('part', 'part')
447+
InvenTreeImage = self.old_state.apps.get_model('common', 'inventreeimage')
448+
ContentType = self.old_state.apps.get_model('contenttypes', 'contenttype')
449+
450+
# --- COMPANY SETUP ---
451+
ct_company = ContentType.objects.get(app_label='company', model='company')
452+
453+
self.company_data = [
454+
{'name': 'CoOne', 'file_name': 'test01.png'},
455+
{'name': 'CoTwo', 'file_name': 'test02.png'},
456+
]
457+
self.co_pks = []
458+
459+
for entry in self.company_data:
460+
# Create company without image
461+
co = Company.objects.create(name=entry['name'])
462+
self.co_pks.append(co.pk)
463+
464+
# Create InvenTreeImage for this company
465+
img = generate_image(filename=entry['file_name'])
466+
InvenTreeImage.objects.create(
467+
content_type=ct_company, object_id=co.pk, image=img, primary=True
468+
)
469+
470+
# Company with no image
471+
no_image_co = Company.objects.create(
472+
name='NoImageCo', description='No image here'
473+
)
474+
self.no_image_co_pk = no_image_co.pk
475+
476+
# --- PART SETUP ---
477+
ct_part = ContentType.objects.get(app_label='part', model='part')
478+
479+
# Dummy MPPT data
480+
tree = {'tree_id': 0, 'level': 0, 'lft': 0, 'rght': 0}
481+
482+
self.part_data = [
483+
{'name': 'PartOne', 'file_name': 'part01.png'},
484+
{'name': 'PartTwo', 'file_name': 'part02.png'},
485+
]
486+
self.part_pks = []
487+
488+
for entry in self.part_data:
489+
# Create part without image
490+
part = Part.objects.create(
491+
name=entry['name'],
492+
description='Test Part Description',
493+
active=True,
494+
assembly=True,
495+
purchaseable=True,
496+
**tree,
497+
)
498+
self.part_pks.append(part.pk)
499+
500+
# Create InvenTreeImage for this part
501+
img = generate_image(filename=entry['file_name'])
502+
InvenTreeImage.objects.create(
503+
content_type=ct_part, object_id=part.pk, image=img, primary=True
504+
)
505+
506+
# Part with no image
507+
no_image_part = Part.objects.create(
508+
name='NoImagePart',
509+
description='Test NoImagePart Description',
510+
active=True,
511+
assembly=True,
512+
purchaseable=True,
513+
**tree,
514+
)
515+
self.no_image_part_pk = no_image_part.pk
516+
517+
def test_company_image_reverse_migrated(self):
518+
"""After reverse migration, Company.image should be restored."""
519+
Company = self.new_state.apps.get_model('company', 'company')
520+
521+
# Check each company has its image restored
522+
for idx, _data in enumerate(self.company_data):
523+
pk = self.co_pks[idx]
524+
co = Company.objects.get(pk=pk)
525+
526+
# Verify image field is populated
527+
self.assertTrue(co.image, f'Image not restored for company {pk}')
528+
self.assertIsNotNone(co.image.name, f'Image name is None for company {pk}')
529+
530+
# Verify image file exists
531+
self.assertTrue(
532+
co.image.storage.exists(co.image.name),
533+
f'Image file does not exist for company {pk}',
534+
)
535+
536+
# Verify company with no image still has no image
537+
no_img_co = Company.objects.get(pk=self.no_image_co_pk)
538+
self.assertFalse(no_img_co.image, 'Company should not have image')
539+
540+
def test_part_image_reverse_migrated(self):
541+
"""After reverse migration, Part.image should be restored."""
542+
Part = self.new_state.apps.get_model('part', 'part')
543+
544+
# Check each part has its image restored
545+
for idx, _data in enumerate(self.part_data):
546+
pk = self.part_pks[idx]
547+
part = Part.objects.get(pk=pk)
548+
549+
# Verify image field is populated
550+
self.assertTrue(part.image, f'Image not restored for part {pk}')
551+
self.assertIsNotNone(part.image.name, f'Image name is None for part {pk}')
552+
553+
# Verify image file exists
554+
self.assertTrue(
555+
part.image.storage.exists(part.image.name),
556+
f'Image file does not exist for part {pk}',
557+
)
558+
559+
# Verify part with no image still has no image
560+
no_img_part = Part.objects.get(pk=self.no_image_part_pk)
561+
self.assertFalse(no_img_part.image, 'Part should not have image')
562+
563+
425564
def prep_currency_migration(self, vals: str):
426565
"""Prepare the environment for the currency migration tests."""
427566
# Set keys

0 commit comments

Comments
 (0)