mirror of
https://github.com/pretix/pretix.git
synced 2026-05-11 16:13:59 +00:00
Subevents: Allow to skip conflicting dates in bulk-creation (Z#23217384) (#6079)
* Subevents: Allow to skip conflicting dates in bulk-creation * Update src/pretix/control/templates/pretixcontrol/subevents/bulk.html * Fix overlap calc for consecutive subevents * Add test for skipping conflicting dates in bulk-creation --------- Co-authored-by: Richard Schreiber <schreiber@pretix.eu> Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
This commit is contained in:
@@ -28,7 +28,7 @@ from django.forms import formset_factory
|
|||||||
from django.forms.utils import ErrorDict
|
from django.forms.utils import ErrorDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from i18nfield.forms import I18nInlineFormSet
|
from i18nfield.forms import I18nInlineFormSet
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
@@ -102,6 +102,16 @@ class SubEventBulkForm(SubEventForm):
|
|||||||
required=False,
|
required=False,
|
||||||
limit_choices=('date_from', 'date_to'),
|
limit_choices=('date_from', 'date_to'),
|
||||||
)
|
)
|
||||||
|
skip_if_overlap = forms.BooleanField(
|
||||||
|
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
|
||||||
|
help_text=pgettext_lazy(
|
||||||
|
'subevent',
|
||||||
|
'This can be useful if all your dates happen in the same location and no repeated dates should '
|
||||||
|
'be created in conflict with existing special events. This respects even inactive dates and works best if '
|
||||||
|
'all dates have both a start and end time.'
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = kwargs['event']
|
self.event = kwargs['event']
|
||||||
|
|||||||
@@ -379,6 +379,8 @@
|
|||||||
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
|
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<hr />
|
||||||
|
{% bootstrap_field form.skip_if_overlap layout="control" horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "General information" %}</legend>
|
<legend>{% trans "General information" %}</legend>
|
||||||
|
|||||||
@@ -917,6 +917,35 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
|
|||||||
if len(subevents) > 100_000:
|
if len(subevents) > 100_000:
|
||||||
raise ValidationError(_('Please do not create more than 100.000 dates at once.'))
|
raise ValidationError(_('Please do not create more than 100.000 dates at once.'))
|
||||||
|
|
||||||
|
if form.cleaned_data.get("skip_if_overlap") and subevents:
|
||||||
|
def overlaps(a_from, a_to, b_from, b_to):
|
||||||
|
if a_from == b_from:
|
||||||
|
return True
|
||||||
|
if a_from > b_from:
|
||||||
|
# a starts after b
|
||||||
|
# check if it starts before b ends
|
||||||
|
return b_to and a_from < b_to
|
||||||
|
# a starts before b
|
||||||
|
# check if it ends before b starts
|
||||||
|
return a_to and a_to > b_from
|
||||||
|
|
||||||
|
date_min = min(se.date_from for se in subevents)
|
||||||
|
date_max = max(se.date_to or se.date_from for se in subevents)
|
||||||
|
dates_existing = list(self.request.event.subevents.annotate(
|
||||||
|
date_fromto=Coalesce('date_to', 'date_from'),
|
||||||
|
).filter(
|
||||||
|
date_from__lte=date_max,
|
||||||
|
date_fromto__gte=date_min,
|
||||||
|
).values('date_from', 'date_to'))
|
||||||
|
subevents = [
|
||||||
|
se for se in subevents if not any(
|
||||||
|
overlaps(se.date_from, se.date_to, other['date_from'], other['date_to'])
|
||||||
|
for other in dates_existing
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if not subevents:
|
||||||
|
raise ValidationError(_('All dates would be skipped because they conflict with existing dates.'))
|
||||||
|
|
||||||
for i, se in enumerate(subevents):
|
for i, se in enumerate(subevents):
|
||||||
se.save(clear_cache=False)
|
se.save(clear_cache=False)
|
||||||
if i % 100 == 0:
|
if i % 100 == 0:
|
||||||
|
|||||||
@@ -747,6 +747,92 @@ class SubEventsTest(SoupTest):
|
|||||||
assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00"
|
assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00"
|
||||||
assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00"
|
assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00"
|
||||||
|
|
||||||
|
def test_create_bulk_skip_existing(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
self.event1.subevents.all().delete()
|
||||||
|
# SubEvent ends at rrule start time
|
||||||
|
self.event1.subevents.create(
|
||||||
|
date_from=datetime.datetime(2018, 4, 4, 9, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
date_to=datetime.datetime(2018, 4, 4, 10, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
# SubEvent overlaps rrule start
|
||||||
|
self.event1.subevents.create(
|
||||||
|
date_from=datetime.datetime(2018, 4, 5, 9, 30, tzinfo=datetime.timezone.utc),
|
||||||
|
date_to=datetime.datetime(2018, 4, 5, 10, 30, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
# SubEvent times are same as rrule
|
||||||
|
self.event1.subevents.create(
|
||||||
|
date_from=datetime.datetime(2018, 4, 6, 10, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
date_to=datetime.datetime(2018, 4, 6, 11, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
# SubEvent starts at rrule end time
|
||||||
|
self.event1.subevents.create(
|
||||||
|
date_from=datetime.datetime(2018, 4, 7, 11, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
date_to=datetime.datetime(2018, 4, 7, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
# SubEvent overlaps entire rrule time
|
||||||
|
self.event1.subevents.create(
|
||||||
|
date_from=datetime.datetime(2018, 4, 8, 9, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
date_to=datetime.datetime(2018, 4, 8, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
# SubEvent has before rrule time and no end
|
||||||
|
self.event1.subevents.create(
|
||||||
|
date_from=datetime.datetime(2018, 4, 9, 9, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
existing_events = list(self.event1.subevents.values_list('pk', flat=True))
|
||||||
|
|
||||||
|
self.event1.settings.timezone = 'Europe/Berlin'
|
||||||
|
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
|
||||||
|
'rruleformset-TOTAL_FORMS': '1',
|
||||||
|
'rruleformset-INITIAL_FORMS': '0',
|
||||||
|
'rruleformset-MIN_NUM_FORMS': '0',
|
||||||
|
'rruleformset-MAX_NUM_FORMS': '1000',
|
||||||
|
'rruleformset-0-end': 'count',
|
||||||
|
'rruleformset-0-count': '10',
|
||||||
|
'rruleformset-0-interval': '1',
|
||||||
|
'rruleformset-0-freq': 'weekly',
|
||||||
|
'rruleformset-0-dtstart': '2018-04-03',
|
||||||
|
'rruleformset-0-weekly_byweekday': ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'],
|
||||||
|
'rruleformset-0-yearly_same': 'on',
|
||||||
|
'rruleformset-0-monthly_same': 'on',
|
||||||
|
'timeformset-TOTAL_FORMS': '1',
|
||||||
|
'timeformset-INITIAL_FORMS': '0',
|
||||||
|
'timeformset-MIN_NUM_FORMS': '1',
|
||||||
|
'timeformset-MAX_NUM_FORMS': '1000',
|
||||||
|
'timeformset-0-time_from': '12:00:00',
|
||||||
|
'timeformset-0-time_to': '13:00:00',
|
||||||
|
'rruleformset-0-until': '2019-04-03',
|
||||||
|
'skip_if_overlap': 'on',
|
||||||
|
'name_0': 'Foo',
|
||||||
|
'active': 'on',
|
||||||
|
'frontpage_text_0': '',
|
||||||
|
'quotas-TOTAL_FORMS': '1',
|
||||||
|
'quotas-INITIAL_FORMS': '0',
|
||||||
|
'quotas-MIN_NUM_FORMS': '0',
|
||||||
|
'quotas-MAX_NUM_FORMS': '1000',
|
||||||
|
'quotas-0-name': 'Q1',
|
||||||
|
'quotas-0-size': '50',
|
||||||
|
'quotas-0-itemvars': str(self.ticket.pk),
|
||||||
|
'checkinlist_set-TOTAL_FORMS': '0',
|
||||||
|
'checkinlist_set-INITIAL_FORMS': '0',
|
||||||
|
'checkinlist_set-MIN_NUM_FORMS': '0',
|
||||||
|
'checkinlist_set-MAX_NUM_FORMS': '1000',
|
||||||
|
})
|
||||||
|
assert doc.select(".alert-success")
|
||||||
|
with scopes_disabled():
|
||||||
|
ses = list(self.event1.subevents.exclude(pk__in=existing_events).order_by('date_from'))
|
||||||
|
|
||||||
|
assert len(ses) == 7
|
||||||
|
assert [s.date_from.date().isoformat() for s in ses] == [
|
||||||
|
'2018-04-03',
|
||||||
|
'2018-04-04',
|
||||||
|
'2018-04-07',
|
||||||
|
'2018-04-09',
|
||||||
|
'2018-04-10',
|
||||||
|
'2018-04-11',
|
||||||
|
'2018-04-12'
|
||||||
|
]
|
||||||
|
|
||||||
def test_delete_bulk(self):
|
def test_delete_bulk(self):
|
||||||
self.subevent2.active = True
|
self.subevent2.active = True
|
||||||
self.subevent2.save()
|
self.subevent2.save()
|
||||||
|
|||||||
Reference in New Issue
Block a user