diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 4c718cb85..7f38a98a9 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -693,6 +693,7 @@ class EventSettingsSerializer(SettingsSerializer): 'frontpage_subevent_ordering', 'event_list_type', 'event_list_available_only', + 'event_calendar_future_only', 'frontpage_text', 'event_info_text', 'attendee_names_asked', diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index c01c12cef..1eb641da6 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1397,6 +1397,19 @@ DEFAULTS = { 'serializer_class': serializers.BooleanField, 'form_kwargs': dict( label=_("Hide all unavailable dates from calendar or list views"), + help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide " + "calendar.") + ) + }, + 'event_calendar_future_only': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Hide all past dates from calendar"), + help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide " + "calendar.") ) }, 'allow_modifications_after_checkin': { diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 17401df82..d136ce819 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -511,6 +511,7 @@ class EventSettingsForm(SettingsForm): 'low_availability_percentage', 'event_list_type', 'event_list_available_only', + 'event_calendar_future_only', 'frontpage_text', 'event_info_text', 'attendee_names_asked', @@ -602,6 +603,7 @@ class EventSettingsForm(SettingsForm): del self.fields['frontpage_subevent_ordering'] del self.fields['event_list_type'] del self.fields['event_list_available_only'] + del self.fields['event_calendar_future_only'] # create "virtual" fields for better UX when editing _asked and _required fields self.virtual_keys = [] diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index f12eb2d77..d8d2ccbcc 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -315,6 +315,9 @@ {% if sform.event_list_available_only %} {% bootstrap_field sform.event_list_available_only layout="control" %} {% endif %} + {% if sform.event_calendar_future_only %} + {% bootstrap_field sform.event_calendar_future_only layout="control" %} + {% endif %} {% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %} {% url "control:organizer.edit" organizer=request.organizer.slug as org_url %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html index fc6220d5c..fa7ed7e82 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html @@ -4,11 +4,13 @@ diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html index b051f9629..17ea3731b 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html @@ -4,11 +4,13 @@ diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index a1d90822f..cd40fb9e9 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -63,7 +63,7 @@ from pretix.base.channels import get_all_sales_channels from pretix.base.models import ( ItemVariation, Quota, SeatCategoryMapping, Voucher, ) -from pretix.base.models.event import SubEvent +from pretix.base.models.event import Event, SubEvent from pretix.base.models.items import ( ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation, ) @@ -74,7 +74,7 @@ from pretix.presale.ical import get_public_ical from pretix.presale.signals import item_description from pretix.presale.views.organizer import ( EventListMixin, add_subevents_for_days, days_for_template, - filter_qs_by_attr, weeks_for_template, + filter_qs_by_attr, has_before_after, weeks_for_template, ) from ...helpers.formats.en.formats import SHORT_MONTH_DAY_FORMAT, WEEK_FORMAT @@ -586,6 +586,11 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): before = datetime(self.year, self.month, 1, 0, 0, 0, tzinfo=tz) - timedelta(days=1) after = datetime(self.year, self.month, ndays, 0, 0, 0, tzinfo=tz) + timedelta(days=1) + if self.request.event.settings.event_calendar_future_only: + limit_before = now().astimezone(tz) + else: + limit_before = before + context['date'] = date(self.year, self.month, 1) context['before'] = before context['after'] = after @@ -593,7 +598,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): ebd = defaultdict(list) add_subevents_for_days( filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier).using(settings.DATABASE_REPLICA), self.request), - before, after, ebd, set(), self.request.event, + limit_before, after, ebd, set(), self.request.event, self.kwargs.get('cart_namespace'), voucher, ) @@ -606,9 +611,22 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): lambda: len(set(str(n) for n in self.request.event.subevents.order_by().values_list('name', flat=True).annotate(c=Count('*'))[:250])) != 1, timeout=120, ) - context['weeks'] = weeks_for_template(ebd, self.year, self.month) + context['weeks'] = weeks_for_template(ebd, self.year, self.month, future_only=self.request.event.settings.event_calendar_future_only) context['months'] = [date(self.year, i + 1, 1) for i in range(12)] - context['years'] = range(now().year - 2, now().year + 3) + if self.request.event.settings.event_calendar_future_only: + context['years'] = range(now().year, now().year + 3) + else: + context['years'] = range(now().year - 2, now().year + 3) + + context['has_before'], context['has_after'] = has_before_after( + Event.objects.none(), + SubEvent.objects.filter( + event=self.request.event, + ), + before, + after, + future_only=self.request.event.settings.event_calendar_future_only + ) elif context['list_type'] == "week": self._set_week_year() tz = self.request.event.timezone @@ -620,6 +638,11 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=tz ) + timedelta(days=1) + if self.request.event.settings.event_calendar_future_only: + limit_before = now().astimezone(tz) + else: + limit_before = before + context['date'] = week.monday() context['before'] = before context['after'] = after @@ -627,7 +650,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): ebd = defaultdict(list) add_subevents_for_days( filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier).using(settings.DATABASE_REPLICA), self.request), - before, after, ebd, set(), self.request.event, + limit_before, after, ebd, set(), self.request.event, self.kwargs.get('cart_namespace'), voucher, ) @@ -640,13 +663,15 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): lambda: len(set(str(n) for n in self.request.event.subevents.order_by().values_list('name', flat=True).annotate(c=Count('*'))[:250])) != 1, timeout=120, ) - context['days'] = days_for_template(ebd, week) + context['days'] = days_for_template(ebd, week, future_only=self.request.event.settings.event_calendar_future_only) years = (self.year - 1, self.year, self.year + 1) weeks = [] for year in years: weeks += [ (date_fromisocalendar(year, i + 1, 1), date_fromisocalendar(year, i + 1, 7)) for i in range(53 if date(year, 12, 31).isocalendar()[1] == 53 else 52) + if not self.request.event.settings.event_calendar_future_only or + date_fromisocalendar(year, i + 1, 7) > now().astimezone(tz).replace(tzinfo=None) ] context['weeks'] = [[w for w in weeks if w[0].year == year] for year in years] context['week_format'] = get_format('WEEK_FORMAT') @@ -655,6 +680,16 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): context['short_month_day_format'] = get_format('SHORT_MONTH_DAY_FORMAT') if context['short_month_day_format'] == 'SHORT_MONTH_DAY_FORMAT': context['short_month_day_format'] = SHORT_MONTH_DAY_FORMAT + + context['has_before'], context['has_after'] = has_before_after( + Event.objects.none(), + SubEvent.objects.filter( + event=self.request.event, + ), + before, + after, + future_only=self.request.event.settings.event_calendar_future_only + ) else: context['subevent_list'] = self.request.event.subevents_sorted( filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier).using(settings.DATABASE_REPLICA), self.request) diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py index 778ec25f7..45cf1f14c 100644 --- a/src/pretix/presale/views/organizer.py +++ b/src/pretix/presale/views/organizer.py @@ -401,11 +401,11 @@ class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView): return ctx -def has_before_after(eventqs, subeventqs, before, after): +def has_before_after(eventqs, subeventqs, before, after, future_only=False): eqs = eventqs.filter(is_public=True, live=True, has_subevents=False) sqs = subeventqs.filter(active=True, is_public=True) return ( - eqs.filter(Q(date_from__lte=before)).exists() or sqs.filter(Q(date_from__lte=before)).exists(), + (not future_only or before > now()) and (eqs.filter(Q(date_from__lte=before)).exists() or sqs.filter(Q(date_from__lte=before)).exists()), eqs.filter(Q(date_to__gte=after) | Q(date_from__gte=after)).exists() or sqs.filter(Q(date_to__gte=after) | Q(date_from__gte=after)).exists() ) @@ -413,7 +413,6 @@ def has_before_after(eventqs, subeventqs, before, after): def add_events_for_days(request, baseqs, before, after, ebd, timezones): qs = baseqs.filter(is_public=True, live=True, has_subevents=False).filter( Q(Q(date_to__gte=before) & Q(date_from__lte=after)) | - Q(Q(date_from__lte=after) & Q(date_to__gte=before)) | Q(Q(date_to__isnull=True) & Q(date_from__gte=before) & Q(date_from__lte=after)) ).order_by( 'date_from' @@ -471,7 +470,6 @@ def add_events_for_days(request, baseqs, before, after, ebd, timezones): def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_namespace=None, voucher=None): qs = qs.filter(active=True, is_public=True).filter( Q(Q(date_to__gte=before) & Q(date_from__lte=after)) | - Q(Q(date_from__lte=after) & Q(date_to__gte=before)) | Q(Q(date_to__isnull=True) & Q(date_from__gte=before) & Q(date_from__lte=after)) ).order_by( 'date_from' @@ -562,7 +560,7 @@ def sort_ev(e): return e['time'] or time(0, 0, 0), str(e['event'].name) -def days_for_template(ebd, week): +def days_for_template(ebd, week, future_only=False): day_format = get_format('WEEK_DAY_FORMAT') if day_format == 'WEEK_DAY_FORMAT': day_format = 'SHORT_DATE_FORMAT' @@ -574,11 +572,13 @@ def days_for_template(ebd, week): 'events': sorted(ebd.get(day), key=sort_ev) if day in ebd else [] } for day in week.days() + if not future_only or day > now().astimezone(get_current_timezone()).date() ] -def weeks_for_template(ebd, year, month): +def weeks_for_template(ebd, year, month, future_only=False): calendar.setfirstweekday(0) # TODO: Configurable + today = now().astimezone(get_current_timezone()).date() return [ [ { @@ -594,6 +594,9 @@ def weeks_for_template(ebd, year, month): for day in week ] for week in calendar.monthcalendar(year, month) + if not future_only or ( + any(day != 0 and date(year, month, day) > today for day in week) + ) ] diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py index 5ad3d56ce..14235912f 100644 --- a/src/pretix/presale/views/widget.py +++ b/src/pretix/presale/views/widget.py @@ -497,6 +497,10 @@ class WidgetAPIProductList(EventListMixin, View): tz = self.request.organizer.timezone before = datetime(self.year, self.month, 1, 0, 0, 0, tzinfo=tz) - timedelta(days=1) after = datetime(self.year, self.month, ndays, 0, 0, 0, tzinfo=tz) + timedelta(days=1) + if hasattr(self.request, 'event') and self.request.event.settings.event_calendar_future_only: + limit_before = min(after, now().astimezone(tz)) + else: + limit_before = before ebd = defaultdict(list) @@ -507,7 +511,7 @@ class WidgetAPIProductList(EventListMixin, View): event__sales_channels__contains=self.request.sales_channel.identifier ), self.request ), - before, after, ebd, set(), self.request.event, + limit_before, after, ebd, set(), self.request.event, kwargs.get('cart_namespace') ) else: @@ -519,7 +523,7 @@ class WidgetAPIProductList(EventListMixin, View): sales_channels__contains=self.request.sales_channel.identifier ), self.request ), - before, after, ebd, timezones + limit_before, after, ebd, timezones ) add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter( event__organizer=self.request.organizer, @@ -528,7 +532,7 @@ class WidgetAPIProductList(EventListMixin, View): event__sales_channels__contains=self.request.sales_channel.identifier ).prefetch_related( 'event___settings_objects', 'event__organizer___settings_objects' - )), self.request), before, after, ebd, timezones) + )), self.request), limit_before, after, ebd, timezones) data['weeks'] = weeks_for_template(ebd, self.year, self.month) for w in data['weeks']: @@ -553,11 +557,16 @@ class WidgetAPIProductList(EventListMixin, View): week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=tz ) + timedelta(days=1) + if hasattr(self.request, 'event') and self.request.event.settings.event_calendar_future_only: + limit_before = now().astimezone(tz) + else: + limit_before = before + ebd = defaultdict(list) if hasattr(self.request, 'event'): add_subevents_for_days( filter_qs_by_attr(self.request.event.subevents_annotated('web'), self.request), - before, after, ebd, set(), self.request.event, + limit_before, after, ebd, set(), self.request.event, kwargs.get('cart_namespace') ) else: @@ -565,7 +574,7 @@ class WidgetAPIProductList(EventListMixin, View): add_events_for_days( self.request, filter_qs_by_attr(Event.annotated(self.request.organizer.events, 'web'), self.request), - before, after, ebd, timezones + limit_before, after, ebd, timezones ) add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter( event__organizer=self.request.organizer, @@ -573,7 +582,7 @@ class WidgetAPIProductList(EventListMixin, View): event__live=True, ).prefetch_related( 'event___settings_objects', 'event__organizer___settings_objects' - )), self.request), before, after, ebd, timezones) + )), self.request), limit_before, after, ebd, timezones) data['days'] = days_for_template(ebd, week) for d in data['days']: @@ -582,9 +591,8 @@ class WidgetAPIProductList(EventListMixin, View): offset = int(self.request.GET.get("offset", 0)) limit = 50 if hasattr(self.request, 'event'): - evs = self.request.event.subevents_sorted( - filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier), self.request) - ) + evs = filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier), self.request) + evs = self.request.event.subevents_sorted(evs) ordering = self.request.event.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str) data['has_more_events'] = False if ordering in ("date_ascending", "date_descending"):