diff --git a/src/pretix/base/migrations/0297_add_reusablemedium_secret.py b/src/pretix/base/migrations/0297_add_reusablemedium_secret.py new file mode 100644 index 0000000000..64bab850a4 --- /dev/null +++ b/src/pretix/base/migrations/0297_add_reusablemedium_secret.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.26 on 2025-11-24 11:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0296_invoice_invoice_from_state"), + ] + + operations = [ + migrations.AddField( + model_name="reusablemedium", + name="secret", + field=models.CharField(max_length=200, null=True), + ), + # use temporary related_name "linked_mediums" for ManyToManyField, so we can migrate existing data + migrations.AddField( + model_name="reusablemedium", + name="linked_orderpositions", + field=models.ManyToManyField( + related_name="linked_mediums", to="pretixbase.orderposition" + ), + ), + migrations.RunSQL( + sql="INSERT INTO pretixbase_reusablemedium_linked_orderpositions (reusablemedium_id, orderposition_id) SELECT id, linked_orderposition_id FROM pretixbase_reusablemedium;", + reverse_sql="DELETE FROM pretixbase_reusablemedium_linked_orderpositions;", + ), + ] diff --git a/src/pretix/base/migrations/0298_reusablemedium_remove_orderposition.py b/src/pretix/base/migrations/0298_reusablemedium_remove_orderposition.py new file mode 100644 index 0000000000..7054783e49 --- /dev/null +++ b/src/pretix/base/migrations/0298_reusablemedium_remove_orderposition.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.26 on 2025-11-24 11:32 + +from django.db import migrations, models + + +def reverse(apps, schema_editor): + ReusableMedium = apps.get_model('pretixbase', 'ReusableMedium') + + qs = ReusableMedium.linked_orderpositions.through.objects + objs = [] + # get last added orderposition from linked_orderpositions + for rm_id, op_id in qs.filter(id__in=qs.values("reusablemedium_id").annotate(max_id=models.Max('id')).values('max_id')).values_list("reusablemedium_id", "orderposition_id"): + #obj = ReusableMedium.objects.get(id=rm_id) + #obj.linked_orderposition_id = op_id + obj = ReusableMedium( + id=rm_id, + linked_orderposition_id=op_id, + ) + objs.append(obj) + + ReusableMedium.objects.bulk_update(objs, ['linked_orderposition_id']) + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0297_add_reusablemedium_secret"), + ] + + operations = [ + # according to the docs, UPDATE FROM should run similarly on sqlite and postgres, but I could not get it to work + # so roll back the data migration with code before deleting data from through-table in 0297 + migrations.RunPython(migrations.RunPython.noop, reverse), + migrations.RemoveField( + model_name="reusablemedium", + name="linked_orderposition", + ), + # change related_name for new ManyToManyField to previously used linked_media + migrations.AlterField( + model_name="reusablemedium", + name="linked_orderpositions", + field=models.ManyToManyField( + related_name="linked_media", to="pretixbase.orderposition" + ), + ), + ] diff --git a/src/pretix/base/models/media.py b/src/pretix/base/models/media.py index d14dbdcc10..8dd765bc72 100644 --- a/src/pretix/base/models/media.py +++ b/src/pretix/base/models/media.py @@ -72,6 +72,11 @@ class ReusableMedium(LoggedModel): max_length=200, verbose_name=pgettext_lazy('reusable_medium', 'Identifier'), ) + secret = models.CharField( + max_length=200, + verbose_name=pgettext_lazy('reusable_medium', 'Secret'), + null=True, blank=True + ) active = models.BooleanField( verbose_name=_('Active'), @@ -89,12 +94,10 @@ class ReusableMedium(LoggedModel): on_delete=models.SET_NULL, verbose_name=_('Customer account'), ) - linked_orderposition = models.ForeignKey( + linked_orderpositions = models.ManyToManyField( OrderPosition, - null=True, blank=True, related_name='linked_media', - on_delete=models.SET_NULL, - verbose_name=_('Linked ticket'), + verbose_name=_('Linked tickets'), ) linked_giftcard = models.ForeignKey( GiftCard, @@ -111,6 +114,11 @@ class ReusableMedium(LoggedModel): objects = ReusableMediumQuerySetManager() + @cached_property + def linked_orderposition(self): + # always return last added linked_orderposition to make it behave backwardscompatible + return self.linked_orderpositions.order_by('pk').last() + @cached_property def media_type(self): return MEDIA_TYPES[self.type]