Allow to pass user data to the widget (#1095)

- [x] Logic
- [x] Tests
- [x] Docs
- [x] find a way to integrate with tracking
This commit is contained in:
Raphael Michel
2018-11-20 17:55:37 +01:00
committed by GitHub
parent b49b2035bd
commit beb0ded6dc
10 changed files with 335 additions and 29 deletions

View File

@@ -750,7 +750,7 @@ class Question(LoggedModel):
@staticmethod
def _clean_identifier(event, code, instance=None):
qs = Question.objects.filter(event=event, identifier=code)
qs = Question.objects.filter(event=event, identifier__iexact=code)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists():

View File

@@ -4,6 +4,7 @@ from decimal import Decimal
from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.dispatch import receiver
@@ -17,9 +18,11 @@ from pretix.base.models import (
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.rich_text import rich_text
from pretix.celery_app import app
from pretix.presale.signals import (
@@ -99,7 +102,7 @@ class CartManager:
AddOperation: 30
}
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None):
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None):
self.event = event
self.cart_id = cart_id
self.now_dt = now()
@@ -111,6 +114,7 @@ class CartManager:
self._variations_cache = {}
self._expiry = None
self.invoice_address = invoice_address
self._widget_data = widget_data or {}
@property
def positions(self):
@@ -605,12 +609,37 @@ class CartManager:
if isinstance(op, self.AddOperation):
for k in range(available_count):
new_cart_positions.append(CartPosition(
cp = CartPosition(
event=self.event, item=op.item, variation=op.variation,
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
subevent=op.subevent, includes_tax=op.includes_tax
))
)
if self.event.settings.attendee_names_asked:
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
if 'attendee-name' in self._widget_data:
cp.attendee_name_parts = {'_legacy': self._widget_data['attendee-name']}
if any('attendee-name-{}'.format(k.replace('_', '-')) in self._widget_data for k, l, w
in scheme['fields']):
cp.attendee_name_parts = {
k: self._widget_data.get('attendee-name-{}'.format(k.replace('_', '-')), '')
for k, l, w in scheme['fields']
}
if self.event.settings.attendee_emails_asked and 'email' in self._widget_data:
cp.attendee_email = self._widget_data.get('email')
cp._answers = {}
for k, v in self._widget_data.items():
if not k.startswith('question-'):
continue
q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first()
if q:
try:
cp._answers[q] = q.clean_answer(v)
except ValidationError:
pass
new_cart_positions.append(cp)
elif isinstance(op, self.ExtendOperation):
if available_count == 1:
op.position.expires = self._expiry
@@ -621,7 +650,11 @@ class CartManager:
else:
raise AssertionError("ExtendOperation cannot affect more than one item")
CartPosition.objects.bulk_create(new_cart_positions)
for p in new_cart_positions:
if p._answers:
p.save()
_save_answers(p, {}, p._answers)
CartPosition.objects.bulk_create([p for p in new_cart_positions if not p._answers])
return err
def commit(self):
@@ -702,7 +735,7 @@ def get_fees(event, request, total, invoice_address, provider):
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None) -> None:
invoice_address: int=None, widget_data=None) -> None:
"""
Adds a list of items to a user's cart.
:param event: The event ID in question
@@ -722,7 +755,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia)
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data)
cm.add_new_items(items)
cm.commit()
except LockTimeoutException:

View File

@@ -309,7 +309,10 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@cached_property
def contact_form(self):
initial = {
'email': self.cart_session.get('email', '')
'email': (
self.cart_session.get('email', '') or
self.cart_session.get('widget_data', {}).get('email', '')
)
}
initial.update(self.cart_session.get('contact_form_data', {}))
return ContactForm(data=self.request.POST if self.request.method == "POST" else None,

View File

@@ -237,6 +237,9 @@
<div class="clearfix"></div>
</div>
{% endif %}
{% if "widget_data" in request.GET %}
<input type="hidden" value="{{ request.GET.widget_data }}" name="widget_data">
{% endif %}
</form>
{% endif %}
{% endblock %}

View File

@@ -1,8 +1,10 @@
import json
import mimetypes
import os
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
from django.db.models import Q
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect
@@ -33,6 +35,11 @@ from pretix.presale.views.event import (
)
from pretix.presale.views.robots import NoSearchIndexViewMixin
try:
widget_data_cache = caches['redis']
except:
widget_data_cache = caches['default']
class CartActionMixin:
@@ -266,11 +273,20 @@ def get_or_create_cart_id(request, create=True):
cart_data = {}
if prefix and 'take_cart_id' in request.GET and current_id:
new_id = current_id
cached_widget_data = widget_data_cache.get('widget_data_{}'.format(current_id))
if cached_widget_data:
cart_data['widget_data'] = cached_widget_data
else:
if not create:
return None
new_id = generate_cart_id(request, prefix=prefix)
if 'widget_data' not in cart_data and 'widget_data' in request.GET:
try:
cart_data['widget_data'] = json.loads(request.GET.get('widget_data'))
except ValueError:
pass
if 'carts' not in request.session:
request.session['carts'] = {}
if new_id not in request.session['carts']:
@@ -349,10 +365,24 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
}
def post(self, request, *args, **kwargs):
cart_id = get_or_create_cart_id(self.request)
if "widget_data" in request.POST:
try:
widget_data = json.loads(request.POST.get("widget_data", "{}"))
except ValueError:
widget_data = {}
else:
widget_data_cache.set('widget_data_{}'.format(cart_id), widget_data, 600)
cs = cart_session(request)
cs['widget_data'] = widget_data
else:
cs = cart_session(request)
widget_data = cs.get('widget_data', {})
items = self._items_from_post_data()
if items:
return self.do(self.request.event.id, items, get_or_create_cart_id(self.request), translation.get_language(),
self.invoice_address.pk)
return self.do(self.request.event.id, items, cart_id, translation.get_language(),
self.invoice_address.pk, widget_data)
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
@@ -449,6 +479,10 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
def get(self, request, *args, **kwargs):
if 'iframe' in request.GET and 'require_cookie' not in request.GET:
return redirect(request.get_full_path() + '&require_cookie=1')
if len(self.request.GET.get('widget_data', '{}')) > 3:
# We've been passed data from a widget, we need to create a cart session to store it.
get_or_create_cart_id(request)
return super().get(request, *args, **kwargs)

View File

@@ -189,6 +189,9 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
return redirect(eventreverse(request.event, 'presale:event.index', kwargs=kwargs) + '?require_cookie=true&cart_id={}'.format(
request.GET.get('take_cart_id')
))
elif request.GET.get('iframe', '') == '1' and len(self.request.GET.get('widget_data', '{}')) > 3:
# We've been passed data from a widget, we need to create a cart session to store it.
get_or_create_cart_id(request)
elif 'require_cookie' in request.GET and settings.SESSION_COOKIE_NAME not in request.COOKIES:
# Cookies are in fact not supported
r = render(request, 'pretixpresale/event/cookies.html', {

View File

@@ -3,6 +3,11 @@
/* This is embedded in an isolation wrapper that exposes siteglobals as the global
scope. */
window.PretixWidget = {
'build_widgets': true,
'widget_data': {}
};
var Vue = module.exports;
var strings = {
@@ -457,6 +462,9 @@ var shared_methods = {
return;
}
var redirect_url = this.$root.voucherFormTarget + '&voucher=' + this.voucher + '&subevent=' + this.$root.subevent;
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + escape(this.$root.widget_data_json);
}
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
@@ -466,6 +474,9 @@ var shared_methods = {
if (this.$root.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + escape(this.$root.widget_data_json);
}
if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
@@ -572,6 +583,7 @@ Vue.component('pretix-widget', {
+ '<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" />'
+ '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />'
+ '<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">'
@@ -618,6 +630,7 @@ Vue.component('pretix-button', {
+ '<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" />'
+ '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />'
+ '<input type="hidden" v-for="item in $root.items" :name="item.item" :value="item.count" />'
+ '<button class="pretix-button" @click="buy">{{ $root.button_text }}</button>'
+ '</form>'
@@ -733,6 +746,9 @@ var shared_root_computed = {
}
}
return has_priced || cnt_items > 1;
},
widget_data_json: function () {
return JSON.stringify(this.widget_data);
}
};
@@ -757,6 +773,23 @@ var create_overlay = function (app) {
app.$root.overlay = framechild;
};
function get_ga_client_id(tracking_id) {
if (typeof ga === "undefined") {
return null;
}
try {
var trackers = ga.getAll();
var i, len;
for (i = 0, len = trackers.length; i < len; i += 1) {
if (trackers[i].get('trackingId') === tracking_id) {
return trackers[i].get('clientId');
}
}
} catch (e) {
}
return null;
}
var create_widget = function (element) {
var event_url = element.attributes.event.value;
if (!event_url.match(/\/$/)) {
@@ -766,6 +799,13 @@ var create_widget = function (element) {
var subevent = element.attributes.subevent ? element.attributes.subevent.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));
for (var i = 0; i < element.attributes.length; i++) {
var attrib = element.attributes[i];
if (attrib.name.match(/^data-.*$/)) {
widget_data[attrib.name.replace(/^data-/, '')] = attrib.value;
}
}
if (element.tagName !== "pretix-widget") {
element.innerHTML = "<pretix-widget></pretix-widget>";
@@ -786,6 +826,7 @@ var create_widget = function (element) {
skip_ssl: skip_ssl,
error: null,
display_add_to_cart: false,
widget_data: widget_data,
loading: 1,
widget_id: 'pretix-widget-' + widget_id,
vouchers_exist: false,
@@ -815,6 +856,13 @@ var create_button = function (element) {
var raw_items = element.attributes.items ? element.attributes.items.value : "";
var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
var button_text = element.innerHTML;
var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
for (var i = 0; i < element.attributes.length; i++) {
var attrib = element.attributes[i];
if (attrib.name.match(/^data-.*$/)) {
widget_data[attrib.name.replace(/^data-/, '')] = attrib.value;
}
}
if (element.tagName !== "pretix-button") {
element.innerHTML = "<pretix-button>" + element.innerHTML + "</pretix-button>";
@@ -840,6 +888,7 @@ var create_button = function (element) {
voucher_code: voucher,
items: items,
error: null,
widget_data: widget_data,
widget_id: 'pretix-widget-' + widget_id,
button_text: button_text
}
@@ -856,27 +905,35 @@ var create_button = function (element) {
/* Find all widgets on the page and render them */
widgetlist = [];
buttonlist = [];
document.createElement("pretix-widget");
document.createElement("pretix-button");
docReady(function () {
var widgets = document.querySelectorAll("pretix-widget, div.pretix-widget-compat");
var wlength = widgets.length;
for (var i = 0; i < wlength; i++) {
var widget = widgets[i];
widgetlist.push(create_widget(widget));
}
window.PretixWidget.buildWidgets = function () {
document.createElement("pretix-widget");
document.createElement("pretix-button");
docReady(function () {
var widgets = document.querySelectorAll("pretix-widget, div.pretix-widget-compat");
var wlength = widgets.length;
for (var i = 0; i < wlength; i++) {
var widget = widgets[i];
widgetlist.push(create_widget(widget));
}
var buttons = document.querySelectorAll("pretix-button, div.pretix-button-compat");
var blength = buttons.length;
for (var i = 0; i < blength; i++) {
var button = buttons[i];
buttonlist.push(create_button(button));
}
});
var buttons = document.querySelectorAll("pretix-button, div.pretix-button-compat");
var blength = buttons.length;
for (var i = 0; i < blength; i++) {
var button = buttons[i];
buttonlist.push(create_button(button));
}
});
};
if (typeof window.pretixWidgetCallback !== "undefined") {
window.pretixWidgetCallback();
}
if (window.PretixWidget.build_widgets) {
window.PretixWidget.buildWidgets();
}
/* Set a global variable for debugging. In DEBUG mode, siteglobals will be window, otherwise it will be something
unnamed. */
siteglobals.pretixwidget = {
siteglobals.pretixwidget_debug = {
'Vue': Vue,
'widgets': widgetlist,
'buttons': buttonlist