diff --git a/doc/user/events/widget.rst b/doc/user/events/widget.rst
index d5c0d1c4b8..ee7ce082ee 100644
--- a/doc/user/events/widget.rst
+++ b/doc/user/events/widget.rst
@@ -136,10 +136,15 @@ If you want to include all your public events, you can just reference your organ
-There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
+There is an optional ``style`` parameter that let's you choose between a monthly calendar view, a week view and a list
+view. If you do not set it, the choice will be taken from your organizer settings::
+
+
+If you have more than 100 events, the system might refuse to show a list view and always show a calendar for performance
+reasons instead.
You can see an example here:
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index 0beeb6ef57..512a60d75e 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -886,14 +886,16 @@ DEFAULTS = {
'serializer_kwargs': dict(
choices=(
('list', _('List')),
- ('calendar', _('Calendar'))
+ ('week', _('Week calendar')),
+ ('calendar', _('Month calendar')),
)
),
'form_kwargs': dict(
label=_('Default overview style'),
choices=(
('list', _('List')),
- ('calendar', _('Calendar'))
+ ('week', _('Week calendar')),
+ ('calendar', _('Month calendar')),
)
),
},
diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py
index 879af6091a..4e36bf8a98 100644
--- a/src/pretix/control/forms/organizer.py
+++ b/src/pretix/control/forms/organizer.py
@@ -290,7 +290,8 @@ class OrganizerSettingsForm(SettingsForm):
label=_('Default overview style'),
choices=(
('list', _('List')),
- ('calendar', _('Calendar'))
+ ('week', _('Week calendar')),
+ ('calendar', _('Month calendar')),
)
)
event_list_availability = forms.BooleanField(
diff --git a/src/pretix/helpers/formats/de/__init__.py b/src/pretix/helpers/formats/de/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pretix/helpers/formats/de/formats.py b/src/pretix/helpers/formats/de/formats.py
new file mode 100644
index 0000000000..1e70e3235e
--- /dev/null
+++ b/src/pretix/helpers/formats/de/formats.py
@@ -0,0 +1,3 @@
+# Date according to https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
+WEEK_FORMAT = '\\K\\W W/o'
+WEEK_DAY_FORMAT = 'D, j.n.'
diff --git a/src/pretix/helpers/formats/en/formats.py b/src/pretix/helpers/formats/en/formats.py
index 28f35153b5..3d5fc61a93 100644
--- a/src/pretix/helpers/formats/en/formats.py
+++ b/src/pretix/helpers/formats/en/formats.py
@@ -2,3 +2,5 @@
SHORT_DATE_FORMAT = 'Y-m-d'
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
TIME_FORMAT = 'H:i'
+WEEK_FORMAT = '\\W W, o'
+WEEK_DAY_FORMAT = 'D, M jS'
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
new file mode 100644
index 0000000000..11fe51b344
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html
@@ -0,0 +1,42 @@
+{% load i18n %}
+{% load eventurl %}
+{% load urlreplace %}
+
+{% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html
index d82b2298aa..b63f7816c2 100644
--- a/src/pretix/presale/templates/pretixpresale/event/index.html
+++ b/src/pretix/presale/templates/pretixpresale/event/index.html
@@ -117,6 +117,8 @@
+ {% endif %}
+ {{ date|date:"F Y" }}
+
+ {% include "pretixpresale/fragment_week_calendar.html" with show_avail=request.organizer.settings.event_list_availability %}
+
+ {% if multiple_timezones %}
+
+ {% blocktrans trimmed %}
+ Note that the events in this view are in different timezones.
+ {% endblocktrans %}
+
+ {% endif %}
+{% endblock %}
diff --git a/src/pretix/presale/templates/pretixpresale/organizers/index.html b/src/pretix/presale/templates/pretixpresale/organizers/index.html
index c34dff1f76..b4bbcd842f 100644
--- a/src/pretix/presale/templates/pretixpresale/organizers/index.html
+++ b/src/pretix/presale/templates/pretixpresale/organizers/index.html
@@ -33,10 +33,15 @@
{% trans "List" %}
+
+
+ {% trans "Week" %}
+
- {% trans "Calendar" %}
+ {% trans "Month" %}
100:
+ if self.request.event.settings.event_list_type not in ("calendar", "week"):
+ self.request.event.settings.event_list_type = "calendar"
+ context['list_type'] = "calendar"
if context['list_type'] == "calendar" and self.request.event.has_subevents:
self._set_month_year()
@@ -389,9 +396,44 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
kwargs.get('cart_namespace')
)
+ context['show_names'] = ebd.get('_subevents_different_names', False) or sum(
+ len(i) for i in ebd.values() if isinstance(i, list)
+ ) < 2
context['weeks'] = weeks_for_template(ebd, self.year, self.month)
context['months'] = [date(self.year, i + 1, 1) for i in range(12)]
context['years'] = range(now().year - 2, now().year + 3)
+ elif context['list_type'] == "week" and self.request.event.has_subevents:
+ self._set_week_year()
+ tz = pytz.timezone(self.request.event.settings.timezone)
+ week = isoweek.Week(self.year, self.week)
+ before = datetime(
+ week.monday().year, week.monday().month, week.monday().day, 0, 0, 0, tzinfo=tz
+ ) - timedelta(days=1)
+ after = datetime(
+ week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=tz
+ ) + timedelta(days=1)
+
+ context['date'] = week.monday()
+ context['before'] = before
+ context['after'] = after
+
+ 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,
+ kwargs.get('cart_namespace')
+ )
+
+ context['show_names'] = ebd.get('_subevents_different_names', False) or sum(
+ len(i) for i in ebd.values() if isinstance(i, list)
+ ) < 2
+ context['days'] = days_for_template(ebd, week)
+ context['weeks'] = [date(self.year, i + 1, 1) for i in range(12)]
+ context['weeks'] = [i + 1 for i in range(53)]
+ context['years'] = range(now().year - 2, now().year + 3)
+ context['week_format'] = get_format('WEEK_FORMAT')
+ if context['week_format'] == 'WEEK_FORMAT':
+ context['week_format'] = WEEK_FORMAT
elif self.request.event.has_subevents:
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 f8bf377e66..cfd2c34869 100644
--- a/src/pretix/presale/views/organizer.py
+++ b/src/pretix/presale/views/organizer.py
@@ -2,12 +2,14 @@ import calendar
from collections import defaultdict
from datetime import date, datetime, timedelta
+import isoweek
import pytz
from django.conf import settings
from django.db.models import Exists, Max, Min, OuterRef, Q
from django.db.models.functions import Coalesce, Greatest
from django.http import Http404, HttpResponse
from django.utils.decorators import method_decorator
+from django.utils.formats import date_format, get_format
from django.utils.timezone import now
from django.views import View
from django.views.decorators.cache import cache_page
@@ -20,6 +22,7 @@ from pretix.base.models import (
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.daterange import daterange
+from pretix.helpers.formats.de.formats import WEEK_FORMAT
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_ical
from pretix.presale.views import OrganizerViewMixin
@@ -193,6 +196,72 @@ class EventListMixin:
else:
self._set_month_to_next_event()
+ def _set_week_to_next_subevent(self):
+ tz = pytz.timezone(self.request.event.settings.timezone)
+ next_sev = self.request.event.subevents.using(settings.DATABASE_REPLICA).filter(
+ active=True,
+ is_public=True,
+ date_from__gte=now()
+ ).select_related('event').order_by('date_from').first()
+
+ if next_sev:
+ datetime_from = next_sev.date_from
+ self.year = datetime_from.astimezone(tz).isocalendar()[0]
+ self.week = datetime_from.astimezone(tz).isocalendar()[1]
+ else:
+ self.year = now().isocalendar()[0]
+ self.week = now().isocalendar()[1]
+
+ def _set_week_to_next_event(self):
+ next_ev = filter_qs_by_attr(Event.objects.using(settings.DATABASE_REPLICA).filter(
+ organizer=self.request.organizer,
+ live=True,
+ is_public=True,
+ date_from__gte=now(),
+ has_subevents=False
+ ), self.request).order_by('date_from').first()
+ next_sev = filter_qs_by_attr(SubEvent.objects.using(settings.DATABASE_REPLICA).filter(
+ event__organizer=self.request.organizer,
+ event__is_public=True,
+ event__live=True,
+ active=True,
+ is_public=True,
+ date_from__gte=now()
+ ), self.request).select_related('event').order_by('date_from').first()
+
+ datetime_from = None
+ if (next_ev and next_sev and next_sev.date_from < next_ev.date_from) or (next_sev and not next_ev):
+ datetime_from = next_sev.date_from
+ next_ev = next_sev.event
+ elif next_ev:
+ datetime_from = next_ev.date_from
+
+ if datetime_from:
+ tz = pytz.timezone(next_ev.settings.timezone)
+ self.year = datetime_from.astimezone(tz).isocalendar()[0]
+ self.week = datetime_from.astimezone(tz).isocalendar()[1]
+ else:
+ self.year = now().isocalendar()[0]
+ self.week = now().isocalendar()[1]
+
+ def _set_week_year(self):
+ if hasattr(self.request, 'event') and self.subevent:
+ tz = pytz.timezone(self.request.event.settings.timezone)
+ self.year = self.subevent.date_from.astimezone(tz).year
+ self.month = self.subevent.date_from.astimezone(tz).month
+ if 'year' in self.request.GET and 'week' in self.request.GET:
+ try:
+ self.year = int(self.request.GET.get('year'))
+ self.week = int(self.request.GET.get('week'))
+ except ValueError:
+ self.year = now().isocalendar()[0]
+ self.week = now().isocalendar()[1]
+ else:
+ if hasattr(self.request, 'event'):
+ self._set_week_to_next_subevent()
+ else:
+ self._set_week_to_next_event()
+
class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView):
model = Event
@@ -206,6 +275,10 @@ class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView):
cv = CalendarView()
cv.request = request
return cv.get(request, *args, **kwargs)
+ elif style == "week":
+ cv = WeekCalendarView()
+ cv.request = request
+ return cv.get(request, *args, **kwargs)
else:
return super().get(request, *args, **kwargs)
@@ -281,6 +354,7 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
if not q.cache_is_hot(now() + timedelta(seconds=5))
]
+ name = None
if quotas_to_compute:
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
@@ -297,6 +371,10 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
tz = pytz.timezone(settings.timezone)
datetime_from = se.date_from.astimezone(tz)
date_from = datetime_from.date()
+ if name is None:
+ name = str(se.name)
+ elif str(se.name) != name:
+ ebd['_subevents_different_names'] = True
if se.event.settings.show_date_to and se.date_to:
date_to = se.date_to.astimezone(tz).date()
d = max(date_from, before.date())
@@ -321,6 +399,20 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
})
+def days_for_template(ebd, week):
+ day_format = get_format('WEEK_DAY_FORMAT')
+ if day_format == 'WEEK_DAY_FORMAT':
+ day_format = 'SHORT_DATE_FORMAT'
+ return [
+ {
+ 'day_formatted': date_format(day, day_format),
+ 'date': day,
+ 'events': ebd.get(day)
+ }
+ for day in week.days()
+ ]
+
+
def weeks_for_template(ebd, year, month):
calendar.setfirstweekday(0) # TODO: Configurable
return [
@@ -382,6 +474,56 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
return ebd
+class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
+ template_name = 'pretixpresale/organizers/calendar_week.html'
+
+ def get(self, request, *args, **kwargs):
+ self._set_week_year()
+ return super().get(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data()
+
+ week = isoweek.Week(self.year, self.week)
+ before = datetime(
+ week.monday().year, week.monday().month, week.monday().day, 0, 0, 0, tzinfo=UTC
+ ) - timedelta(days=1)
+ after = datetime(
+ week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=UTC
+ ) + timedelta(days=1)
+
+ ctx['date'] = week.monday()
+ ctx['before'] = before
+ ctx['after'] = after
+
+ ebd = self._events_by_day(before, after)
+
+ ctx['days'] = days_for_template(ebd, week)
+ ctx['weeks'] = [date(self.year, i + 1, 1) for i in range(12)]
+ ctx['weeks'] = [i + 1 for i in range(53)]
+ ctx['years'] = range(now().year - 2, now().year + 3)
+ ctx['week_format'] = get_format('WEEK_FORMAT')
+ if ctx['week_format'] == 'WEEK_FORMAT':
+ ctx['week_format'] = WEEK_FORMAT
+ ctx['multiple_timezones'] = self._multiple_timezones
+
+ return ctx
+
+ def _events_by_day(self, before, after):
+ ebd = defaultdict(list)
+ timezones = set()
+ add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
+ add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
+ event__organizer=self.request.organizer,
+ event__is_public=True,
+ event__live=True,
+ ).prefetch_related(
+ 'event___settings_objects', 'event__organizer___settings_objects'
+ )), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
+ self._multiple_timezones = len(timezones) > 1
+ return ebd
+
+
@method_decorator(cache_page(300), name='dispatch')
class OrganizerIcalDownload(OrganizerViewMixin, View):
def get(self, request, *args, **kwargs):
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index 9dee7c4845..5b73df578e 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -6,6 +6,7 @@ from collections import defaultdict
from datetime import date, datetime, timedelta
from urllib.parse import urljoin
+import isoweek
import pytz
from django.conf import settings
from django.contrib.staticfiles import finders
@@ -43,7 +44,7 @@ from pretix.presale.views.event import (
)
from pretix.presale.views.organizer import (
EventListMixin, add_events_for_days, add_subevents_for_days,
- filter_qs_by_attr, weeks_for_template,
+ days_for_template, filter_qs_by_attr, weeks_for_template,
)
logger = logging.getLogger(__name__)
@@ -360,6 +361,12 @@ class WidgetAPIProductList(EventListMixin, View):
list_type = self.request.GET.get("style", o.settings.event_list_type)
data['list_type'] = list_type
+ if hasattr(self.request, 'event') and data['list_type'] not in ("calendar", "week"):
+ if self.request.event.subevents.count() > 100:
+ if self.request.event.settings.event_list_type not in ("calendar", "week"):
+ self.request.event.settings.event_list_type = "calendar"
+ data['list_type'] = list_type = 'calendar'
+
cache_key = ':'.join([
'widget.py',
'eventlist',
@@ -414,6 +421,48 @@ class WidgetAPIProductList(EventListMixin, View):
if not d:
continue
d['events'] = self._serialize_events(d['events'] or [])
+ elif list_type == "week":
+ self._set_week_year()
+
+ if hasattr(self.request, 'event'):
+ tz = pytz.timezone(self.request.event.settings.timezone)
+ else:
+ tz = pytz.UTC
+
+ week = isoweek.Week(self.year, self.week)
+ data['week'] = [self.year, self.week]
+ before = datetime(
+ week.monday().year, week.monday().month, week.monday().day, 0, 0, 0, tzinfo=tz
+ ) - timedelta(days=1)
+ after = datetime(
+ week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=tz
+ ) + timedelta(days=1)
+
+ 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,
+ kwargs.get('cart_namespace')
+ )
+ else:
+ timezones = set()
+ add_events_for_days(
+ self.request,
+ filter_qs_by_attr(Event.annotated(self.request.organizer.events, 'web'), self.request),
+ before, after, ebd, timezones
+ )
+ add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
+ event__organizer=self.request.organizer,
+ event__is_public=True,
+ event__live=True,
+ ).prefetch_related(
+ 'event___settings_objects', 'event__organizer___settings_objects'
+ )), self.request), before, after, ebd, timezones)
+
+ data['days'] = days_for_template(ebd, week)
+ for d in data['days']:
+ d['events'] = self._serialize_events(d['events'] or [])
else:
if hasattr(self.request, 'event'):
evs = self.request.event.subevents_sorted(
diff --git a/src/pretix/static/pretixpresale/js/widget/widget.js b/src/pretix/static/pretixpresale/js/widget/widget.js
index 8e49250c98..b7667f3ab1 100644
--- a/src/pretix/static/pretixpresale/js/widget/widget.js
+++ b/src/pretix/static/pretixpresale/js/widget/widget.js
@@ -45,6 +45,8 @@ var strings = {
'back': django.pgettext('widget', 'Back'),
'next_month': django.pgettext('widget', 'Next month'),
'previous_month': django.pgettext('widget', 'Previous month'),
+ 'next_week': django.pgettext('widget', 'Next week'),
+ 'previous_week': django.pgettext('widget', 'Previous week'),
'show_seating': django.pgettext('widget', 'Open seat selection'),
'days': {
'MO': django.gettext('Mo'),
@@ -90,6 +92,17 @@ var padNumber = function(number, size) {
return s;
};
+var getISOWeeks = function (y) {
+ var d, isLeap;
+
+ d = new Date(y, 0, 1);
+ isLeap = new Date(y, 1, 29).getMonth() === 1;
+
+ //check for a Jan 1 that's a Thursday or a leap year that has a
+ //Wednesday jan 1. Otherwise it's 52
+ return d.getDay() === 4 || isLeap && d.getDay() === 3 ? 53 : 52
+};
+
/* HTTP API Call helpers */
var api = {
'_getXHR': function () {
@@ -679,7 +692,7 @@ Vue.component('pretix-overlay', {
Vue.component('pretix-widget-event-form', {
template: ('