import datetime import json from decimal import Decimal from bs4 import BeautifulSoup from django.conf import settings from django.test import TestCase, override_settings 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 from .test_cart import CartTestMixin class WidgetCartTest(CartTestMixin, TestCase): def setUp(self): super().setUp() self.order = Order.objects.create( status=Order.STATUS_PENDING, event=self.event, email='admin@localhost', datetime=now() - datetime.timedelta(days=3), expires=now() + datetime.timedelta(days=11), total=Decimal("23"), locale='en' ) self.ticket_pos = OrderPosition.objects.create( order=self.order, item=self.ticket, variation=None, price=Decimal("23"), attendee_name_parts={'full_name': "Peter"} ) def test_iframe_entry_view_wrapper(self): self.client.get('/%s/%s/?iframe=1&locale=de' % (self.orga.slug, self.event.slug)) assert 'iframe_session' in self.client.session assert self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value == "de" def test_allow_frame_if_namespaced(self): response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)) assert 'X-Frame-Options' in response response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaaa/' % (self.orga.slug, self.event.slug)) assert 'X-Frame-Options' not in response response = self.client.get('/%s/%s/waitinglist' % (self.orga.slug, self.event.slug)) assert 'X-Frame-Options' in response response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaaa/waitinglist' % (self.orga.slug, self.event.slug)) assert 'X-Frame-Options' not in response def test_allow_frame_on_order(self): response = self.client.get('/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)) assert 'X-Frame-Options' not in response response = self.client.get('/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)) assert 'X-Frame-Options' not in response response = self.client.get('/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)) assert 'X-Frame-Options' not in response response = self.client.get('/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)) assert 'X-Frame-Options' not in response def test_allow_cors_if_namespaced(self): response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { 'item_%d' % self.ticket.id: '1', 'ajax': 1 }) assert 'Access-Control-Allow-Origin' not in response response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaaa/cart/add' % (self.orga.slug, self.event.slug), { 'item_%d' % self.ticket.id: '1', 'ajax': 1 }) assert response['Access-Control-Allow-Origin'] == '*' def test_cart_isolation(self): response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { 'item_%d' % self.ticket.id: '1' }, follow=True) self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug), target_status_code=200) doc = BeautifulSoup(response.rendered_content, "lxml") assert len(doc.select('.cart .cart-row')) == 2 self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text) self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text) response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaaa/' % (self.orga.slug, self.event.slug)) doc = BeautifulSoup(response.rendered_content, "lxml") assert len(doc.select('.cart .cart-row')) == 0 response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaaa/cart/add' % (self.orga.slug, self.event.slug), { 'item_%d' % self.ticket.id: '1' }, follow=True) self.assertRedirects(response, '/%s/%s/w/aaaaaaaaaaaaaaaa/?require_cookie=true' % (self.orga.slug, self.event.slug), target_status_code=200) doc = BeautifulSoup(response.rendered_content, "lxml") assert len(doc.select('.cart .cart-row')) == 2 self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text) self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text) response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaab/' % (self.orga.slug, self.event.slug)) doc = BeautifulSoup(response.rendered_content, "lxml") assert len(doc.select('.cart .cart-row')) == 0 response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaab/cart/add' % (self.orga.slug, self.event.slug), { 'item_%d' % self.ticket.id: '1' }, follow=True) self.assertRedirects(response, '/%s/%s/w/aaaaaaaaaaaaaaab/?require_cookie=true' % (self.orga.slug, self.event.slug), target_status_code=200) doc = BeautifulSoup(response.rendered_content, "lxml") assert len(doc.select('.cart .cart-row')) == 2 self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text) self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text) def test_product_list_view(self): response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug)) 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, "vouchers_exist": False, "waiting_list_enabled": False, "error": None, "items_by_category": [ { "items": [ { "require_voucher": False, "order_min": None, "max_price": None, "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False}, "picture": None, "has_variations": 0, "description": None, "min_price": None, "avail": [100, None], "variations": [], "id": self.ticket.pk, "free_price": False, "original_price": None, "name": "Early-bird ticket", "order_max": 4 }, { "require_voucher": False, "order_min": None, "max_price": "14.00", "price": None, "picture": None, "has_variations": 4, "description": None, "min_price": "12.00", "avail": None, "variations": [ { "value": "Red", "id": self.shirt_red.pk, "price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False}, "description": None, "avail": [100, None], "order_max": 2 }, { "value": "Blue", "id": self.shirt_blue.pk, "price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False}, "description": None, "avail": [100, None], "order_max": 2 } ], "id": self.shirt.pk, "free_price": False, "original_price": None, "name": "T-Shirt", "order_max": None } ], "description": None, "id": self.category.pk, "name": "Everything" } ], "itemnum": 2, "display_add_to_cart": True, "cart_exists": False } def test_product_list_view_with_voucher(self): self.event.vouchers.create(item=self.ticket, code="ABCDE") response = self.client.get('/%s/%s/widget/product_list?voucher=ABCDE' % (self.orga.slug, self.event.slug)) 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, "vouchers_exist": True, "waiting_list_enabled": False, "error": None, "items_by_category": [ { "items": [ { "require_voucher": False, "order_min": None, "max_price": None, "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False}, "picture": None, "has_variations": 0, "description": None, "min_price": None, "avail": [100, None], "variations": [], "id": self.ticket.pk, "free_price": False, "original_price": None, "name": "Early-bird ticket", "order_max": 4 }, ], "description": None, "id": self.category.pk, "name": "Everything" } ], "itemnum": 1, "display_add_to_cart": True, "cart_exists": False } def test_product_list_view_with_voucher_variation_through_quota(self): self.event.vouchers.create(quota=self.quota_shirts, code="ABCDE") self.quota_shirts.variations.remove(self.shirt_blue) response = self.client.get('/%s/%s/widget/product_list?voucher=ABCDE' % (self.orga.slug, self.event.slug)) 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, "vouchers_exist": True, "waiting_list_enabled": False, "error": None, "items_by_category": [ { "items": [ { 'id': self.shirt.pk, 'name': 'T-Shirt', 'picture': None, 'description': None, 'has_variations': 2, 'require_voucher': False, 'order_min': None, 'order_max': None, 'price': None, 'min_price': '14.00', 'max_price': '14.00', 'free_price': False, 'avail': None, 'original_price': None, 'variations': [ { 'id': self.shirt_red.pk, 'value': 'Red', 'order_max': 2, 'description': None, 'price': { 'gross': '14.00', 'net': '11.76', 'tax': '2.24', 'rate': '19.00', 'name': '', 'includes_mixed_tax_rate': False }, 'avail': [100, None] }, ] } ], "description": None, "id": self.category.pk, "name": "Everything" } ], "itemnum": 1, "display_add_to_cart": True, "cart_exists": False } def test_product_list_view_with_voucher_expired(self): self.event.vouchers.create(item=self.ticket, code="ABCDE", valid_until=now() - datetime.timedelta(days=1)) response = self.client.get('/%s/%s/widget/product_list?voucher=ABCDE' % (self.orga.slug, self.event.slug)) 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, "vouchers_exist": True, "waiting_list_enabled": False, "error": "This voucher is expired.", "items_by_category": [], "display_add_to_cart": False, "cart_exists": False, "itemnum": 0, } @override_settings(COMPRESS_PRECOMPILERS=settings.COMPRESS_PRECOMPILERS_ORIGINAL) def test_css_customized(self): response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug)) c = b"".join(response.streaming_content).decode() assert '#7f5a91' in c assert '#33c33c' not in c assert '#34c34c' not in c self.orga.settings.primary_color = "#33c33c" regenerate_organizer_css.apply(args=(self.orga.pk,)) response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug)) c = b"".join(response.streaming_content).decode() assert '#7f5a91' not in c assert '#33c33c' in c assert '#34c34c' not in c self.event.settings.primary_color = "#34c34c" regenerate_css.apply(args=(self.event.pk,)) response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug)) c = b"".join(response.streaming_content).decode() assert '#7f5a91' not in c assert '#33c33c' not in c assert '#34c34c' in c def test_js_localized(self): response = self.client.get('/widget/v1.en.js') c = response.content.decode() assert '%m/%d/%Y' in c assert '%d.%m.%Y' not in c response = self.client.get('/widget/v1.de.js') c = response.content.decode() assert '%m/%d/%Y' not in c assert '%d.%m.%Y' in c def test_product_list_view_with_bundle_sold_out(self): self.quota_shirts.size = 0 self.quota_shirts.save() self.ticket.bundles.create(bundled_item=self.shirt, bundled_variation=self.shirt_blue, designated_price=2, count=1) response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug)) assert response['Access-Control-Allow-Origin'] == '*' data = json.loads(response.content.decode()) assert data["items_by_category"][0]["items"][0]["avail"] == [0, None] def test_product_list_view_with_bundle_mixed_tax_rate(self): self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00')) self.shirt.tax_rule = self.tr7 self.shirt.require_bundling = True self.shirt.save() self.ticket.bundles.create(bundled_item=self.shirt, bundled_variation=self.shirt_blue, designated_price=2, count=1) response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug)) assert response['Access-Control-Allow-Origin'] == '*' data = json.loads(response.content.decode()) assert len(data["items_by_category"][0]["items"]) == 1 assert data["items_by_category"][0]["items"][0]["price"] == { "gross": "23.00", "net": "19.52", "tax": "3.48", "name": "MIXED!", "rate": "19.00", "includes_mixed_tax_rate": True } def test_subevent_list(self): self.event.has_subevents = True self.event.settings.timezone = 'Europe/Berlin' 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)) self.event.subevents.create(name="Hidden", active=True, is_public=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 11: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 11: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.settings.timezone = 'Europe/Berlin' 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)) self.event.subevents.create(name="Hidden", active=True, is_public=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': '11:00', 'continued': False, 'date_range': 'Jan. 1, 2019 11: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': '11:00', 'continued': False, 'date_range': 'Jan. 4, 2019 11: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.settings.timezone = 'Europe/Berlin' 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)) self.event.subevents.create(name="Hidden", active=True, is_public=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.settings.timezone = 'Europe/Berlin' 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)) self.event.subevents.create(name="Hidden", active=True, is_public=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 11:00', 'event_url': 'http://example.com/ccc/30c3/', 'name': 'Present', 'subevent': se1.pk, 'time': '11: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 11:00', 'event_url': 'http://example.com/ccc/30c3/', 'name': 'Future', 'subevent': se2.pk, 'time': '11: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] ] }