diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
index a7dd8959ad..c7d87ecb39 100644
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -189,7 +189,9 @@ class EventMixin:
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
- return qs.prefetch_related(
+ return qs.annotate(
+ has_paid_item=Exists(Item.objects.filter(event_id=OuterRef(cls._event_id), default_price__gt=0))
+ ).prefetch_related(
Prefetch(
'quotas',
to_attr='active_quotas',
@@ -280,6 +282,7 @@ class Event(EventMixin, LoggedModel):
"""
settings_namespace = 'event'
+ _event_id = 'pk'
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
testmode = models.BooleanField(default=False)
@@ -786,7 +789,9 @@ class Event(EventMixin, LoggedModel):
'name_ascending': ('name', 'date_from'),
'name_descending': ('-name', 'date_from'),
}[ordering]
- subevs = queryset.filter(
+ subevs = queryset.annotate(
+ has_paid_item=self.cache.get_or_set('has_paid_item', lambda: self.items.filter(default_price__gt=0).exists(), 3600)
+ ).filter(
Q(active=True) & Q(is_public=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24))
@@ -980,6 +985,7 @@ class SubEvent(EventMixin, LoggedModel):
:type location: str
"""
+ _event_id = 'event_id'
event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT)
active = models.BooleanField(default=False, verbose_name=_("Active"),
help_text=_("Only with this checkbox enabled, this date is visible in the "
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html
index 7f553a1ab2..57329b2805 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html
@@ -24,7 +24,11 @@
{% elif subev.best_availability_state == 20 %}
{% trans "Reserved" %}
{% elif subev.best_availability_state < 20 %}
- {% trans "Sold out" %}
+ {% if subev.has_paid_item %}
+ {% trans "Sold out" %}
+ {% else %}
+ {% trans "Fully booked" %}
+ {% endif %}
{% endif %}
{% elif subev.presale_is_running %}
{% trans "Book now" %}
diff --git a/src/pretix/presale/templates/pretixpresale/fragment_calendar.html b/src/pretix/presale/templates/pretixpresale/fragment_calendar.html
index 30d591d1c3..437de17591 100644
--- a/src/pretix/presale/templates/pretixpresale/fragment_calendar.html
+++ b/src/pretix/presale/templates/pretixpresale/fragment_calendar.html
@@ -57,6 +57,11 @@
{% endif %}
{{ event.time|date:"TIME_FORMAT" }}
+ {% if event.time_end %}
+ – {{ event.time_end|date:"TIME_FORMAT" }}
+ {% endif %}
+ {% if event.event.settings.show_date_to and event. %}
+ {% endif %}
{% if not show_names|default_if_none:True %}
{% endif %}
@@ -74,7 +79,11 @@
{% elif event.event.best_availability_state == 20 %}
{% trans "Reserved" %}
{% elif event.event.best_availability_state < 20 %}
- {% trans "Sold out" %}
+ {% if event.event.has_paid_item %}
+ {% trans "Sold out" %}
+ {% else %}
+ {% trans "Fully booked" %}
+ {% endif %}
{% endif %}
{% elif event.event.presale_is_running %}
{% trans "Book now" %}
diff --git a/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html b/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html
index c10a5385b3..732c6e6ec0 100644
--- a/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html
+++ b/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html
@@ -58,7 +58,11 @@
{% elif event.event.best_availability_state == 20 %}
{% trans "Reserved" %}
{% elif event.event.best_availability_state < 20 %}
- {% trans "Sold out" %}
+ {% if event.event.has_paid_item %}
+ {% trans "Sold out" %}
+ {% else %}
+ {% trans "Fully booked" %}
+ {% endif %}
{% endif %}
{% elif event.event.presale_is_running %}
{% trans "Book now" %}
diff --git a/src/pretix/presale/templates/pretixpresale/organizers/index.html b/src/pretix/presale/templates/pretixpresale/organizers/index.html
index 47317a85ee..855e968b9e 100644
--- a/src/pretix/presale/templates/pretixpresale/organizers/index.html
+++ b/src/pretix/presale/templates/pretixpresale/organizers/index.html
@@ -108,7 +108,11 @@
{% elif e.best_availability_state == 20 %}
{% trans "Reserved" %}
{% elif e.best_availability_state < 20 %}
- {% trans "Sold out" %}
+ {% if e.has_paid_item %}
+ {% trans "Sold out" %}
+ {% else %}
+ {% trans "Fully booked" %}
+ {% endif %}
{% endif %}
{% elif e.presale_is_running %}
{% trans "Book now" %}
diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py
index 56ab24dda0..2824f03327 100644
--- a/src/pretix/presale/views/organizer.py
+++ b/src/pretix/presale/views/organizer.py
@@ -17,9 +17,7 @@ from django.views.generic import ListView, TemplateView
from pytz import UTC
from pretix.base.i18n import language
-from pretix.base.models import (
- Event, EventMetaValue, SubEvent, SubEventMetaValue,
-)
+from pretix.base.models import Event, EventMetaValue, SubEvent, SubEventMetaValue
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.daterange import daterange
from pretix.helpers.formats.de.formats import WEEK_FORMAT
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index f9ef96ebe7..48ae51b781 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -317,7 +317,10 @@ class WidgetAPIProductList(EventListMixin, View):
availability['text'] = gettext('Reserved')
elif ev.best_availability_state < Quota.AVAILABILITY_RESERVED:
availability['color'] = 'red'
- availability['text'] = gettext('Sold out')
+ if ev.has_paid_item:
+ availability['text'] = gettext('Sold out')
+ else:
+ availability['text'] = gettext('Fully booked')
elif ev.presale_is_running:
availability['color'] = 'green'
availability['text'] = gettext('Book now')