Fix #277 -- Embeddable shop (#622)

* Vendor vue.js

* Refactor item_group_by_category to support vouchers

* Widget: Show product list

* Widget: free prices

* Widget: pictures and loading indicator

* Widget: First iframe steps

* Widget: Do not rerender iframe

* Widget: Error handling

* Improve widget

* Widget: localization tech

* Fix invoice style

* Voucher attribute and waiting list

* Add some iframe chrome

* First step to namespaced carts

* More isolation steps

* More cart isolation things

* More cart isolation things

* Mobile stuff

* Show cart on checkout pages

* PayPal and Stripe support

* Enable downloads

* Locale handling

* change text "save URL to this exact page"

* Widget: voucher redemption

* Widget: CSS

* CSS: Responsive

* Widget: CSS improvements

* Widget: Add embedding code generator

* Widget: Error messages and SSL check

* First tests

* Widget: tests

* Don't use IDs in widgets

* Widget: static files caching
This commit is contained in:
Raphael Michel
2017-10-28 21:54:27 +02:00
committed by GitHub
parent df7fbe5a66
commit 9767243a6d
56 changed files with 12819 additions and 317 deletions

View File

@@ -41,6 +41,7 @@ event_urls = [
"settings/invoice",
"settings/invoice/preview",
"settings/display",
"settings/widget",
"settings/tax/",
"settings/tax/add",
"settings/tax/1/",
@@ -167,6 +168,7 @@ event_permission_urls = [
("can_change_event_settings", "settings/email", 200),
("can_change_event_settings", "settings/display", 200),
("can_change_event_settings", "settings/invoice", 200),
("can_change_event_settings", "settings/widget", 200),
("can_change_event_settings", "settings/invoice/preview", 200),
("can_change_event_settings", "settings/tax/", 200),
("can_change_event_settings", "settings/tax/1/", 404),

View File

@@ -90,6 +90,7 @@ def logged_in_client(client, event):
('/control/event/{orga}/{event}/settings/permissions', 200),
('/control/event/{orga}/{event}/settings/payment', 200),
('/control/event/{orga}/{event}/settings/tickets', 200),
('/control/event/{orga}/{event}/settings/widget', 200),
# ('/control/event/{orga}/{event}/settings/tickets/preview/(?P<output>[^/]+)', 200),
('/control/event/{orga}/{event}/settings/email', 200),
('/control/event/{orga}/{event}/settings/invoice', 200),

View File

@@ -26,9 +26,10 @@ class CartTestMixin:
self.event = Event.objects.create(
organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
live=True
live=True,
plugins="pretix.plugins.banktransfer"
)
self.tr19 = self.event.tax_rules.create(rate=19)
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0)
self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2)
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12,

View File

@@ -0,0 +1,286 @@
import datetime
import json
from decimal import Decimal
from bs4 import BeautifulSoup
from django.conf import settings
from django.test import TestCase
from django.utils.timezone import now
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"),
payment_provider='banktransfer',
locale='en'
)
self.ticket_pos = OrderPosition.objects.create(
order=self.order,
item=self.ticket,
variation=None,
price=Decimal("23"),
attendee_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' % (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/' % (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/' % (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/' % (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 == {
"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"},
"picture": None,
"has_variations": 0,
"description": None,
"min_price": None,
"avail": [100, None],
"variations": [],
"id": self.ticket.pk,
"free_price": False,
"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"},
"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"},
"description": None,
"avail": [100, None],
"order_max": 2
}
],
"id": self.shirt.pk,
"free_price": False,
"name": "T-Shirt",
"order_max": None
}
],
"description": None,
"id": self.category.pk,
"name": "Everything"
}
],
"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 == {
"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"},
"picture": None,
"has_variations": 0,
"description": None,
"min_price": None,
"avail": [100, None],
"variations": [],
"id": self.ticket.pk,
"free_price": False,
"name": "Early-bird ticket",
"order_max": 4
},
],
"description": None,
"id": self.category.pk,
"name": "Everything"
}
],
"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 == {
"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
}
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 '#8E44B3' 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 '#8E44B3' 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 '#8E44B3' 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