'
+ + 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 8e1c2865fd..e3909e1dca 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 8019455530..6b9cb86de4 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 7e06e4ef1c..de1a2fe3d9 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 47c4887f6d..7e2294d3f3 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]
+ ]
+ }