diff --git a/doc/user/events/widget.rst b/doc/user/events/widget.rst index fcbdd7770..b58bb6f41 100644 --- a/doc/user/events/widget.rst +++ b/doc/user/events/widget.rst @@ -114,6 +114,35 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v +Multi-event selection +--------------------- + +If you want to embed multiple events in a single widget, you can do so. If it's multiple dates of an event series, just leave off the ``series`` attribute:: + + + +If you want to include all your public events, you can just reference your organizer:: + + + +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:: + + + + +You can see an example here: + +.. raw:: html + + + + pretix Button ------------- diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 2f58b9e91..7f9266438 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -1,7 +1,7 @@ import string import uuid from collections import OrderedDict -from datetime import datetime, time +from datetime import datetime, time, timedelta from operator import attrgetter import pytz @@ -668,8 +668,8 @@ class Event(EventMixin, LoggedModel): }[ordering] subevs = queryset.filter( Q(active=True) & ( - Q(Q(date_to__isnull=True) & Q(date_from__gte=now())) - | Q(date_to__gte=now()) + Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24))) + | Q(date_to__gte=now() - timedelta(hours=24)) ) ) # order_by doesn't make sense with I18nField for f in reversed(orderfields): diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index a8632593a..4ad226462 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1192,7 +1192,7 @@ class TaxRuleForm(I18nModelForm): class WidgetCodeForm(forms.Form): subevent = forms.ModelChoiceField( label=pgettext_lazy('subevent', "Date"), - required=True, + required=False, queryset=SubEvent.objects.none() ) language = forms.ChoiceField( diff --git a/src/pretix/presale/style.py b/src/pretix/presale/style.py index 8f3e8006f..6a98da43a 100644 --- a/src/pretix/presale/style.py +++ b/src/pretix/presale/style.py @@ -105,7 +105,7 @@ def regenerate_organizer_css(organizer_id: int): organizer.settings.set('presale_css_checksum', checksum) # widget.scss - css, checksum = compile_scss(organizer) + css, checksum = compile_scss(organizer, file='widget.scss', fonts=False) fname = 'pub/{}/widget.{}.css'.format(organizer.slug, checksum[:16]) if organizer.settings.get('presale_widget_css_checksum', '') != checksum: newname = default_storage.save(fname, ContentFile(css.encode('utf-8'))) diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py index 5b1e9a26d..dca48ba1c 100644 --- a/src/pretix/presale/urls.py +++ b/src/pretix/presale/urls.py @@ -107,6 +107,9 @@ organizer_patterns = [ url(r'^events/ical/$', pretix.presale.views.organizer.OrganizerIcalDownload.as_view(), name='organizer.ical'), + url(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(), + name='organizer.widget.productlist'), + url(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='organizer.widget.css'), ] locale_patterns = [ diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 7d2f95216..f0fbca775 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -22,7 +22,7 @@ from pretix.base.models.event import SubEvent from pretix.multidomain.urlreverse import eventreverse from pretix.presale.ical import get_ical from pretix.presale.views.organizer import ( - add_subevents_for_days, weeks_for_template, + EventListMixin, add_subevents_for_days, weeks_for_template, ) from . import ( @@ -157,7 +157,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'): @method_decorator(allow_frame_if_namespaced, 'dispatch') @method_decorator(iframe_entry_view_wrapper, 'dispatch') -class EventIndex(EventViewMixin, CartMixin, TemplateView): +class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): template_name = "pretixpresale/event/index.html" def get(self, request, *args, **kwargs): @@ -204,32 +204,6 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView): else: return super().get(request, *args, **kwargs) - def _set_month_year(self): - tz = pytz.timezone(self.request.event.settings.timezone) - if self.subevent: - self.year = self.subevent.date_from.astimezone(tz).year - self.month = self.subevent.date_from.astimezone(tz).month - elif 'year' in self.request.GET and 'month' in self.request.GET: - try: - self.year = int(self.request.GET.get('year')) - self.month = int(self.request.GET.get('month')) - except ValueError: - self.year = now().year - self.month = now().month - else: - next_sev = self.request.event.subevents.filter( - active=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).year - self.month = datetime_from.astimezone(tz).month - else: - self.year = now().year - self.month = now().month - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if not self.request.event.has_subevents or self.subevent: diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py index 0129fb88f..ccf495165 100644 --- a/src/pretix/presale/views/organizer.py +++ b/src/pretix/presale/views/organizer.py @@ -83,22 +83,9 @@ def filter_qs_by_attr(qs, request): return qs -class OrganizerIndex(OrganizerViewMixin, ListView): - model = Event - context_object_name = 'events' - template_name = 'pretixpresale/organizers/index.html' - paginate_by = 30 +class EventListMixin: - def get(self, request, *args, **kwargs): - style = request.GET.get("style", request.organizer.settings.event_list_type) - if style == "calendar": - cv = CalendarView() - cv.request = request - return cv.get(request, *args, **kwargs) - else: - return super().get(request, *args, **kwargs) - - def get_queryset(self): + def _get_event_queryset(self): query = Q(is_public=True) & Q(live=True) qs = self.request.organizer.events.filter(query) qs = qs.annotate( @@ -131,6 +118,89 @@ class OrganizerIndex(OrganizerViewMixin, ListView): qs = Event.annotated(filter_qs_by_attr(qs, self.request)) return qs + def _set_month_to_next_subevent(self): + tz = pytz.timezone(self.request.event.settings.timezone) + next_sev = self.request.event.subevents.filter( + active=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).year + self.month = datetime_from.astimezone(tz).month + else: + self.year = now().year + self.month = now().month + + def _set_month_to_next_event(self): + next_ev = filter_qs_by_attr(Event.objects.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.filter( + event__organizer=self.request.organizer, + event__is_public=True, + event__live=True, + active=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).year + self.month = datetime_from.astimezone(tz).month + else: + self.year = now().year + self.month = now().month + + def _set_month_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 'month' in self.request.GET: + try: + self.year = int(self.request.GET.get('year')) + self.month = int(self.request.GET.get('month')) + except ValueError: + self.year = now().year + self.month = now().month + else: + if hasattr(self.request, 'event'): + self._set_month_to_next_subevent() + else: + self._set_month_to_next_event() + + +class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView): + model = Event + context_object_name = 'events' + template_name = 'pretixpresale/organizers/index.html' + paginate_by = 30 + + def get(self, request, *args, **kwargs): + style = request.GET.get("style", request.organizer.settings.event_list_type) + if style == "calendar": + cv = CalendarView() + cv.request = request + return cv.get(request, *args, **kwargs) + else: + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return self._get_event_queryset() + def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) for event in ctx['events']: @@ -243,50 +313,11 @@ def weeks_for_template(ebd, year, month): ] -class CalendarView(OrganizerViewMixin, TemplateView): +class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView): template_name = 'pretixpresale/organizers/calendar.html' def get(self, request, *args, **kwargs): - if 'year' in kwargs and 'month' in kwargs: - self.year = int(kwargs.get('year')) - self.month = int(kwargs.get('month')) - elif 'year' in request.GET and 'month' in request.GET: - try: - self.year = int(request.GET.get('year')) - self.month = int(request.GET.get('month')) - except ValueError: - self.year = now().year - self.month = now().month - else: - next_ev = filter_qs_by_attr(Event.objects.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.filter( - event__organizer=self.request.organizer, - event__is_public=True, - event__live=True, - active=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).year - self.month = datetime_from.astimezone(tz).month - else: - self.year = now().year - self.month = now().month + self._set_month_year() return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py index 3f9289ad5..65491b135 100644 --- a/src/pretix/presale/views/widget.py +++ b/src/pretix/presale/views/widget.py @@ -1,8 +1,12 @@ +import calendar import hashlib import json import logging +from collections import defaultdict +from datetime import date, datetime, timedelta from urllib.parse import urljoin +import pytz from django.conf import settings from django.contrib.staticfiles import finders from django.core.files.base import ContentFile @@ -24,16 +28,21 @@ from django.views.i18n import ( from lxml import etree from pretix.base.i18n import language -from pretix.base.models import CartPosition, Voucher +from pretix.base.models import CartPosition, Event, Quota, SubEvent, Voucher from pretix.base.services.cart import error_messages from pretix.base.settings import GlobalSettingsObject from pretix.base.templatetags.rich_text import rich_text +from pretix.helpers.daterange import daterange from pretix.helpers.thumb import get_thumbnail from pretix.multidomain.urlreverse import build_absolute_uri from pretix.presale.views.cart import get_or_create_cart_id from pretix.presale.views.event import ( get_grouped_items, item_group_by_category, ) +from pretix.presale.views.organizer import ( + EventListMixin, add_events_for_days, add_subevents_for_days, + filter_qs_by_attr, weeks_for_template, +) logger = logging.getLogger(__name__) @@ -43,7 +52,8 @@ def indent(s): def widget_css_etag(request, **kwargs): - return request.event.settings.presale_widget_css_checksum or request.organizer.settings.presale_widget_css_checksum + o = getattr(request, 'event', request.organizer) + return o.settings.presale_widget_css_checksum or o.settings.presale_widget_css_checksum def widget_js_etag(request, lang, **kwargs): @@ -54,8 +64,9 @@ def widget_js_etag(request, lang, **kwargs): @condition(etag_func=widget_css_etag) @cache_page(60) def widget_css(request, **kwargs): - if request.event.settings.presale_widget_css_file: - resp = FileResponse(default_storage.open(request.event.settings.presale_widget_css_file), + o = getattr(request, 'event', request.organizer) + if o.settings.presale_widget_css_file: + resp = FileResponse(default_storage.open(o.settings.presale_widget_css_file), content_type='text/css') return resp else: @@ -151,7 +162,7 @@ def get_picture(event, picture): return urljoin(build_absolute_uri(event, 'presale:event.index'), get_thumbnail(picture.name, '60x60^').thumb.url) -class WidgetAPIProductList(View): +class WidgetAPIProductList(EventListMixin, View): def _get_items(self): items, display_add_to_cart = get_grouped_items( @@ -201,33 +212,179 @@ class WidgetAPIProductList(View): }) return grps, display_add_to_cart, len(items) - def dispatch(self, request, *args, **kwargs): + def response(self, data): + resp = JsonResponse(data) + resp['Access-Control-Allow-Origin'] = '*' + return resp + + def get(self, request, *args, **kwargs): + if not hasattr(request, 'event'): + return self._get_event_list(request, **kwargs) + if not request.event.live: - resp = JsonResponse({ + return self.response({ 'error': ugettext('This ticket shop is currently disabled.') }) - resp['Access-Control-Allow-Origin'] = '*' - return resp self.subevent = None if request.event.has_subevents: if 'subevent' in kwargs: self.subevent = request.event.subevents.filter(pk=kwargs['subevent'], active=True).first() if not self.subevent: - raise Http404() + return self.response({ + 'error': ugettext('The selected date does not exist in this event series.') + }) else: - raise Http404() + return self._get_event_list(request, **kwargs) else: if 'subevent' in kwargs: - raise Http404() + return self.response({ + 'error': ugettext('This is not an event series.') + }) + return self._get_event_view(request, **kwargs) + def dispatch(self, request, *args, **kwargs): if 'lang' in request.GET and request.GET.get('lang') in [lc for lc, ll in settings.LANGUAGES]: with language(request.GET.get('lang')): - return super().dispatch(request, *args, **kwargs) + return self.get(request, **kwargs) else: - return super().dispatch(request, *args, **kwargs) + return self.get(request, **kwargs) - def get(self, request, **kwargs): + def _get_availability(self, ev, event): + availability = {} + if ev.presale_is_running and event.settings.event_list_availability and ev.best_availability_state is not None: + if ev.best_availability_state == Quota.AVAILABILITY_OK: + availability['color'] = 'green' + availability['text'] = ugettext('Tickets on sale') + elif event.settings.waiting_list_enabled and ev.best_availability_state >= 0: + availability['color'] = 'orange' + availability['text'] = ugettext('Waiting list') + elif ev.best_availability_state == Quota.AVAILABILITY_RESERVED: + availability['color'] = 'orange' + availability['text'] = ugettext('Reserved') + elif ev.best_availability_state < Quota.AVAILABILITY_RESERVED: + availability['color'] = 'red' + availability['text'] = ugettext('Sold out') + elif ev.presale_is_running: + availability['color'] = 'green' + availability['text'] = ugettext('Tickets on sale') + elif ev.presale_has_ended: + availability['color'] = 'red' + availability['text'] = ugettext('Sale over') + elif event.settings.presale_start_show_date and ev.presale_start: + availability['color'] = 'orange' + availability['text'] = ugettext('from %(start_date)s') % date_format(ev.presale_start, "SHORT_DATE_FORMAT") + else: + availability['color'] = 'orange' + availability['text'] = ugettext('Sale Soon') + return availability + + def _serialize_events(self, ebd): + events = [] + for e in ebd: + ev = e['event'] + if isinstance(ev, SubEvent): + event = ev.event + else: + event = ev + events.append({ + 'name': str(ev.name), + 'time': date_format(e['time'], 'TIME_FORMAT') if e.get('time') and event.settings.show_times else None, + 'continued': e['continued'], + 'date_range': ev.get_date_range_display() + ( + " " + date_format(ev.date_from, "TIME_FORMAT") if event.settings.show_times else "" + ), + 'availability': self._get_availability(ev, event), + 'event_url': build_absolute_uri(event, 'presale:event.index'), + 'subevent': ev.pk if isinstance(ev, SubEvent) else None, + }) + return events + + def _get_event_list(self, request, **kwargs): + data = {} + o = getattr(request, 'event', request.organizer) + list_type = self.request.GET.get("style", o.settings.event_list_type) + data['list_type'] = list_type + + if list_type == "calendar": + self._set_month_year() + _, ndays = calendar.monthrange(self.year, self.month) + + data['date'] = date(self.year, self.month, 1) + if hasattr(self.request, 'event'): + tz = pytz.timezone(self.request.event.settings.timezone) + else: + tz = pytz.UTC + 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) + + ebd = defaultdict(list) + + if hasattr(self.request, 'event'): + add_subevents_for_days( + self.request.event.subevents_annotated('web'), + before, after, ebd, set(), self.request.event, + kwargs.get('cart_namespace') + ) + else: + timezones = set() + add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web'), 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['weeks'] = weeks_for_template(ebd, self.year, self.month) + for w in data['weeks']: + for d in w: + if not d: + continue + d['events'] = self._serialize_events(d['events'] or []) + else: + if hasattr(self.request, 'event'): + evs = self.request.event.subevents_sorted( + self.request.event.subevents_annotated(self.request.sales_channel) + ) + data['events'] = [ + { + 'name': str(ev.name), + 'date_range': ev.get_date_range_display() + ( + " " + date_format(ev.date_from, "TIME_FORMAT") if ev.event.settings.show_times else "" + ), + 'availability': self._get_availability(ev, ev.event), + 'event_url': build_absolute_uri(ev.event, 'presale:event.index'), + 'subevent': ev.pk, + } for ev in evs + ] + else: + data['events'] = [] + qs = self._get_event_queryset() + for event in qs: + tz = pytz.timezone(event.cache.get_or_set('timezone', lambda: event.settings.timezone)) + if event.has_subevents: + dr = daterange( + event.min_from.astimezone(tz), + (event.max_fromto or event.max_to or event.max_from).astimezone(tz) + ) + avail = {'color': 'none', 'text': ugettext('Event series')} + else: + dr = event.get_date_range_display() + ( + " " + date_format(event.date_from, "TIME_FORMAT") if event.settings.show_times else "" + ) + avail = self._get_availability(event, event) + data['events'].append({ + 'name': str(event.name), + 'date_range': dr, + 'availability': avail, + 'event_url': build_absolute_uri(event, 'presale:event.index'), + }) + + return self.response(data) + + def _get_event_view(self, request, **kwargs): data = { 'currency': request.event.currency, 'display_net_prices': request.event.settings.display_net_prices, @@ -241,6 +398,7 @@ class WidgetAPIProductList(View): data['cart_exists'] = True ev = self.subevent or request.event + data['name'] = str(ev.name) fail = False if not ev.presale_is_running: @@ -298,6 +456,4 @@ class WidgetAPIProductList(View): self.request.event.get_cache().set('vouchers_exist', vouchers_exist) data['vouchers_exist'] = vouchers_exist - resp = JsonResponse(data) - resp['Access-Control-Allow-Origin'] = '*' - return resp + return self.response(data) diff --git a/src/pretix/static/pretixpresale/js/widget/widget.js b/src/pretix/static/pretixpresale/js/widget/widget.js index 203e3727d..77f2b0c79 100644 --- a/src/pretix/static/pretixpresale/js/widget/widget.js +++ b/src/pretix/static/pretixpresale/js/widget/widget.js @@ -36,6 +36,33 @@ var strings = { 'close': django.pgettext('widget', 'Close'), 'continue': django.pgettext('widget', 'Continue'), 'variations': django.pgettext('widget', 'See variations'), + 'back_to_list': django.pgettext('widget', 'Choose a different event'), + 'back': django.pgettext('widget', 'Back'), + 'next_month': django.pgettext('widget', 'Next month'), + 'previous_month': django.pgettext('widget', 'Previous month'), + 'days': { + 'MO': django.gettext('Mo'), + 'TU': django.gettext('Tu'), + 'WE': django.gettext('We'), + 'TH': django.gettext('Th'), + 'FR': django.gettext('Fr'), + 'SA': django.gettext('Sa'), + 'SU': django.gettext('Su'), + }, + 'months': { + '01': django.gettext('January'), + '02': django.gettext('February'), + '03': django.gettext('March'), + '04': django.gettext('April'), + '05': django.gettext('May'), + '06': django.gettext('June'), + '07': django.gettext('July'), + '08': django.gettext('August'), + '09': django.gettext('September'), + '10': django.gettext('October'), + '11': django.gettext('November'), + '12': django.gettext('December'), + } }; var setCookie = function (cname, cvalue, exdays) { @@ -51,6 +78,12 @@ var getCookie = function (name) { else return null; }; +var padNumber = function(number, size) { + var s = String(number); + while (s.length < (size || 2)) {s = "0" + s;} + return s; +}; + /* HTTP API Call helpers */ var api = { '_getXHR': function () { @@ -181,9 +214,9 @@ Vue.component('availbox', { }, waiting_list_url: function () { if (this.item.has_variations) { - return this.$root.event_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id; + return this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id; } else { - return this.$root.event_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id; + return this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id; } } } @@ -405,13 +438,17 @@ var shared_methods = { this.$root.overlay.frame_loading = true; this.async_task_interval = 100; - api._postFormJSON(url, this.$refs.form, this.buy_callback, this.buy_error_callback); + var form = this.$refs.form; + if (form === undefined) { + form = this.$refs.formcomp.$refs.form; + } + api._postFormJSON(url, form, this.buy_callback, this.buy_error_callback); } }, buy_error_callback: function (xhr, data) { if (xhr.status === 405 && typeof xhr.responseURL !== "undefined") { // Likely a redirect! - this.$root.event_url = xhr.responseURL.substr(0, xhr.responseURL.indexOf("/cart/add") - 18); + this.$root.target_url = xhr.responseURL.substr(0, xhr.responseURL.indexOf("/cart/add") - 18); this.$root.overlay.frame_loading = false; this.buy(); return; @@ -435,7 +472,7 @@ var shared_methods = { setCookie(this.$root.cookieName, data.cart_id, 30); } if (data.redirect.substr(0, 1) === '/') { - data.redirect = this.$root.event_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect; + data.redirect = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect; } var url = data.redirect; if (url.indexOf('?')) { @@ -456,7 +493,7 @@ var shared_methods = { } else { this.async_task_id = data.async_id; if (data.check_url) { - this.async_task_check_url = this.$root.event_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.check_url; + this.async_task_check_url = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.check_url; } this.async_task_timeout = window.setTimeout(this.buy_check, this.async_task_interval); this.async_task_interval = 250; @@ -480,7 +517,7 @@ var shared_methods = { iframe.src = redirect_url; }, resume: function () { - var redirect_url = this.$root.event_url + 'w/' + widget_id + '/?iframe=1&locale=' + lang; + var redirect_url = this.$root.target_url + 'w/' + widget_id + '/?iframe=1&locale=' + lang; if (this.$root.cart_id) { redirect_url += '&take_cart_id=' + this.$root.cart_id; } @@ -504,6 +541,7 @@ var shared_widget_data = function () { async_task_timeout: null, async_task_interval: 100, voucher: null, + mobile: false, } }; @@ -586,10 +624,16 @@ Vue.component('pretix-overlay', { } }); -Vue.component('pretix-widget', { - template: ('
' - + '
' - + shared_loading_fragment +Vue.component('pretix-widget-event-form', { + template: ('
' + + '' + + '
' + + '{{ $root.name }}' + + '
' + '
' + '' + '' @@ -597,7 +641,7 @@ Vue.component('pretix-widget', { + '
{{ $root.error }}
' + '
' - + '' + strings['cart_exists'] @@ -605,7 +649,7 @@ Vue.component('pretix-widget', { + '
' + '' + '
' - + '' + + '' + '
' + '
' + '
' + '

'+ strings['redeem_voucher'] +'

' + '
' - + '' + + '' + '
' + '' + '' + '
' - + '' + + '' + '
' + '
' + '' + + '
' + ), + methods: { + back_to_list: function() { + this.$root.target_url = this.$root.parent_stack.pop(); + this.$root.error = null; + this.$root.subevent = null; + if (this.$root.events !== undefined) { + this.$root.view = "events"; + } else { + this.$root.view = "weeks"; + } + } + } +}); + +Vue.component('pretix-widget-event-list-entry', { + template: ('' + + '
{{ event.name }}
' + + '' + + '
{{ event.availability.text }}
' + + '
'), + props: { + event: Object + }, + computed: { + classObject: function () { + var o = { + 'pretix-widget-event-list-entry': true + }; + o['pretix-widget-event-availability-' + this.event.availability.color] = true; + return o + } + }, + methods: { + select: function () { + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = this.event.event_url; + this.$root.error = null; + this.$root.subevent = this.event.subevent; + this.$root.loading++; + this.$root.reload(); + } + } +}); + +Vue.component('pretix-widget-event-list', { + template: ('
' + + '' + + '' + + '
'), + methods: { + back_to_calendar: function () { + if (this.$root.weeks) { + this.$root.events = undefined; + this.$root.view = "weeks"; + } else { + this.$root.loading++; + this.$root.target_url = this.$root.parent_stack.pop(); + this.$root.error = null; + this.$root.reload(); + } + }, + } +}); + +Vue.component('pretix-widget-event-calendar-event', { + template: ('' + + '' + + '{{ event.name }}' + + '' + + '
{{ event.time }}
' + + '
{{ event.availability.text }}
' + + '
'), + props: { + event: Object + }, + computed: { + classObject: function () { + var o = { + 'pretix-widget-event-calendar-event': true + }; + o['pretix-widget-event-availability-' + this.event.availability.color] = true; + return o + } + }, + methods: { + select: function () { + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = this.event.event_url; + this.$root.error = null; + this.$root.subevent = this.event.subevent; + this.$root.loading++; + this.$root.reload(); + } + } +}); + +Vue.component('pretix-widget-event-calendar-cell', { + template: ('' + + '
' + + '{{ daynum }}' + + '
' + + '
' + + '' + + '
' + + ''), + props: { + day: Object + }, + methods: { + selectDay: function () { + if (!this.day || !this.day.events.length || !this.$parent.$parent.$parent.mobile) { + return; + } + if (this.day.events.length === 1) { + var ev = this.day.events[0]; + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = ev.event_url; + this.$root.error = null; + this.$root.subevent = ev.subevent; + this.$root.loading++; + this.$root.reload(); + } else { + this.$root.events = this.day.events; + this.$root.view = "events"; + } + } + }, + computed: { + daynum: function () { + if (!this.day) { + return; + } + return this.day.date.substr(8); + }, + classObject: function () { + var o = {}; + if (this.day && this.day.events.length > 0) { + o['pretix-widget-has-events'] = true; + var best = 'red'; + for (var i = 0; i < this.day.events.length; i++) { + var ev = this.day.events[i]; + if (ev.availability.color === 'green') { + best = 'green'; + } else if (ev.availability.color === 'orange' || best !== 'green') { + best = 'orange' + } + } + o['pretix-widget-day-availability-' + best] = true; + } + return o + } + } +}); + +Vue.component('pretix-widget-event-calendar-row', { + template: ('' + + '' + + ''), + props: { + week: Array + }, +}); + +Vue.component('pretix-widget-event-calendar', { + template: ('
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + strings['days']['MO'] + '' + strings['days']['TU'] + '' + strings['days']['WE'] + '' + strings['days']['TH'] + '' + strings['days']['FR'] + '' + strings['days']['SA'] + '' + strings['days']['SU'] + '
' + + '
'), + computed: { + monthname: function () { + return strings['months'][this.$root.date.substr(5, 2)] + ' ' + this.$root.date.substr(0, 4); + } + }, + methods: { + back_to_list: function () { + this.$root.weeks = undefined; + this.$root.view = "events"; + }, + prevmonth: function () { + var curMonth = parseInt(this.$root.date.substr(5, 2)); + var curYear = parseInt(this.$root.date.substr(0, 4)); + curMonth--; + if (curMonth < 1) { + curMonth = 12; + curYear--; + } + this.$root.date = String(curYear) + "-" + padNumber(curMonth, 2) + "-01"; + this.$root.loading++; + this.$root.reload(); + }, + nextmonth: function () { + var curMonth = parseInt(this.$root.date.substr(5, 2)); + var curYear = parseInt(this.$root.date.substr(0, 4)); + curMonth++; + if (curMonth > 12) { + curMonth = 1; + curYear++; + } + this.$root.date = String(curYear) + "-" + padNumber(curMonth, 2) + "-01"; + this.$root.loading++; + this.$root.reload(); + } + }, +}); + +Vue.component('pretix-widget', { + template: ('
' + + '
' + + shared_loading_fragment + + '
{{ $root.error }}
' + + '' + + '' + + '' + '
' + '
' + strings.poweredby @@ -632,6 +923,18 @@ Vue.component('pretix-widget', { ), data: shared_widget_data, methods: shared_methods, + mounted: function () { + this.mobile = this.$refs.wrapper.clientWidth <= 800; + }, + computed: { + classObject: function () { + o = {'pretix-widget': true}; + if (this.mobile) { + o['pretix-widget-mobile'] = true; + } + return o; + } + } }); Vue.component('pretix-button', { @@ -674,9 +977,9 @@ var shared_root_methods = { reload: function () { var url; if (this.$root.subevent) { - url = this.$root.event_url + this.$root.subevent + '/widget/product_list?lang=' + lang; + url = this.$root.target_url + this.$root.subevent + '/widget/product_list?lang=' + lang; } else { - url = this.$root.event_url + 'widget/product_list?lang=' + lang; + url = this.$root.target_url + 'widget/product_list?lang=' + lang; } var cart_id = getCookie(this.cookieName); if (this.$root.voucher_code) { @@ -685,6 +988,12 @@ var shared_root_methods = { if (cart_id) { url += "&cart_id=" + cart_id; } + if (this.$root.date !== null) { + url += "&year=" + this.$root.date.substr(0, 4) + "&month=" + this.$root.date.substr(5, 2); + } + if (this.$root.style !== null) { + url = url + '&style=' + this.$root.style; + } var root = this.$root; api._getJSON(url, function (data, xhr) { if (typeof xhr.responseURL !== "undefined" && xhr.responseURL !== url) { @@ -692,21 +1001,34 @@ var shared_root_methods = { if (root.subevent) { new_url = new_url.substr(0, new_url.lastIndexOf("/", new_url.length - 1) + 1); } - root.event_url = new_url; + root.target_url = new_url; root.reload(); return; } - root.categories = data.items_by_category; - root.currency = data.currency; - root.display_net_prices = data.display_net_prices; - root.error = data.error; - root.display_add_to_cart = data.display_add_to_cart; - root.waiting_list_enabled = data.waiting_list_enabled; - root.show_variations_expanded = data.show_variations_expanded; - root.cart_id = cart_id; - root.cart_exists = data.cart_exists; - root.vouchers_exist = data.vouchers_exist; - root.itemnum = data.itemnum; + if (data.weeks !== undefined) { + root.weeks = data.weeks; + root.date = data.date; + root.events = undefined; + root.view = "weeks"; + } else if (data.events !== undefined) { + root.events = data.events; + root.weeks = undefined; + root.view = "events"; + } else { + root.view = "event"; + root.name = data.name; + root.categories = data.items_by_category; + root.currency = data.currency; + root.display_net_prices = data.display_net_prices; + root.error = data.error; + root.display_add_to_cart = data.display_add_to_cart; + root.waiting_list_enabled = data.waiting_list_enabled; + root.show_variations_expanded = data.show_variations_expanded; + root.cart_id = cart_id; + root.cart_exists = data.cart_exists; + root.vouchers_exist = data.vouchers_exist; + root.itemnum = data.itemnum; + } if (root.loading > 0) { root.loading--; } @@ -718,15 +1040,22 @@ var shared_root_methods = { root.loading--; } }); + }, + choose_event: function (event) { + root.target_url = event.event_url; + this.$root.error = null; + root.subevent = event.subevent; + root.loading++; + root.reload(); } }; var shared_root_computed = { cookieName: function () { - return "pretix_widget_" + this.event_url.replace(/[^a-zA-Z0-9]+/g, "_"); + return "pretix_widget_" + this.target_url.replace(/[^a-zA-Z0-9]+/g, "_"); }, voucherFormTarget: function () { - var form_target = this.event_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang; + var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang; var cookie = getCookie(this.cookieName); if (cookie) { form_target += "&take_cart_id=" + cookie; @@ -737,11 +1066,11 @@ var shared_root_computed = { return form_target; }, formTarget: function () { - var checkout_url = "/" + this.event_url.replace(/^[^\/]+:\/\/([^\/]+)\//, "") + "w/" + widget_id + "/"; + var checkout_url = "/" + this.target_url.replace(/^[^\/]+:\/\/([^\/]+)\//, "") + "w/" + widget_id + "/"; if (!this.$root.cart_exists) { checkout_url += "checkout/start"; } - var form_target = this.event_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url); + var form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url); var cookie = getCookie(this.cookieName); if (cookie) { form_target += "&take_cart_id=" + cookie; @@ -812,12 +1141,13 @@ function get_ga_client_id(tracking_id) { } var create_widget = function (element) { - var event_url = element.attributes.event.value; - if (!event_url.match(/\/$/)) { - event_url += "/"; + var target_url = element.attributes.event.value; + if (!target_url.match(/\/$/)) { + target_url += "/"; } var voucher = element.attributes.voucher ? element.attributes.voucher.value : null; var subevent = element.attributes.subevent ? element.attributes.subevent.value : null; + var style = element.attributes.style ? element.attributes.style.value : null; var skip_ssl = element.attributes["skip-ssl-check"] ? true : false; var disable_vouchers = element.attributes["disable-vouchers"] ? true : false; var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data)); @@ -836,16 +1166,23 @@ var create_widget = function (element) { el: element, data: function () { return { - event_url: event_url, + target_url: target_url, + parent_stack: [], subevent: subevent, is_button: false, categories: null, currency: null, + name: null, voucher_code: voucher, display_net_prices: false, show_variations_expanded: false, skip_ssl: skip_ssl, + style: style, error: null, + weeks: null, + date: null, + events: null, + view: null, display_add_to_cart: false, widget_data: widget_data, loading: 1, @@ -868,9 +1205,9 @@ var create_widget = function (element) { }; var create_button = function (element) { - var event_url = element.attributes.event.value; - if (!event_url.match(/\/$/)) { - event_url += "/"; + var target_url = element.attributes.event.value; + if (!target_url.match(/\/$/)) { + target_url += "/"; } var voucher = element.attributes.voucher ? element.attributes.voucher.value : null; var subevent = element.attributes.subevent ? element.attributes.subevent.value : null; @@ -902,7 +1239,7 @@ var create_button = function (element) { el: element, data: function () { return { - event_url: event_url, + target_url: target_url, subevent: subevent, is_button: true, skip_ssl: skip_ssl, diff --git a/src/pretix/static/pretixpresale/scss/widget.scss b/src/pretix/static/pretixpresale/scss/widget.scss index 8e1c2865f..e3909e1dc 100644 --- a/src/pretix/static/pretixpresale/scss/widget.scss +++ b/src/pretix/static/pretixpresale/scss/widget.scss @@ -116,9 +116,9 @@ padding: 10px; text-align: center; margin: 10px 0; - background-color: $alert-danger-bg; - border-color: $alert-danger-border; - color: $alert-danger-text; + background-color: white; + border: 2px solid $brand-danger; + color: $brand-danger; border-radius: $alert-border-radius; } @@ -345,6 +345,136 @@ max-height: 1000px; overflow: hidden; } + .pretix-widget-event-header { + padding-top: 10px; + text-align: center; + } + .pretix-widget-event-list-back { + padding-top: 10px; + text-align: center; + display: block; + a { + display: block; + } + } + .pretix-widget-back { + padding-bottom: 10px; + text-align: center; + display: block; + a { + display: block; + } + } + + .pretix-widget-event-list { + padding: 10px 0; + cursor: pointer; + } + .pretix-widget-event-list-entry { + display: flex; + flex-direction: row; + padding: 5px 0; + flex-wrap: wrap; + color: $text-color; + + &:hover, &:active, &:focus { + background: $gray-lighter; + text-decoration: none; + } + + .pretix-widget-event-list-entry-name { + width: 50%; + padding: 5px; + box-sizing: border-box; + } + .pretix-widget-event-list-entry-date { + width: 25%; + padding: 5px; + box-sizing: border-box; + } + .pretix-widget-event-list-entry-availability { + width: 25%; + text-align: right; + padding: 7px 5px 3px; + box-sizing: border-box; + span { + display: inline; + padding: 2px 6px 3px; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 4px; + } + } + } + + .pretix-widget-event-availability-orange .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-orange.pretix-widget-event-calendar-event { + background-color: $brand-warning; + } + .pretix-widget-event-availability-none .pretix-widget-event-list-entry-availability span { + background-color: $brand-primary; + } + .pretix-widget-event-availability-green .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-green.pretix-widget-event-calendar-event { + background-color: $brand-success; + } + .pretix-widget-event-availability-red .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-red.pretix-widget-event-calendar-event { + background-color: $brand-danger; + } + + .pretix-widget-event-calendar { + padding-top: 10px; + + .pretix-widget-event-calendar-head { + display: flex; + flex-direction: row; + + strong { + width: 50%; + text-align: center; + display: block; + } + .pretix-widget-event-calendar-next-month, .pretix-widget-event-calendar-previous-month { + display: block; + width: 25%; + } + .pretix-widget-event-calendar-next-month { + text-align: right; + } + } + .pretix-widget-event-calendar-event { + display: block; + border-radius: 4px; + padding: 5px; + color: white; + cursor: pointer; + margin-bottom: 5px; + &:last-child { + margin-bottom: 0; + } + &:hover { + text-decoration: none; + } + } + + .pretix-widget-event-calendar-table { + width: 100%; + th, td { + width: 14.285714285714286%; + vertical-align: top; + padding: 10px 5px; + } + } + .pretix-widget-event-calendar-day { + font-weight: bold; + } + } } @keyframes pretix-widget-bounce-in { @@ -504,28 +634,71 @@ fill: $brand-primary; } -@media (max-width: $screen-sm-max) { - .pretix-widget { - .pretix-widget-item-info-col { +.pretix-widget.pretix-widget-mobile { + .pretix-widget-item-info-col { + width: 100%; + float: none; + margin-bottom: 5px; + } + .pretix-widget-item-price-col, .pretix-widget-item-availability-col { + width: 50%; + } + .pretix-widget-action { + width: 100%; + margin-left: 0; + } + .pretix-widget-voucher-input-wrap { + width: 100%; + float: none; + } + .pretix-widget-voucher-button-wrap { + width: 100%; + float: none; + margin-top: 10px; + } + + .pretix-widget-event-list-entry { + .pretix-widget-event-list-entry-name { width: 100%; - float: none; - margin-bottom: 5px; } - .pretix-widget-item-price-col, .pretix-widget-item-availability-col { + .pretix-widget-event-list-entry-date { width: 50%; } - .pretix-widget-action { - width: 100%; - margin-left: 0; + .pretix-widget-event-list-entry-availability { + width: 50%; } - .pretix-widget-voucher-input-wrap { - width: 100%; - float: none; + } + + .pretix-widget-event-calendar { + .pretix-widget-event-calendar-events { + display: none; } - .pretix-widget-voucher-button-wrap { - width: 100%; - float: none; - margin-top: 10px; + td.pretix-widget-has-events { + background: $brand-primary; + color: white; + cursor: pointer; + &.pretix-widget-day-availability-red { + background: $brand-danger; + } + &.pretix-widget-day-availability-green { + background: $brand-success; + } + &.pretix-widget-day-availability-orange { + background: $brand-warning; + } + } + + .pretix-widget-event-calendar-head { + display: block; + strong { + width: 100%; + display: block; + } + .pretix-widget-event-calendar-next-month, .pretix-widget-event-calendar-previous-month { + display: block; + width: 100%; + text-align: center; + } } } } diff --git a/src/requirements/dev.txt b/src/requirements/dev.txt index 801945553..6b9cb86de 100644 --- a/src/requirements/dev.txt +++ b/src/requirements/dev.txt @@ -17,3 +17,5 @@ pytest-cache pytest-sugar responses potypo +freezegun + diff --git a/src/setup.py b/src/setup.py index 7e06e4ef1..de1a2fe3d 100644 --- a/src/setup.py +++ b/src/setup.py @@ -158,7 +158,8 @@ setup( 'isort', 'pytest-mock==1.6.*', 'pytest-rerunfailures', - 'responses' + 'responses', + 'freezegun', ], 'memcached': ['pylibmc'], 'mysql': ['mysqlclient'], diff --git a/src/tests/presale/test_widget.py b/src/tests/presale/test_widget.py index 47c4887f6..7e2294d3f 100644 --- a/src/tests/presale/test_widget.py +++ b/src/tests/presale/test_widget.py @@ -6,6 +6,7 @@ from bs4 import BeautifulSoup from django.conf import settings from django.test import TestCase from django.utils.timezone import now +from freezegun import freeze_time from pretix.base.models import Order, OrderPosition from pretix.presale.style import regenerate_css, regenerate_organizer_css @@ -123,6 +124,7 @@ class WidgetCartTest(CartTestMixin, TestCase): assert response['Access-Control-Allow-Origin'] == '*' data = json.loads(response.content.decode()) assert data == { + "name": "30C3", "currency": "EUR", "show_variations_expanded": False, "display_net_prices": False, @@ -202,6 +204,7 @@ class WidgetCartTest(CartTestMixin, TestCase): assert response['Access-Control-Allow-Origin'] == '*' data = json.loads(response.content.decode()) assert data == { + "name": "30C3", "currency": "EUR", "show_variations_expanded": False, "display_net_prices": False, @@ -245,6 +248,7 @@ class WidgetCartTest(CartTestMixin, TestCase): assert response['Access-Control-Allow-Origin'] == '*' data = json.loads(response.content.decode()) assert data == { + "name": "30C3", "currency": "EUR", "show_variations_expanded": False, "display_net_prices": False, @@ -289,3 +293,221 @@ class WidgetCartTest(CartTestMixin, TestCase): c = response.content.decode() assert '%m/%d/%Y' not in c assert '%d.%m.%Y' in c + + def test_subevent_list(self): + self.event.has_subevents = True + self.event.save() + with freeze_time("2019-01-01 10:00:00"): + self.event.subevents.create(name="Past", active=True, date_from=now() - datetime.timedelta(days=3)) + se1 = self.event.subevents.create(name="Present", active=True, date_from=now()) + se2 = self.event.subevents.create(name="Future", active=True, date_from=now() + datetime.timedelta(days=3)) + self.event.subevents.create(name="Disabled", active=False, date_from=now() + datetime.timedelta(days=3)) + + response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug)) + data = json.loads(response.content.decode()) + settings.SITE_URL = 'http://example.com' + assert data == { + 'list_type': 'list', + 'events': [ + {'name': 'Present', 'date_range': 'Jan. 1, 2019 10:00', 'availability': {'color': 'green', 'text': 'Tickets on sale'}, + 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk}, + {'name': 'Future', 'date_range': 'Jan. 4, 2019 10:00', 'availability': {'color': 'green', 'text': 'Tickets on sale'}, + 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk} + ] + } + + def test_subevent_calendar(self): + self.event.has_subevents = True + self.event.save() + with freeze_time("2019-01-01 10:00:00"): + self.event.subevents.create(name="Past", active=True, date_from=now() - datetime.timedelta(days=3)) + se1 = self.event.subevents.create(name="Present", active=True, date_from=now()) + se2 = self.event.subevents.create(name="Future", active=True, date_from=now() + datetime.timedelta(days=3)) + self.event.subevents.create(name="Disabled", active=False, date_from=now() + datetime.timedelta(days=3)) + + response = self.client.get('/%s/%s/widget/product_list?style=calendar' % (self.orga.slug, self.event.slug)) + settings.SITE_URL = 'http://example.com' + data = json.loads(response.content.decode()) + assert data == { + 'list_type': 'calendar', + 'date': '2019-01-01', + 'weeks': [ + [ + None, + {'day': 1, 'date': '2019-01-01', 'events': [ + {'name': 'Present', 'time': '10:00', 'continued': False, 'date_range': 'Jan. 1, 2019 10:00', + 'availability': {'color': 'green', 'text': 'Tickets on sale'}, + 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk}]}, + {'day': 2, 'date': '2019-01-02', 'events': []}, + {'day': 3, 'date': '2019-01-03', 'events': []}, + {'day': 4, 'date': '2019-01-04', 'events': [ + {'name': 'Future', 'time': '10:00', 'continued': False, 'date_range': 'Jan. 4, 2019 10:00', + 'availability': {'color': 'green', 'text': 'Tickets on sale'}, + 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk}]}, + {'day': 5, 'date': '2019-01-05', 'events': []}, + {'day': 6, 'date': '2019-01-06', 'events': []} + ], + [ + {'day': 7, 'date': '2019-01-07', 'events': []}, + {'day': 8, 'date': '2019-01-08', 'events': []}, + {'day': 9, 'date': '2019-01-09', 'events': []}, + {'day': 10, 'date': '2019-01-10', 'events': []}, + {'day': 11, 'date': '2019-01-11', 'events': []}, + {'day': 12, 'date': '2019-01-12', 'events': []}, + {'day': 13, 'date': '2019-01-13', 'events': []} + ], + [ + {'day': 14, 'date': '2019-01-14', 'events': []}, + {'day': 15, 'date': '2019-01-15', 'events': []}, + {'day': 16, 'date': '2019-01-16', 'events': []}, + {'day': 17, 'date': '2019-01-17', 'events': []}, + {'day': 18, 'date': '2019-01-18', 'events': []}, + {'day': 19, 'date': '2019-01-19', 'events': []}, + {'day': 20, 'date': '2019-01-20', 'events': []} + ], + [ + {'day': 21, 'date': '2019-01-21', 'events': []}, + {'day': 22, 'date': '2019-01-22', 'events': []}, + {'day': 23, 'date': '2019-01-23', 'events': []}, + {'day': 24, 'date': '2019-01-24', 'events': []}, + {'day': 25, 'date': '2019-01-25', 'events': []}, + {'day': 26, 'date': '2019-01-26', 'events': []}, + {'day': 27, 'date': '2019-01-27', 'events': []} + ], + [ + {'day': 28, 'date': '2019-01-28', 'events': []}, + {'day': 29, 'date': '2019-01-29', 'events': []}, + {'day': 30, 'date': '2019-01-30', 'events': []}, + {'day': 31, 'date': '2019-01-31', 'events': []}, + None, None, None + ] + ] + } + + def test_event_list(self): + self.event.has_subevents = True + self.event.save() + with freeze_time("2019-01-01 10:00:00"): + self.orga.events.create(name="Past", live=True, is_public=True, slug='past', date_from=now() - datetime.timedelta(days=3)) + self.orga.events.create(name="Present", live=True, is_public=True, slug='present', date_from=now()) + self.orga.events.create(name="Future", live=True, is_public=True, slug='future', date_from=now() + datetime.timedelta(days=3)) + self.orga.events.create(name="Disabled", live=False, is_public=True, slug='disabled', date_from=now() + datetime.timedelta(days=3)) + self.orga.events.create(name="Secret", live=True, is_public=False, slug='secret', date_from=now() + datetime.timedelta(days=3)) + self.event.subevents.create(name="Past", active=True, date_from=now() - datetime.timedelta(days=3)) + self.event.subevents.create(name="Present", active=True, date_from=now()) + self.event.subevents.create(name="Future", active=True, date_from=now() + datetime.timedelta(days=3)) + self.event.subevents.create(name="Disabled", active=False, date_from=now() + datetime.timedelta(days=3)) + + settings.SITE_URL = 'http://example.com' + response = self.client.get('/%s/widget/product_list' % (self.orga.slug,)) + data = json.loads(response.content.decode()) + assert data == { + 'events': [ + {'availability': {'color': 'none', 'text': 'Event series'}, + 'date_range': 'Dec. 29, 2018 – Jan. 4, 2019', + 'event_url': 'http://example.com/ccc/30c3/', + 'name': '30C3'}, + {'availability': {'color': 'green', 'text': 'Tickets on sale'}, + 'date_range': 'Jan. 1, 2019 10:00', + 'event_url': 'http://example.com/ccc/present/', + 'name': 'Present'}, + {'availability': {'color': 'green', 'text': 'Tickets on sale'}, + 'date_range': 'Jan. 4, 2019 10:00', + 'event_url': 'http://example.com/ccc/future/', + 'name': 'Future'} + ], + 'list_type': 'list' + } + + def test_event_calendar(self): + self.event.has_subevents = True + self.event.save() + with freeze_time("2019-01-01 10:00:00"): + self.orga.events.create(name="Past", live=True, is_public=True, slug='past', date_from=now() - datetime.timedelta(days=3)) + self.orga.events.create(name="Present", live=True, is_public=True, slug='present', date_from=now()) + self.orga.events.create(name="Future", live=True, is_public=True, slug='future', date_from=now() + datetime.timedelta(days=3)) + self.orga.events.create(name="Disabled", live=False, is_public=True, slug='disabled', date_from=now() + datetime.timedelta(days=3)) + self.orga.events.create(name="Secret", live=True, is_public=False, slug='secret', date_from=now() + datetime.timedelta(days=3)) + self.event.subevents.create(name="Past", active=True, date_from=now() - datetime.timedelta(days=3)) + se1 = self.event.subevents.create(name="Present", active=True, date_from=now()) + se2 = self.event.subevents.create(name="Future", active=True, date_from=now() + datetime.timedelta(days=3)) + self.event.subevents.create(name="Disabled", active=False, date_from=now() + datetime.timedelta(days=3)) + + response = self.client.get('/%s/widget/product_list?style=calendar' % (self.orga.slug,)) + settings.SITE_URL = 'http://example.com' + data = json.loads(response.content.decode()) + assert data == { + 'date': '2019-01-01', + 'list_type': 'calendar', + 'weeks': [ + [None, + {'date': '2019-01-01', + 'day': 1, + 'events': [{'availability': {'color': 'green', + 'text': 'Tickets on sale'}, + 'continued': False, + 'date_range': 'Jan. 1, 2019 10:00', + 'event_url': 'http://example.com/ccc/present/', + 'name': 'Present', + 'subevent': None, + 'time': '10:00'}, + {'availability': {'color': 'green', + 'text': 'Tickets on sale'}, + 'continued': False, + 'date_range': 'Jan. 1, 2019 10:00', + 'event_url': 'http://example.com/ccc/30c3/', + 'name': 'Present', + 'subevent': se1.pk, + 'time': '10:00'}]}, + {'date': '2019-01-02', 'day': 2, 'events': []}, + {'date': '2019-01-03', 'day': 3, 'events': []}, + {'date': '2019-01-04', + 'day': 4, + 'events': [{'availability': {'color': 'green', + 'text': 'Tickets on sale'}, + 'continued': False, + 'date_range': 'Jan. 4, 2019 10:00', + 'event_url': 'http://example.com/ccc/future/', + 'name': 'Future', + 'subevent': None, + 'time': '10:00'}, + {'availability': {'color': 'green', + 'text': 'Tickets on sale'}, + 'continued': False, + 'date_range': 'Jan. 4, 2019 10:00', + 'event_url': 'http://example.com/ccc/30c3/', + 'name': 'Future', + 'subevent': se2.pk, + 'time': '10:00'}]}, + {'date': '2019-01-05', 'day': 5, 'events': []}, + {'date': '2019-01-06', 'day': 6, 'events': []}], + [{'date': '2019-01-07', 'day': 7, 'events': []}, + {'date': '2019-01-08', 'day': 8, 'events': []}, + {'date': '2019-01-09', 'day': 9, 'events': []}, + {'date': '2019-01-10', 'day': 10, 'events': []}, + {'date': '2019-01-11', 'day': 11, 'events': []}, + {'date': '2019-01-12', 'day': 12, 'events': []}, + {'date': '2019-01-13', 'day': 13, 'events': []}], + [{'date': '2019-01-14', 'day': 14, 'events': []}, + {'date': '2019-01-15', 'day': 15, 'events': []}, + {'date': '2019-01-16', 'day': 16, 'events': []}, + {'date': '2019-01-17', 'day': 17, 'events': []}, + {'date': '2019-01-18', 'day': 18, 'events': []}, + {'date': '2019-01-19', 'day': 19, 'events': []}, + {'date': '2019-01-20', 'day': 20, 'events': []}], + [{'date': '2019-01-21', 'day': 21, 'events': []}, + {'date': '2019-01-22', 'day': 22, 'events': []}, + {'date': '2019-01-23', 'day': 23, 'events': []}, + {'date': '2019-01-24', 'day': 24, 'events': []}, + {'date': '2019-01-25', 'day': 25, 'events': []}, + {'date': '2019-01-26', 'day': 26, 'events': []}, + {'date': '2019-01-27', 'day': 27, 'events': []}], + [{'date': '2019-01-28', 'day': 28, 'events': []}, + {'date': '2019-01-29', 'day': 29, 'events': []}, + {'date': '2019-01-30', 'day': 30, 'events': []}, + {'date': '2019-01-31', 'day': 31, 'events': []}, + None, + None, + None] + ] + }