Add program times for items (Z#23178639)

* Add program times for items

* Fix frontend date validation

* Add ical data for program times [wip]

* Improve ical data for program times

* Remove duplicate code and add comments

* Adjust migration

* Remove program times form for event series

* Add pdf placeholder [wip]

* Improve explanation text with suggestion

Co-authored-by: Raphael Michel <michel@pretix.eu>

* Fix import sorting

* Improve ical generation

* Improve ical entry description

* Fix migration

* Add copyability for program times fot items and events

* Update migration

* Add API endpoints/functions, fix isort

* Improve variable name

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Remove todo comment

* Add documentation, Change endpoint name

* Change related name

* Remove unnecessary code block

* Add program times to item API

* Fix imports

* Add log text

* Use daterange helper

* Add and update API tests

* Add another API test

* Add program times to cloning tests

* Update query count because of program times query

* Invalidate cached tickets on program time changes

* Reduce invalidation calls

* Update migration after rebase

* Apply improvements to invalidation from review

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* remove unneccessary attr=item param

* remove unnecessary kwargs for formset_factory

* fix local var name being overwritten in for-loop

* fix empty formset being saved

* Use subevent if available

* make code less verbose

* remove double event-label in ical desc

* fix unnecessary var re-assign

* fix ev vs p.subevent

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Phin Wolkwitz
2025-11-06 12:24:47 +01:00
committed by GitHub
parent 7041d40972
commit fd9d03786b
20 changed files with 903 additions and 89 deletions

View File

@@ -20,10 +20,12 @@
# <https://www.gnu.org/licenses/>.
#
import datetime
from collections import namedtuple
from urllib.parse import urlparse
import vobject
from django.conf import settings
from django.db.models import prefetch_related_objects
from django.utils.formats import date_format
from django.utils.translation import gettext as _
@@ -122,61 +124,109 @@ def get_private_icals(event, positions):
creation_time = datetime.datetime.now(datetime.timezone.utc)
calobjects = []
calentries = set() # using set for automatic deduplication of CalEntries
CalEntry = namedtuple('CalEntry', ['summary', 'description', 'location', 'dtstart', 'dtend', 'uid'])
evs = set(p.subevent or event for p in positions)
for ev in evs:
if isinstance(ev, Event):
# collecting the positions' calendar entries, preferring the most exact date and time available (positions > subevent > event)
prefetch_related_objects(positions, 'item__program_times')
for p in positions:
ev = p.subevent or event
program_times = p.item.program_times.all()
if program_times:
# if program times have been configured, they are preferred for the position's calendar entries
url = build_absolute_uri(event, 'presale:event.index')
for index, pt in enumerate(program_times):
summary = _('{event} - {item}').format(event=ev, item=p.item.name)
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
descr.append(str(_('Start: {datetime}')).format(
datetime=date_format(pt.start.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
descr.append(str(_('End: {datetime}')).format(
datetime=date_format(pt.end.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
location = None
dtstart = pt.start.astimezone(tz)
dtend = pt.end.astimezone(tz)
uid = 'pretix-{}-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
p.item.id,
index,
urlparse(url).netloc
)
calentries.add(CalEntry(summary, description, location, dtstart, dtend, uid))
else:
url = build_absolute_uri(event, 'presale:event.index', {
'subevent': ev.pk
})
# without program times, the subevent or event times are used for calendar entries, preferring subevents
if p.subevent:
url = build_absolute_uri(event, 'presale:event.index', {
'subevent': p.subevent.pk
})
else:
url = build_absolute_uri(event, 'presale:event.index')
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
if ev.date_admission:
descr.append(str(_('Admission: {datetime}')).format(
datetime=date_format(ev.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
if ev.date_admission:
descr.append(str(_('Admission: {datetime}')).format(
datetime=date_format(ev.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
summary = str(ev.name)
if ev.location:
location = ", ".join(l.strip() for l in str(ev.location).splitlines() if l.strip())
else:
location = None
if event.settings.show_times:
dtstart = ev.date_from.astimezone(tz)
else:
dtstart = ev.date_from.astimezone(tz).date()
if event.settings.show_date_to and ev.date_to:
if event.settings.show_times:
dtend = ev.date_to.astimezone(tz)
else:
# with full-day events date_to in pretix is included (e.g. last day)
# whereas dtend in vcalendar is non-inclusive => add one day for export
dtend = ev.date_to.astimezone(tz).date() + datetime.timedelta(days=1)
else:
dtend = None
uid = 'pretix-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
ev.pk if p.subevent else '0',
urlparse(url).netloc
)
calentries.add(CalEntry(summary, description, location, dtstart, dtend, uid))
for calentry in calentries:
cal = vobject.iCalendar()
cal.add('prodid').value = '-//pretix//{}//'.format(settings.PRETIX_INSTANCE_NAME.replace(" ", "_"))
vevent = cal.add('vevent')
vevent.add('summary').value = str(ev.name)
vevent.add('description').value = description
vevent.add('summary').value = calentry.summary
vevent.add('description').value = calentry.description
vevent.add('dtstamp').value = creation_time
if ev.location:
vevent.add('location').value = ", ".join(l.strip() for l in str(ev.location).splitlines() if l.strip())
vevent.add('uid').value = 'pretix-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
ev.pk if not isinstance(ev, Event) else '0',
urlparse(url).netloc
)
if event.settings.show_times:
vevent.add('dtstart').value = ev.date_from.astimezone(tz)
else:
vevent.add('dtstart').value = ev.date_from.astimezone(tz).date()
if event.settings.show_date_to and ev.date_to:
if event.settings.show_times:
vevent.add('dtend').value = ev.date_to.astimezone(tz)
else:
# with full-day events date_to in pretix is included (e.g. last day)
# whereas dtend in vcalendar is non-inclusive => add one day for export
vevent.add('dtend').value = ev.date_to.astimezone(tz).date() + datetime.timedelta(days=1)
if calentry.location:
vevent.add('location').value = calentry.location
vevent.add('uid').value = calentry.uid
vevent.add('dtstart').value = calentry.dtstart
if calentry.dtend:
vevent.add('dtend').value = calentry.dtend
calobjects.append(cal)
return calobjects