Fix #878 -- Add multi-event widget

This commit is contained in:
Raphael Michel
2019-03-19 12:16:09 +01:00
parent ca7d55082b
commit 49e706a580
13 changed files with 1096 additions and 168 deletions

View File

@@ -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):

View File

@@ -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(

View File

@@ -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')))

View File

@@ -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 = [

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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: ('<div class="pretix-widget-wrapper">'
+ '<div class="pretix-widget">'
+ shared_loading_fragment
Vue.component('pretix-widget-event-form', {
template: ('<div class="pretix-widget-event-form">'
+ '<div class="pretix-widget-event-list-back" v-if="$root.events || $root.weeks">'
+ '<a href="#" @click.prevent="back_to_list">&lsaquo; '
+ strings['back_to_list']
+ '</a>'
+ '</div>'
+ '<div class="pretix-widget-event-header" v-if="$root.events || $root.weeks">'
+ '<strong>{{ $root.name }}</strong>'
+ '</div>'
+ '<form method="post" :action="$root.formTarget" ref="form" target="_blank">'
+ '<input type="hidden" name="_voucher_code" :value="$root.voucher_code" v-if="$root.voucher_code">'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />'
@@ -597,7 +641,7 @@ Vue.component('pretix-widget', {
+ '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>'
+ '<div class="pretix-widget-info-message pretix-widget-clickable"'
+ ' v-if="$root.cart_exists">'
+ '<button @click.prevent="resume" class="pretix-widget-resume-button" type="button">'
+ '<button @click.prevent="$parent.resume" class="pretix-widget-resume-button" type="button">'
+ strings['resume_checkout']
+ '</button>'
+ strings['cart_exists']
@@ -605,7 +649,7 @@ Vue.component('pretix-widget', {
+ '</div>'
+ '<category v-for="category in this.$root.categories" :category="category" :key="category.id"></category>'
+ '<div class="pretix-widget-action" v-if="$root.display_add_to_cart">'
+ '<button @click="buy" type="submit">' + strings.buy + '</button>'
+ '<button @click="$parent.buy" type="submit">' + strings.buy + '</button>'
+ '</div>'
+ '</form>'
+ '<form method="get" :action="$root.voucherFormTarget" target="_blank" '
@@ -613,15 +657,262 @@ Vue.component('pretix-widget', {
+ '<div class="pretix-widget-voucher">'
+ '<h3 class="pretix-widget-voucher-headline">'+ strings['redeem_voucher'] +'</h3>'
+ '<div class="pretix-widget-voucher-input-wrap">'
+ '<input class="pretix-widget-voucher-input" type="text" v-model="voucher" name="voucher" placeholder="'+strings.voucher_code+'">'
+ '<input class="pretix-widget-voucher-input" type="text" v-model="$parent.voucher" name="voucher" placeholder="'+strings.voucher_code+'">'
+ '</div>'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<input type="hidden" name="locale" value="' + lang + '" />'
+ '<div class="pretix-widget-voucher-button-wrap">'
+ '<button @click="redeem">' + strings.redeem + '</button>'
+ '<button @click="$parent.redeem">' + strings.redeem + '</button>'
+ '</div>'
+ '</div>'
+ '</form>'
+ '</div>'
),
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: ('<a :class="classObject" @click.prevent="select">'
+ '<div class="pretix-widget-event-list-entry-name">{{ event.name }}</div>'
+ '<div class="pretix-widget-event-list-entry-date">{{ event.date_range }}</div>'
+ '<div class="pretix-widget-event-list-entry-availability"><span>{{ event.availability.text }}</span></div>'
+ '</a>'),
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: ('<div class="pretix-widget-event-list">'
+ '<div class="pretix-widget-back" v-if="$root.weeks || $root.parent_stack.length > 0">'
+ '<a href="#" @click.prevent="back_to_calendar">&lsaquo; '
+ strings['back']
+ '</a>'
+ '</div>'
+ '<pretix-widget-event-list-entry v-for="event in $root.events" :event="event" :key="event.url"></pretix-widget-event-list-entry>'
+ '</div>'),
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: ('<a :class="classObject" @click.prevent="select">'
+ '<strong class="pretix-widget-event-calendar-event-name">'
+ '{{ event.name }}'
+ '</strong>'
+ '<div class="pretix-widget-event-calendar-event-date" v-if="!event.continued && event.time">{{ event.time }}</div>'
+ '<div class="pretix-widget-event-calendar-event-availability" v-if="!event.continued">{{ event.availability.text }}</div>'
+ '</a>'),
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: ('<td :class="classObject" @click.prevent="selectDay">'
+ '<div class="pretix-widget-event-calendar-day" v-if="day">'
+ '{{ daynum }}'
+ '</div>'
+ '<div class="pretix-widget-event-calendar-events" v-if="day">'
+ '<pretix-widget-event-calendar-event v-for="e in day.events" :event="e"></pretix-widget-event-calendar-event>'
+ '</div>'
+ '</td>'),
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: ('<tr>'
+ '<pretix-widget-event-calendar-cell v-for="d in week" :day="d"></pretix-widget-event-calendar-cell>'
+ '</tr>'),
props: {
week: Array
},
});
Vue.component('pretix-widget-event-calendar', {
template: ('<div class="pretix-widget-event-calendar" ref="calendar">'
+ '<div class="pretix-widget-back" v-if="$root.events !== undefined">'
+ '<a href="#" @click.prevent="back_to_list">&lsaquo; '
+ strings['back']
+ '</a>'
+ '</div>'
+ '<div class="pretix-widget-event-calendar-head">'
+ '<a class="pretix-widget-event-calendar-previous-month" href="#" @click.prevent="prevmonth">&laquo; '
+ strings['previous_month']
+ '</a> '
+ '<strong>{{ monthname }}</strong> '
+ '<a class="pretix-widget-event-calendar-next-month" href="#" @click.prevent="nextmonth">'
+ strings['next_month']
+ ' &raquo;</a>'
+ '</div>'
+ '<table class="pretix-widget-event-calendar-table">'
+ '<thead>'
+ '<tr>'
+ '<th>' + strings['days']['MO'] + '</th>'
+ '<th>' + strings['days']['TU'] + '</th>'
+ '<th>' + strings['days']['WE'] + '</th>'
+ '<th>' + strings['days']['TH'] + '</th>'
+ '<th>' + strings['days']['FR'] + '</th>'
+ '<th>' + strings['days']['SA'] + '</th>'
+ '<th>' + strings['days']['SU'] + '</th>'
+ '</tr>'
+ '</thead>'
+ '<tbody>'
+ '<pretix-widget-event-calendar-row v-for="week in $root.weeks" :week="week"></pretix-widget-event-calendar-row>'
+ '</tbody>'
+ '</table>'
+ '</div>'),
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: ('<div class="pretix-widget-wrapper" ref="wrapper">'
+ '<div :class="classObject">'
+ shared_loading_fragment
+ '<div class="pretix-widget-error-message" v-if="$root.error && $root.view !== \'event\'">{{ $root.error }}</div>'
+ '<pretix-widget-event-form ref="formcomp" v-if="$root.view === \'event\'"></pretix-widget-event-form>'
+ '<pretix-widget-event-list v-if="$root.view === \'events\'"></pretix-widget-event-list>'
+ '<pretix-widget-event-calendar v-if="$root.view === \'weeks\'"></pretix-widget-event-calendar>'
+ '<div class="pretix-widget-clear"></div>'
+ '<div class="pretix-widget-attribution">'
+ 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,

View File

@@ -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;
}
}
}
}

View File

@@ -17,3 +17,5 @@ pytest-cache
pytest-sugar
responses
potypo
freezegun

View File

@@ -158,7 +158,8 @@ setup(
'isort',
'pytest-mock==1.6.*',
'pytest-rerunfailures',
'responses'
'responses',
'freezegun',
],
'memcached': ['pylibmc'],
'mysql': ['mysqlclient'],

View File

@@ -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]
]
}