diff --git a/src/pretix/base/migrations/0302_fixup_eventmetaproperties.py b/src/pretix/base/migrations/0302_fixup_eventmetaproperties.py new file mode 100644 index 0000000000..89672216ed --- /dev/null +++ b/src/pretix/base/migrations/0302_fixup_eventmetaproperties.py @@ -0,0 +1,91 @@ +# Generated by Django 5.2.12 on 2026-04-28 11:34 +import logging + +from django.db import IntegrityError, migrations, transaction +from django.db.models import Count, F + +logger = logging.getLogger(__name__) + + +def fix_cross_organizer_eventmetavalues(apps, schema_editor): + EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty") + EventMetaValue = apps.get_model("pretixbase", "EventMetaValue") + + cross_org_values = EventMetaValue.objects.filter( + event__organizer__pk__ne=F('property__organizer__pk') + ).order_by('event__organizer__slug', 'event__slug') + for emv in cross_org_values: + logger.warning("%s", f"Fixing cross-organizer EventMetaValue: {emv.event.organizer.slug}/{emv.event.slug}") + logger.warning(" %s", f"{emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}") + try: + emv.property = emv.event.organizer.meta_properties.get(name=emv.property.name) + if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists(): + correct = EventMetaValue.objects.get(event=emv.event, property=emv.property) + if correct.value != emv.value: + logger.warning(" %s", f"WARN: conflicting EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one") + else: + logger.warning(" %s", f"OK: same-value EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one") + logger.warning(" %s", f"keeping: {correct.property.name}({correct.property.id}@{correct.property.organizer.slug}) = {repr(correct.value)}") + emv.delete() + else: + logger.warning(" %s", f"OK: found existing EventMetaProperty in {emv.event.organizer.slug}, updating reference") + logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}") + emv.save(update_fields=["property"]) + except EventMetaProperty.DoesNotExist: + meta_prop = emv.property + meta_prop.pk = None + meta_prop.organizer = emv.event.organizer + meta_prop.filter_public = False + meta_prop.save(force_insert=True) + logger.warning(" %s", f"WARN: found no matching EventMetaProperty, creating") + logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}") + emv.save(update_fields=["property"]) + + +def make_eventmetaproperties_unique(apps, schema_editor): + EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty") + EventMetaValue = apps.get_model("pretixbase", "EventMetaValue") + + duplicates = EventMetaProperty.objects.values('organizer', 'organizer__slug', 'name').annotate(count=Count('id')).filter(count__gt=1) + for dup in duplicates: + logger.warning("%s", f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}") + props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name'])) + + target = props[0] + invalid = props[1:] + + try: + with transaction.atomic(): + affected = EventMetaValue.objects.filter( + event__organizer=dup['organizer'], property__in=invalid + ).update( + property=target + ) + logger.warning("%s", f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})") + + except IntegrityError as e: + logger.warning("%s", f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})") + logger.warning("%s", f" {e}") + for prop in invalid: + newname = f'{prop.name}_DUPLICATE_{prop.id}' + logger.warning("%s", f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})") + prop.name = newname + prop.filter_public = False + prop.save() + + else: + for prop in invalid: + logger.warning("%s", f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})") + prop.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0301_reusablemedium_remove_orderposition"), + ] + + operations = [ + migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop), + migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop), + ] diff --git a/src/pretix/base/migrations/0303_alter_eventmetaproperty_unique_together.py b/src/pretix/base/migrations/0303_alter_eventmetaproperty_unique_together.py new file mode 100644 index 0000000000..a16012c2a0 --- /dev/null +++ b/src/pretix/base/migrations/0303_alter_eventmetaproperty_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.12 on 2026-04-28 11:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0302_fixup_eventmetaproperties"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="eventmetaproperty", + unique_together={("organizer", "name")}, + ), + ] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index a56424e05e..464d534f51 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -1851,6 +1851,7 @@ class EventMetaProperty(LoggedModel): class Meta: ordering = ("position", "name",) + unique_together = ('organizer', 'name') @property def choice_keys(self):