Event calendar: Allow to show a message if no events are found

This commit is contained in:
Raphael Michel
2026-08-07 14:10:30 +02:00
parent 421a2e37d0
commit 73d03aa037
16 changed files with 107 additions and 4 deletions
+17
View File
@@ -933,6 +933,23 @@ DEFAULTS = {
"is over. You can use it to describe other options to get a ticket, such as a box office.") "is over. You can use it to describe other options to get a ticket, such as a box office.")
) )
}, },
'event_list_empty_text': {
'default': LazyI18nString.from_gettext(
gettext_noop('No dates are currently available for sale that match your criteria.'),
),
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=pgettext_lazy("subevents", "Text for empty date results"),
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=pgettext_lazy("subevents", "This text will be shown if the calendar or list of dates is empty, "
"e.g. because a month does not contain any dates or a filter chosen by the user does "
"find any results. You can use this to advertise ways to get in touch with you to "
"arrange further dates. We do not recommend more than one or two sentences.")
)
},
'payment_explanation': { 'payment_explanation': {
'default': '', 'default': '',
'type': LazyI18nString, 'type': LazyI18nString,
+2
View File
@@ -600,6 +600,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'imprint_url', 'imprint_url',
'checkout_email_helptext', 'checkout_email_helptext',
'presale_has_ended_text', 'presale_has_ended_text',
'event_list_empty_text',
'voucher_explanation_text', 'voucher_explanation_text',
'checkout_success_text', 'checkout_success_text',
'show_dates_on_frontpage', 'show_dates_on_frontpage',
@@ -734,6 +735,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_available_only'] del self.fields['event_list_available_only']
del self.fields['event_list_filters'] del self.fields['event_list_filters']
del self.fields['event_calendar_future_only'] del self.fields['event_calendar_future_only']
del self.fields['event_list_empty_text']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([ self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items() (a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0]) ], key=lambda a: a[0])
@@ -275,6 +275,9 @@
{% bootstrap_field sform.event_calendar_future_only layout="control" %} {% bootstrap_field sform.event_calendar_future_only layout="control" %}
{% endif %} {% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %} {% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
{% if sform.event_list_empty_text %}
{% bootstrap_field sform.event_list_empty_text layout="control" addon_after="%" %}
{% endif %}
<h4>{% trans "Order details" %}</h4> <h4>{% trans "Order details" %}</h4>
@@ -2,6 +2,7 @@
{% load eventurl %} {% load eventurl %}
{% load icon %} {% load icon %}
{% load urlreplace %} {% load urlreplace %}
{% load rich_text %}
<nav aria-label="{% trans "calendar navigation" %}"> <nav aria-label="{% trans "calendar navigation" %}">
<ul class="row calendar-nav"> <ul class="row calendar-nav">
<li class="text-left flip"> <li class="text-left flip">
@@ -50,4 +51,12 @@
</li> </li>
</ul> </ul>
</nav> </nav>
{% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability weeks=subevent_list.weeks show_names=subevent_list.show_names %} <div class="calendar-container">
{% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability weeks=subevent_list.weeks show_names=subevent_list.show_names %}
{% if subevent_list.any_events is False %}
<div class="calendar-empty">
<span class="fa fa-calendar-times-o fa-4x text-muted"></span>
{{ event.settings.event_list_empty_text|rich_text }}
</div>
{% endif %}
</div>
@@ -2,6 +2,7 @@
{% load eventurl %} {% load eventurl %}
{% load icon %} {% load icon %}
{% load urlreplace %} {% load urlreplace %}
{% load rich_text %}
<nav aria-label="{% trans "calendar navigation" %}"> <nav aria-label="{% trans "calendar navigation" %}">
<ul class="row calendar-nav"> <ul class="row calendar-nav">
<li class="text-left flip"> <li class="text-left flip">
@@ -59,7 +60,15 @@
</li> </li>
</ul> </ul>
</nav> </nav>
{% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability days=subevent_list.days show_names=subevent_list.show_names %} <div class="calendar-container">
{% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability days=subevent_list.days show_names=subevent_list.show_names %}
{% if subevent_list.any_events is False %}
<div class="calendar-empty">
<span class="fa fa-calendar-times-o fa-4x text-muted"></span>
{{ event.settings.event_list_empty_text|rich_text }}
</div>
{% endif %}
</div>
<div class="row visible-xs"> <div class="row visible-xs">
<div class="col-xs-6 text-left flip"> <div class="col-xs-6 text-left flip">
<a href="?{% url_replace request "date" subevent_list.before|date:"o-\WW" %}" <a href="?{% url_replace request "date" subevent_list.before|date:"o-\WW" %}"
@@ -2,6 +2,7 @@
{% load icon %} {% load icon %}
{% load eventurl %} {% load eventurl %}
{% load date_fast %} {% load date_fast %}
{% load rich_text %}
<div class="event-list full-width-list alternating-rows"> <div class="event-list full-width-list alternating-rows">
{% for subev in subevent_list.subevent_list %} {% for subev in subevent_list.subevent_list %}
@@ -43,5 +44,10 @@
</a> </a>
</p> </p>
</article> </article>
{% empty %}
<div class="calendar-empty">
<span class="fa fa-calendar-times-o fa-4x text-muted"></span>
{{ event.settings.event_list_empty_text|rich_text }}
</div>
{% endfor %} {% endfor %}
</div> </div>
+2
View File
@@ -356,6 +356,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
after, after,
future_only=self.request.event.settings.event_calendar_future_only future_only=self.request.event.settings.event_calendar_future_only
) )
context['any_events'] = any(ebd)
elif context['list_type'] == "week": elif context['list_type'] == "week":
self._set_week_year() self._set_week_year()
tz = self.request.event.timezone tz = self.request.event.timezone
@@ -430,6 +431,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
after, after,
future_only=self.request.event.settings.event_calendar_future_only future_only=self.request.event.settings.event_calendar_future_only
) )
context['any_events'] = any(ebd)
else: else:
subevents = self.request.event.subevents_sorted( subevents = self.request.event.subevents_sorted(
filter_qs_by_attr( filter_qs_by_attr(
+6
View File
@@ -642,6 +642,8 @@ class WidgetAPIProductList(EventListMixin, View):
sales_channel=self.request.sales_channel, sales_channel=self.request.sales_channel,
) )
if not any(ebd):
data['empty_text'] = str(rich_text(request.event.settings.event_list_empty_text, safelinks=False))
data['weeks'] = weeks_for_template(ebd, self.year, self.month) data['weeks'] = weeks_for_template(ebd, self.year, self.month)
for w in data['weeks']: for w in data['weeks']:
for d in w: for d in w:
@@ -700,6 +702,8 @@ class WidgetAPIProductList(EventListMixin, View):
sales_channel=self.request.sales_channel, sales_channel=self.request.sales_channel,
) )
if not any(ebd):
data['empty_text'] = str(rich_text(request.event.settings.event_list_empty_text, safelinks=False))
data['days'] = days_for_template(ebd, week) data['days'] = days_for_template(ebd, week)
for d in data['days']: for d in data['days']:
d['events'] = self._serialize_events(d['events'] or []) d['events'] = self._serialize_events(d['events'] or [])
@@ -770,6 +774,8 @@ class WidgetAPIProductList(EventListMixin, View):
'event_url': eventreverse_absolute(event, 'presale:event.index'), 'event_url': eventreverse_absolute(event, 'presale:event.index'),
}) })
if not data['events']:
data['empty_text'] = str(rich_text(request.event.settings.event_list_empty_text, safelinks=False))
cache.set(cache_key, data, 30) cache.set(cache_key, data, 30)
# These pages are cached for a really short duration this should make them pretty accurate, while still # These pages are cached for a really short duration this should make them pretty accurate, while still
# providing some protection against burst traffic. # providing some protection against burst traffic.
@@ -1,3 +1,35 @@
.calendar-container:has(.calendar-empty), .event-list:has(.calendar-empty) {
position: relative;
overflow: hidden;
.table-responsive, .week-calendar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 0;
}
.calendar-empty {
position: relative;
z-index: 100;
background: rgba(255, 255, 255, 0.9);
text-align: center;
font-size: $font-size-large;
padding: 30px 0;
.fa {
margin-block: 12px;
}
*:last-child {
margin-bottom: 0;
}
}
}
.table-calendar, .week-calendar, .day-calendar { .table-calendar, .week-calendar, .day-calendar {
td, th { td, th {
width: 14.29%; width: 14.29%;
@@ -513,7 +513,7 @@ h2 .label {
} }
.event-list.full-width-list { .event-list.full-width-list {
h3, h4, p { h3, h4, article p {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
@@ -748,6 +748,13 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
display: block; display: block;
width: 100%; width: 100%;
} }
.pretix-widget-event-list-empty,
.pretix-widget-event-week-empty,
.pretix-widget-event-calendar-empty {
padding: 10px 0;
text-align: center;
}
} }
@@ -38,6 +38,7 @@ export interface ProductListResponse {
events?: EventEntry[] events?: EventEntry[]
has_more_events?: boolean has_more_events?: boolean
meta_filter_fields?: MetaFilterField[] meta_filter_fields?: MetaFilterField[]
empty_text?: string
weeks?: DayEntry[][] weeks?: DayEntry[][]
date?: string date?: string
days?: DayEntry[] days?: DayEntry[]
@@ -92,7 +92,9 @@ function nextmonth () {
| {{ STRINGS.next_month }} &raquo; | {{ STRINGS.next_month }} &raquo;
//- Calendar //- Calendar
.pretix-widget-event-calendar-empty(v-if="!!store.emptyText", v-html="store.emptyText")
table.pretix-widget-event-calendar-table( table.pretix-widget-event-calendar-table(
v-else
:id="id", :id="id",
tabindex="0", tabindex="0",
:aria-labelledby="ariaLabelledby" :aria-labelledby="ariaLabelledby"
@@ -62,6 +62,8 @@ console.log(store)
:event="event" :event="event"
) )
.pretix-widget-event-list-empty(v-if="!store.events.length && store.emptyText", v-html="store.emptyText")
p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents") p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents")
button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }} button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }}
</template> </template>
@@ -88,7 +88,8 @@ function nextweek () {
| {{ STRINGS.next_week }} &raquo; | {{ STRINGS.next_week }} &raquo;
//- Actual calendar //- Actual calendar
.pretix-widget-event-week-table(:id="id", tabindex="0", :aria-label="weekname") .pretix-widget-event-week-empty(v-if="store.emptyText", v-html="store.emptyText")
.pretix-widget-event-week-table(v-else, :id="id", tabindex="0", :aria-label="weekname")
.pretix-widget-event-week-col(v-for="d in store.days", :key="d?.date || ''") .pretix-widget-event-week-col(v-for="d in store.days", :key="d?.date || ''")
EventWeekCell(:day="d", :mobile="mobile") EventWeekCell(:day="d", :mobile="mobile")
</template> </template>
@@ -95,6 +95,7 @@ export function createWidgetStore (config: {
offset: 0, offset: 0,
appendEvents: false, appendEvents: false,
metaFilterFields: [] as MetaFilterField[], metaFilterFields: [] as MetaFilterField[],
emptyText: null as string | null,
// UI state // UI state
mobile: false, mobile: false,
@@ -270,6 +271,7 @@ export function createWidgetStore (config: {
this.name = data.name ?? null this.name = data.name ?? null
this.frontpageText = data.frontpage_text ?? null this.frontpageText = data.frontpage_text ?? null
this.metaFilterFields = data.meta_filter_fields ?? [] this.metaFilterFields = data.meta_filter_fields ?? []
this.emptyText = data.empty_text ?? null
} else if (data.days !== undefined) { } else if (data.days !== undefined) {
this.days = data.days this.days = data.days
this.date = null this.date = null
@@ -279,6 +281,7 @@ export function createWidgetStore (config: {
this.name = data.name ?? null this.name = data.name ?? null
this.frontpageText = data.frontpage_text ?? null this.frontpageText = data.frontpage_text ?? null
this.metaFilterFields = data.meta_filter_fields ?? [] this.metaFilterFields = data.meta_filter_fields ?? []
this.emptyText = data.empty_text ?? null
} else if (data.events !== undefined) { } else if (data.events !== undefined) {
this.events = this.appendEvents && this.events this.events = this.appendEvents && this.events
? this.events.concat(data.events) ? this.events.concat(data.events)
@@ -290,6 +293,7 @@ export function createWidgetStore (config: {
this.frontpageText = data.frontpage_text ?? null this.frontpageText = data.frontpage_text ?? null
this.hasMoreEvents = data.has_more_events ?? false this.hasMoreEvents = data.has_more_events ?? false
this.metaFilterFields = data.meta_filter_fields ?? [] this.metaFilterFields = data.meta_filter_fields ?? []
this.emptyText = data.empty_text ?? null
} else { } else {
this.view = 'event' this.view = 'event'
this.targetUrl = data.target_url ?? this.targetUrl this.targetUrl = data.target_url ?? this.targetUrl