mirror of
https://github.com/pretix/pretix.git
synced 2026-05-15 16:54:00 +00:00
Compare commits
8 Commits
release/20
...
fix-clone-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c808afc2e9 | ||
|
|
baf7e098b6 | ||
|
|
a78e2f3ceb | ||
|
|
2fdd9f991c | ||
|
|
111127c0fa | ||
|
|
f1a4b51604 | ||
|
|
c9d29bbca5 | ||
|
|
bfdf959fad |
81
src/pretix/base/migrations/0299_fixup_eventmetaproperties.py
Normal file
81
src/pretix/base/migrations/0299_fixup_eventmetaproperties.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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'))
|
||||
for emv in cross_org_values:
|
||||
logger.info(f"Fixup cross-organizer value {emv.event.organizer.slug}/{emv.event.slug}\n before: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {emv.value}")
|
||||
try:
|
||||
emv.property = emv.event.organizer.meta_properties.filter(name=emv.property.name).first()
|
||||
logger.info(f" found existing EventMetaProperty in {emv.event.organizer.slug}")
|
||||
if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists():
|
||||
logger.info(f" EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
||||
emv.delete()
|
||||
continue
|
||||
except EventMetaProperty.DoesNotExist:
|
||||
meta_prop = emv.property
|
||||
meta_prop.pk = None
|
||||
meta_prop.organizer = emv.event.organizer
|
||||
meta_prop.save(force_insert=True)
|
||||
logger.info(f" created new EventMetaProperty")
|
||||
emv.property = meta_prop
|
||||
logger.info(f" after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {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.info(f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}")
|
||||
props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name']))
|
||||
|
||||
# TODO: any better idea than picking the property to keep more or less randomly (first in database order)?
|
||||
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.info(f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.info(f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
||||
logger.info(f" {e}")
|
||||
for prop in invalid:
|
||||
newname = f'{prop.name}_DUPLICATE_{prop.id}'
|
||||
logger.info(f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})")
|
||||
prop.name = newname
|
||||
prop.save()
|
||||
|
||||
else:
|
||||
for prop in invalid:
|
||||
logger.info(f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})")
|
||||
prop.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0298_pluggable_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop),
|
||||
migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -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", "0299_fixup_eventmetaproperties"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="eventmetaproperty",
|
||||
unique_together={("organizer", "name")},
|
||||
),
|
||||
]
|
||||
@@ -877,6 +877,8 @@ class Event(EventMixin, LoggedModel):
|
||||
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
|
||||
)
|
||||
|
||||
is_cross_organizer = other.organizer_id != self.organizer_id
|
||||
|
||||
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
|
||||
# Plugins can create data in installed() hook based on existing data of the event.
|
||||
# Calling set_active_plugins() results in defaults being created while actually data
|
||||
@@ -905,6 +907,15 @@ class Event(EventMixin, LoggedModel):
|
||||
for emv in EventMetaValue.objects.filter(event=other):
|
||||
emv.pk = None
|
||||
emv.event = self
|
||||
if is_cross_organizer:
|
||||
try:
|
||||
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
|
||||
except EventMetaProperty.DoesNotExist:
|
||||
meta_prop = emv.property
|
||||
meta_prop.pk = None
|
||||
meta_prop.organizer = self.organizer
|
||||
meta_prop.save(force_insert=True)
|
||||
emv.property = meta_prop
|
||||
emv.save(force_insert=True)
|
||||
|
||||
for fl in EventFooterLink.objects.filter(event=other):
|
||||
@@ -958,13 +969,13 @@ class Event(EventMixin, LoggedModel):
|
||||
if i.tax_rule_id:
|
||||
i.tax_rule = tax_map[i.tax_rule_id]
|
||||
|
||||
if i.grant_membership_type and other.organizer_id != self.organizer_id:
|
||||
if i.grant_membership_type and is_cross_organizer:
|
||||
i.grant_membership_type = None
|
||||
|
||||
i.save() # no force_insert since i.picture.save could have already inserted
|
||||
i.log_action('pretix.object.cloned')
|
||||
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
if require_membership_types and not is_cross_organizer:
|
||||
i.require_membership_types.set(require_membership_types)
|
||||
|
||||
if not i.all_sales_channels:
|
||||
@@ -979,7 +990,7 @@ class Event(EventMixin, LoggedModel):
|
||||
v._prefetched_objects_cache = {}
|
||||
v.save(force_insert=True)
|
||||
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
if require_membership_types and not is_cross_organizer:
|
||||
v.require_membership_types.set(require_membership_types)
|
||||
if not v.all_sales_channels:
|
||||
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
@@ -1830,6 +1841,7 @@ class EventMetaProperty(LoggedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ("position", "name",)
|
||||
unique_together = ('organizer', 'name')
|
||||
|
||||
@property
|
||||
def choice_keys(self):
|
||||
@@ -1863,6 +1875,8 @@ class EventMetaValue(LoggedModel):
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.event and self.event.organizer != self.property.organizer:
|
||||
raise ValidationError(_("Property and event must belong to the same organizer."))
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
@@ -238,6 +238,9 @@ def test_full_clone_cross_organizer_differences():
|
||||
sc1_c = organizer.sales_channels.create(identifier="c")
|
||||
sc2_a = organizer2.sales_channels.get(identifier="web")
|
||||
sc2_c = organizer2.sales_channels.create(identifier="c")
|
||||
o1_meta_prop_a = organizer.meta_properties.create(name="Prop to copy")
|
||||
o1_meta_prop_b = organizer.meta_properties.create(name="Prop to find")
|
||||
o2_meta_prop_b = organizer2.meta_properties.create(name="Prop to find")
|
||||
|
||||
event = Event.objects.create(
|
||||
organizer=organizer, name='Dummy', slug='dummy',
|
||||
@@ -262,6 +265,9 @@ def test_full_clone_cross_organizer_differences():
|
||||
event.settings.payment_giftcard__enabled = True
|
||||
event.settings.payment_giftcard__restrict_to_sales_channels = ['web', 'b', 'c']
|
||||
|
||||
event.meta_values.create(property=o1_meta_prop_a, value='a')
|
||||
event.meta_values.create(property=o1_meta_prop_b, value='b')
|
||||
|
||||
copied_event = Event.objects.create(
|
||||
organizer=organizer2, name='Dummy2', slug='dummy2',
|
||||
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
@@ -284,3 +290,9 @@ def test_full_clone_cross_organizer_differences():
|
||||
|
||||
assert event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'b', 'c']
|
||||
assert copied_event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'c']
|
||||
|
||||
assert event.meta_values.get(property__name=o1_meta_prop_a.name).property.organizer == organizer
|
||||
assert copied_event.meta_values.get(property__name=o1_meta_prop_a.name).value == 'a'
|
||||
assert copied_event.meta_values.get(property__name=o1_meta_prop_a.name).property.organizer == organizer2
|
||||
assert copied_event.meta_values.get(property=o2_meta_prop_b).value == 'b'
|
||||
assert copied_event.meta_values.get(property=o2_meta_prop_b).property.organizer == organizer2
|
||||
|
||||
Reference in New Issue
Block a user