Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
48c110b32b Allow the automatic creation of customer accounts for guest users 2022-12-21 15:02:51 +01:00
17 changed files with 234 additions and 185 deletions

View File

@@ -87,18 +87,6 @@ website. If you confident to have a good reason for not using SSL, you can overr
<pretix-widget event="https://pretix.eu/demo/democon/" skip-ssl-check></pretix-widget>
Seating plans
-------------
By default, events with seating plans just show a button that opens the seating plan. You can also have the seating
plan embedded into the widget directly by using::
<pretix-widget event="https://pretix.eu/demo/democon/" seating-embedded></pretix-widget>
Note that the seating plan will only be embedded if the widget has enough space (currently min. 992 pixels width, may change
in the future) and that the seating plan part of the widget can unfortunately *not* be styled with CSS like the rest of
the widget.
Always open a new tab
---------------------

View File

@@ -59,10 +59,10 @@ class RelativeDateWrapper:
def date(self, event) -> datetime.date:
from .models import SubEvent
if isinstance(self.data, datetime.datetime):
return self.data.date()
elif isinstance(self.data, datetime.date):
if isinstance(self.data, datetime.date):
return self.data
elif isinstance(self.data, datetime.datetime):
return self.data.date()
else:
if self.data.minutes_before is not None:
raise ValueError('A minute-based relative datetime can not be used as a date')

View File

@@ -64,9 +64,9 @@ from pretix.base.i18n import (
LazyLocaleException, get_language_without_region, language,
)
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
Voucher,
CartPosition, Customer, Device, Event, GiftCard, Item, ItemVariation,
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import (
@@ -849,7 +849,7 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None, sales_channel: str='web', shown_total=None,
customer=None):
customer=None, customer_attached=False):
payments = []
sales_channel = get_all_sales_channels()[sales_channel]
@@ -931,6 +931,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
if meta_info:
for msg in meta_info.get('confirm_messages', []):
order.log_action('pretix.event.order.consent', data={'msg': msg})
if customer and customer_attached:
customer.log_action('pretix.customer.order.attached', data={'order': order.code})
order_placed.send(event, order=order)
return order, payments
@@ -981,9 +983,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if not p['pprov']:
raise OrderError(error_messages['internal'])
if customer:
customer = event.organizer.customers.get(pk=customer)
if email == settings.PRETIX_EMAIL_NONE_VALUE:
email = None
@@ -995,6 +994,30 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
except InvoiceAddress.DoesNotExist:
pass
customer_attached = False
if customer:
customer = event.organizer.customers.get(pk=customer)
elif event.settings.customer_accounts_link_by_email in ('attach', 'create') and email:
try:
customer = event.organizer.customers.get(email__iexact=email)
customer_attached = True
except Customer.MultipleObjectsReturned:
logger.warning(f'Multiple customer accounts found for {email}, not attaching.')
except Customer.DoesNotExist:
if event.settings.customer_accounts_link_by_email == 'create':
customer = event.organizer.customers.create(
email=email,
name_parts=addr.name_parts if addr else None,
phone=(meta_info or {}).get('contact_form_data', {}).get('phone'),
is_active=True,
is_verified=False,
locale=locale,
)
customer.set_unusable_password()
customer.save()
customer.log_action('pretix.customer.created.auto', {})
customer_attached = True
requires_seat = Exists(
SeatCategoryMapping.objects.filter(
Q(product=OuterRef('item'))
@@ -1018,7 +1041,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
locale=locale,
invoice_address=addr,
meta_info=meta_info,
customer=customer,
customer=customer if not customer_attached else None,
)
lockfn = NoLockManager
@@ -1041,10 +1064,11 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel,
customer=customer if not customer_attached else None)
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer)
shown_total=shown_total, customer=customer, customer_attached=customer_attached)
try:
for p in payment_objs:
if p.provider == 'free':

View File

@@ -159,13 +159,37 @@ DEFAULTS = {
},
'customer_accounts_link_by_email': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('False', _('Keep guest orders separate and do not link them to customer accounts.')),
('True', _('Do not attach guest orders to customer accounts, but show them in customer '
'accounts with a verified matching email address.')),
('attach', _('Attach guest orders to existing customer accounts with a matching email address if such '
'an account exists.')),
('create', _('Attach guest orders to existing customer accounts with a matching email and '
'automatically create a customer account for all others.')),
('forbidden', _('Do not allow guest orders.')),
),
),
'form_kwargs': dict(
label=_("Match orders based on email address"),
help_text=_("This will allow registered customers to access orders made with the same email address, even if the customer "
"was not logged in during the purchase.")
label=_("Guest order handling"),
widget=forms.RadioSelect,
choices=(
('False', _('Keep guest orders separate and do not link them to customer accounts.')),
('True', _('Do not attach guest orders to customer accounts, but show them in customer '
'accounts with a verified matching email address.')),
('attach', _('Attach guest orders to existing customer accounts with a matching email address if '
'such an account exists.')),
('create', _('Attach guest orders to existing customer accounts with a matching email address and '
'automatically create a customer account for all others.')),
('forbidden', _('Do not allow guest orders.')),
),
help_text=_('Please be aware that creating customer accounts without a users consent might not be a '
'good idea for privacy reasons in some jurisdictions or situations. We always recommend to '
'let the user choose if they want an account.'),
)
},
'max_items_per_order': {

View File

@@ -331,7 +331,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.membershiptype.changed': _('The membership type has been changed.'),
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
'pretix.customer.created': _('The account has been created.'),
'pretix.customer.created.auto': _('The account has been automatically created during an order.'),
'pretix.customer.claimed': _('The automatically created account has been claimed by the customer.'),
'pretix.customer.changed': _('The account has been changed.'),
'pretix.customer.order.attached': _('An order was automatically attached to the customer.'),
'pretix.customer.membership.created': _('A membership for this account has been added.'),
'pretix.customer.membership.changed': _('A membership of this account has been changed.'),
'pretix.customer.membership.deleted': _('A membership of this account has been deleted.'),

View File

@@ -79,9 +79,9 @@
</a>
</td>
<td>
{% if not c.is_verified %}<strike>{% endif %}
{% if not c.is_verified %}<span class="text-muted">{% endif %}
{{ c.email|default_if_none:"" }}
{% if not c.is_verified %}</strike>{% endif %}
{% if not c.is_verified %}</span>{% endif %}
</td>
<td>{{ c.name }}</td>
<td>{% if c.external_identifier %}{{ c.external_identifier }}{% endif %}</td>

View File

@@ -2215,9 +2215,9 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
def get_queryset(self):
q = Q(customer=self.customer)
if self.request.organizer.settings.customer_accounts_link_by_email and self.customer.email:
if self.request.organizer.settings.customer_accounts_link_by_email == 'True' and self.customer.email:
# This is safe because we only let customers with verified emails log in
q |= Q(email__iexact=self.customer.email)
q |= Q(customer__isnull=True, email__iexact=self.customer.email)
qs = Order.objects.filter(
q
).select_related('event').order_by('-datetime')

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-14 13:09+0000\n"
"PO-Revision-Date: 2022-12-21 20:00+0000\n"
"Last-Translator: Fazenda Dengo <fazendadengo@gmail.com>\n"
"PO-Revision-Date: 2021-09-27 06:00+0000\n"
"Last-Translator: Diego Rodrigo <diegorodrigo90@gmail.com>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n"
"Language: pt_BR\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.15\n"
"X-Generator: Weblate 4.8\n"
#: pretix/api/auth/devicesecurity.py:28
msgid ""
@@ -9268,8 +9268,10 @@ msgstr "Nome do evento"
#: pretix/base/settings.py:3031 pretix/base/settings.py:3045
#: pretix/base/settings.py:3096 pretix/base/settings.py:3114
#: pretix/base/settings.py:3133
#, fuzzy
#| msgid "Full name"
msgid "Family name"
msgstr "Sobrenome"
msgstr "Nome completo"
#: pretix/base/settings.py:2943 pretix/base/settings.py:2959
#: pretix/base/settings.py:2975 pretix/base/settings.py:2990

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-14 13:09+0000\n"
"PO-Revision-Date: 2022-12-21 20:00+0000\n"
"Last-Translator: Fazenda Dengo <fazendadengo@gmail.com>\n"
"PO-Revision-Date: 2022-11-28 19:03+0000\n"
"Last-Translator: Vasco Baleia <vb2003.12@gmail.com>\n"
"Language-Team: Portuguese (Portugal) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_PT/>\n"
"Language: pt_PT\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.15\n"
"X-Generator: Weblate 4.14.1\n"
#: pretix/api/auth/devicesecurity.py:28
msgid ""
@@ -9560,7 +9560,7 @@ msgstr "Nome próprio"
#: pretix/base/settings.py:3096 pretix/base/settings.py:3114
#: pretix/base/settings.py:3133
msgid "Family name"
msgstr "Sobrenome"
msgstr "Apelido"
#: pretix/base/settings.py:2943 pretix/base/settings.py:2959
#: pretix/base/settings.py:2975 pretix/base/settings.py:2990

View File

@@ -269,7 +269,7 @@ class CustomerStep(CartMixin, TemplateFlowStep):
@cached_property
def guest_allowed(self):
return not any(
return self.request.event.settings.customer_accounts_link_by_email != 'forbidden' and not any(
p.item.require_membership or
(p.variation and p.variation.require_membership) or
p.item.grant_membership_type_id

View File

@@ -144,6 +144,7 @@ class RegistrationForm(forms.Form):
self.standalone = kwargs.pop('standalone')
self.signer = signing.TimestampSigner(salt=f'customer-registration-captcha-{get_client_ip(request)}')
super().__init__(*args, **kwargs)
self.instance = None
event = getattr(request, "event", None)
if event and event.settings.order_phone_asked:
@@ -214,14 +215,17 @@ class RegistrationForm(forms.Form):
if email is not None:
try:
self.request.organizer.customers.get(email=email.lower())
existing = self.request.organizer.customers.get(email=email.lower())
except Customer.DoesNotExist:
pass
else:
raise forms.ValidationError(
{'email': self.error_messages['duplicate']},
code='duplicate',
)
if not existing.is_active or existing.is_verified or existing.has_usable_password():
raise forms.ValidationError(
{'email': self.error_messages['duplicate']},
code='duplicate',
)
else:
self.instance = existing
if self.standalone:
expect = -1
@@ -256,19 +260,29 @@ class RegistrationForm(forms.Form):
return self.cleaned_data
def create(self):
customer = self.request.organizer.customers.create(
email=self.cleaned_data['email'],
name_parts=self.cleaned_data['name_parts'],
phone=self.cleaned_data.get('phone'),
is_active=True,
is_verified=False,
locale=get_language_without_region(),
)
customer.set_unusable_password()
customer.save()
customer.log_action('pretix.customer.created', {})
customer.send_activation_mail()
return customer
if self.instance:
self.instance.name_parts = self.cleaned_data['name_parts']
if self.cleaned_data.get('phone'):
self.instance.phone = self.cleaned_data.get('phone')
self.instance.locale = get_language_without_region()
self.instance.save()
self.instance.log_action('pretix.customer.claimed', {})
self.instance.send_activation_mail()
return self.instance
else:
customer = self.request.organizer.customers.create(
email=self.cleaned_data['email'],
name_parts=self.cleaned_data['name_parts'],
phone=self.cleaned_data.get('phone'),
is_active=True,
is_verified=False,
locale=get_language_without_region(),
)
customer.set_unusable_password()
customer.save()
customer.log_action('pretix.customer.created', {})
customer.send_activation_mail()
return customer
class SetPasswordForm(forms.Form):

View File

@@ -352,9 +352,9 @@ class ProfileView(CustomerRequiredMixin, ListView):
def get_queryset(self):
q = Q(customer=self.request.customer)
if self.request.organizer.settings.customer_accounts_link_by_email and self.request.customer.email:
if self.request.organizer.settings.customer_accounts_link_by_email == 'True' and self.request.customer.email:
# This is safe because we only let customers with verified emails log in
q |= Q(email__iexact=self.request.customer.email)
q |= Q(customer__isnull=True, email__iexact=self.request.customer.email)
qs = Order.objects.filter(
q
).prefetch_related(

View File

@@ -663,15 +663,11 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
return context
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class SeatingPlanView(EventViewMixin, TemplateView):
template_name = "pretixpresale/event/seatingplan.html"
def dispatch(self, request, *args, **kwargs):
r = super().dispatch(request, *args, **kwargs)
r.xframe_options_exempt = True
return r
def get(self, request, *args, **kwargs):
from pretix.presale.views.cart import get_or_create_cart_id

View File

@@ -141,21 +141,18 @@ var api = {
},
'_postFormJSON': function (endpoint, form, callback, err_callback) {
var params;
if (Array.isArray(form)) {
params = form
} else {
params = [].filter.call(form.elements, function (el) {
return (el.type !== 'checkbox' && el.type !== 'radio') || el.checked;
}).filter(function (el) {
return !!el.name && !!el.value;
}).filter(function (el) {
var params = [].filter.call(form.elements, function (el) {
return (el.type !== 'checkbox' && el.type !== 'radio') || el.checked;
})
.filter(function (el) {
return !!el.name && !!el.value;
})
.filter(function (el) {
return !el.disabled;
})
}
params = params.map(function (el) {
return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
}).join('&');
.map(function (el) {
return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
}).join('&');
var xhr = api._getXHR();
xhr.open("POST", endpoint, true);
@@ -364,7 +361,6 @@ Vue.component('variation', {
template: ('<div class="pretix-widget-variation">'
+ '<div class="pretix-widget-item-row">'
// Variation description
+ '<div class="pretix-widget-item-info-col">'
+ '<div class="pretix-widget-item-title-and-description">'
+ '<strong class="pretix-widget-item-title">{{ variation.value }}</strong>'
@@ -376,15 +372,12 @@ Vue.component('variation', {
+ '</div>'
+ '</div>'
// Price
+ '<div class="pretix-widget-item-price-col">'
+ '<pricebox :price="variation.price" :free_price="item.free_price" :original_price="orig_price"'
+ ' :field_name="\'price_\' + item.id + \'_\' + variation.id" v-if="$root.showPrices">'
+ '</pricebox>'
+ '<span v-if="!$root.showPrices">&nbsp;</span>'
+ '</div>'
// Availability
+ '<div class="pretix-widget-item-availability-col">'
+ '<availbox :item="item" :variation="variation"></availbox>'
+ '</div>'
@@ -412,7 +405,6 @@ Vue.component('item', {
template: ('<div v-bind:class="classObject">'
+ '<div class="pretix-widget-item-row pretix-widget-main-item-row">'
// Product description
+ '<div class="pretix-widget-item-info-col">'
+ '<img :src="item.picture" v-if="item.picture" class="pretix-widget-item-picture">'
+ '<div class="pretix-widget-item-title-and-description">'
@@ -432,7 +424,6 @@ Vue.component('item', {
+ '</div>'
+ '</div>'
// Price
+ '<div class="pretix-widget-item-price-col">'
+ '<pricebox :price="item.price" :free_price="item.free_price" v-if="!item.has_variations && $root.showPrices"'
+ ' :field_name="\'price_\' + item.id" :original_price="item.original_price">'
@@ -440,8 +431,6 @@ Vue.component('item', {
+ '<div class="pretix-widget-pricebox" v-if="item.has_variations && $root.showPrices">{{ pricerange }}</div>'
+ '<span v-if="!$root.showPrices">&nbsp;</span>'
+ '</div>'
// Availability
+ '<div class="pretix-widget-item-availability-col">'
+ '<a v-if="show_toggle" href="#" @click.prevent.stop="expand">'+ strings.variations + '</a>'
+ '<availbox v-if="!item.has_variations" :item="item"></availbox>'
@@ -450,7 +439,6 @@ Vue.component('item', {
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
// Variations
+ '<div :class="varClasses" v-if="item.has_variations">'
+ '<variation v-for="variation in item.variations" :variation="variation" :item="item" :key="variation.id">'
+ '</variation>'
@@ -525,7 +513,7 @@ Vue.component('category', {
});
var shared_methods = {
buy: function (event, data) {
buy: function (event) {
if (this.$root.useIframe) {
if (event) {
event.preventDefault();
@@ -544,7 +532,7 @@ var shared_methods = {
this.$root.overlay.frame_loading = true;
this.async_task_interval = 100;
var form = data === undefined ? this.$refs.form : data;
var form = this.$refs.form;
if (form === undefined) {
form = this.$refs.formcomp.$refs.form;
}
@@ -666,7 +654,7 @@ var shared_methods = {
}
},
handleResize: function () {
this.clientWidth = this.$refs.wrapper.clientWidth;
this.mobile = this.$refs.wrapper.clientWidth <= 800;
}
};
@@ -677,7 +665,7 @@ var shared_widget_data = function () {
async_task_timeout: null,
async_task_interval: 100,
voucher: null,
clientWidth: 1000,
mobile: false,
}
};
@@ -773,7 +761,6 @@ Vue.component('pretix-overlay', {
Vue.component('pretix-widget-event-form', {
template: ('<div class="pretix-widget-event-form">'
// Back navigation
+ '<div class="pretix-widget-event-list-back" v-if="$root.events || $root.weeks || $root.days">'
+ '<a href="#" @click.prevent.stop="back_to_list" v-if="!$root.subevent">&lsaquo; '
+ strings['back_to_list']
@@ -782,28 +769,18 @@ Vue.component('pretix-widget-event-form', {
+ strings['back_to_dates']
+ '</a>'
+ '</div>'
// Event name
+ '<div class="pretix-widget-event-header" v-if="$root.events || $root.weeks || $root.days">'
+ '<strong>{{ $root.name }}</strong>'
+ '</div>'
// Date range
+ '<div class="pretix-widget-event-details" v-if="($root.events || $root.weeks || $root.days) && $root.date_range">'
+ '{{ $root.date_range }}'
+ '</div>'
// Form start
+ '<div class="pretix-widget-event-description" v-if="($root.events || $root.weeks || $root.days) && $root.frontpage_text" v-html="$root.frontpage_text"></div>'
+ '<form method="post" :action="$root.formAction" ref="form" :target="$root.formTarget">'
+ '<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" />'
// Error message
+ '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>'
// Resume cart
+ '<div class="pretix-widget-info-message pretix-widget-clickable"'
+ ' v-if="$root.cart_exists">'
+ '<button @click.prevent.stop="$parent.resume" class="pretix-widget-resume-button" type="button">'
@@ -812,20 +789,11 @@ Vue.component('pretix-widget-event-form', {
+ strings['cart_exists']
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
// Seating plan
+ '<div class="pretix-widget-seating-link-wrapper" v-if="$root.has_seating_plan && !show_seating_plan_inline">'
+ '<div class="pretix-widget-seating-link-wrapper" v-if="this.$root.has_seating_plan">'
+ '<button class="pretix-widget-seating-link" @click.prevent.stop="$root.startseating">'
+ strings['show_seating']
+ '</button>'
+ '</div>'
+ '<div class="pretix-widget-seating-embed" v-else-if="$root.has_seating_plan && show_seating_plan_inline">'
+ '<iframe :key="\'seatingframe\' + $root.loadid" class="pretix-widget-seating-embed-iframe" ref="seatingframe"'
+ ' :src="seatingframe" frameborder="0" referrerpolicy="origin" allowtransparency="true">'
+ '</iframe>'
+ '</div>'
// Waiting list for seating plan
+ '<div class="pretix-widget-seating-waitinglist" v-if="this.$root.has_seating_plan && this.$root.has_seating_plan_waitinglist">'
+ '<div class="pretix-widget-seating-waitinglist-text">'
+ strings['seating_plan_waiting_list']
@@ -837,18 +805,11 @@ Vue.component('pretix-widget-event-form', {
+ '</div>'
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
// Actual product list
+ '<category v-for="category in this.$root.categories" :category="category" :key="category.id"></category>'
// Buy button
+ '<div class="pretix-widget-action" v-if="$root.display_add_to_cart">'
+ '<button @click="$parent.buy" type="submit" :disabled="buy_disabled">{{ this.buy_label }}</button>'
+ '</div>'
+ '</form>'
// Voucher form
+ '<form method="get" :action="$root.voucherFormTarget" target="_blank" '
+ ' v-if="$root.vouchers_exist && !$root.disable_vouchers && !$root.voucher_code">'
+ '<div class="pretix-widget-voucher">'
@@ -866,7 +827,6 @@ Vue.component('pretix-widget-event-form', {
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
+ '</form>'
+ '</div>'
),
data: function () {
@@ -878,14 +838,10 @@ Vue.component('pretix-widget-event-form', {
this.$root.$on('amounts_changed', this.calculate_buy_disabled)
this.$root.$on('focus_voucher_field', this.focus_voucher_field)
this.calculate_buy_disabled()
window.addEventListener('message', this.on_seat_select);
},
beforeDestroy: function() {
this.$root.$off('amounts_changed', this.calculate_buy_disabled)
this.$root.$off('focus_voucher_field', this.focus_voucher_field)
window.addEventListener('message', this.on_seat_select);
},
computed: {
buy_label: function () {
@@ -915,26 +871,9 @@ Vue.component('pretix-widget-event-form', {
} else {
return strings.buy;
}
},
show_seating_plan_inline: function () {
return this.$root.seating_embedded && this.$parent.clientWidth > 992;
},
seatingframe: function () {
var seatingframe_url = this.$root.target_url;
if (this.$root.subevent){
seatingframe_url += '/' + this.$root.subevent;
}
seatingframe_url += '/seatingframe/?inline=1&locale=' + lang + '&widget_id=' + this.$root.widgetindex;
return seatingframe_url;
},
}
},
methods: {
on_seat_select: function (ev) {
if (ev.data.source !== "pretix_widget_seating") return;
if (parseInt(ev.data.widget_id) !== this.$root.widgetindex) return; // In case multiple widgets are on this page
if (ev.data.action !== "buy") return;
this.$parent.buy(null, ev.data.data);
},
focus_voucher_field: function() {
this.$refs.voucherinput.scrollIntoView(false)
this.$refs.voucherinput.focus()
@@ -1218,21 +1157,15 @@ Vue.component('pretix-widget-event-calendar-row', {
Vue.component('pretix-widget-event-calendar', {
template: ('<div class="pretix-widget-event-calendar" ref="calendar">'
// Back navigation
+ '<div class="pretix-widget-back" v-if="$root.events !== undefined">'
+ '<a href="#" @click.prevent.stop="back_to_list">&lsaquo; '
+ strings['back']
+ '</a>'
+ '</div>'
// Headline
+ '<div class="pretix-widget-event-header" v-if="$root.parent_stack.length > 0">'
+ '<strong>{{ $root.name }}</strong>'
+ '</div>'
+ '<div class="pretix-widget-event-description" v-if="$root.parent_stack.length > 0 && $root.frontpage_text" v-html="$root.frontpage_text"></div>'
// Calendar navigation
+ '<div class="pretix-widget-event-calendar-head">'
+ '<a class="pretix-widget-event-calendar-previous-month" href="#" @click.prevent.stop="prevmonth">&laquo; '
+ strings['previous_month']
@@ -1242,8 +1175,6 @@ Vue.component('pretix-widget-event-calendar', {
+ strings['next_month']
+ ' &raquo;</a>'
+ '</div>'
// Calendar
+ '<table class="pretix-widget-event-calendar-table">'
+ '<thead>'
+ '<tr>'
@@ -1302,19 +1233,14 @@ Vue.component('pretix-widget-event-calendar', {
Vue.component('pretix-widget-event-week-calendar', {
template: ('<div class="pretix-widget-event-calendar pretix-widget-event-week-calendar" ref="weekcalendar">'
// Back navigation
+ '<div class="pretix-widget-back" v-if="$root.events !== undefined">'
+ '<a href="#" @click.prevent.stop="back_to_list">&lsaquo; '
+ strings['back']
+ '</a>'
+ '</div>'
// Event header
+ '<div class="pretix-widget-event-header" v-if="$root.parent_stack.length > 0">'
+ '<strong>{{ $root.name }}</strong>'
+ '</div>'
// Calendar navigation
+ '<div class="pretix-widget-event-description" v-if="$root.parent_stack.length > 0 && $root.frontpage_text" v-html="$root.frontpage_text"></div>'
+ '<div class="pretix-widget-event-calendar-head">'
+ '<a class="pretix-widget-event-calendar-previous-month" href="#" @click.prevent.stop="prevweek">&laquo; '
@@ -1325,15 +1251,12 @@ Vue.component('pretix-widget-event-week-calendar', {
+ strings['next_week']
+ ' &raquo;</a>'
+ '</div>'
// Actual calendar
+ '<div class="pretix-widget-event-week-table">'
+ '<div class="pretix-widget-event-week-col" v-for="d in $root.days">'
+ '<pretix-widget-event-week-cell :day="d">'
+ '</pretix-widget-event-week-cell>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>'),
computed: {
@@ -1400,12 +1323,9 @@ Vue.component('pretix-widget', {
data: shared_widget_data,
methods: shared_methods,
mounted: function () {
this.clientWidth = this.$refs.wrapper.clientWidth;
this.mobile = this.$refs.wrapper.clientWidth <= 600;
},
computed: {
mobile: function () {
return this.clientWidth <= 600;
},
classObject: function () {
o = {'pretix-widget': true};
if (this.mobile) {
@@ -1568,7 +1488,6 @@ var shared_root_methods = {
root.has_seating_plan_waitinglist = data.has_seating_plan_waitinglist;
root.itemnum = data.itemnum;
}
root.loadid++; // force-reload iframes
root.poweredby = data.poweredby;
if (root.loading > 0) {
root.loading--;
@@ -1707,7 +1626,7 @@ var shared_root_computed = {
},
widget_data_json: function () {
return JSON.stringify(this.widget_data);
},
}
};
var create_overlay = function (app) {
@@ -1749,7 +1668,7 @@ function get_ga_client_id(tracking_id) {
return null;
}
var create_widget = function (element, widgetindex) {
var create_widget = function (element) {
var target_url = element.attributes.event.value;
if (!target_url.match(/\/$/)) {
target_url += "/";
@@ -1758,7 +1677,6 @@ var create_widget = function (element, widgetindex) {
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 seating_embedded = element.attributes["seating-embedded"] ? true : false;
var disable_iframe = element.attributes["disable-iframe"] ? true : false;
var disable_vouchers = element.attributes["disable-vouchers"] ? true : false;
var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
@@ -1781,7 +1699,6 @@ var create_widget = function (element, widgetindex) {
el: element,
data: function () {
return {
widgetindex: widgetindex,
target_url: target_url,
parent_stack: [],
subevent: subevent,
@@ -1803,7 +1720,6 @@ var create_widget = function (element, widgetindex) {
voucher_explanation_text: null,
show_variations_expanded: !!variations,
skip_ssl: skip_ssl,
seating_embedded: seating_embedded,
disable_iframe: disable_iframe,
style: style,
connection_error: false,
@@ -1818,7 +1734,6 @@ var create_widget = function (element, widgetindex) {
display_add_to_cart: false,
widget_data: widget_data,
loading: 1,
loadid: 1,
widget_id: 'pretix-widget-' + widget_id,
vouchers_exist: false,
disable_vouchers: disable_vouchers,
@@ -1919,7 +1834,7 @@ window.PretixWidget.buildWidgets = function () {
var wlength = widgets.length;
for (var i = 0; i < wlength; i++) {
var widget = widgets[i];
widgetlist.push(create_widget(widget, i + 1));
widgetlist.push(create_widget(widget));
}
var buttons = document.querySelectorAll("pretix-button, div.pretix-button-compat");

View File

@@ -331,15 +331,6 @@
width: 100%;
}
.pretix-widget-seating-embed {
margin: 0 -10px;
}
.pretix-widget-seating-embed-iframe {
width: 100%;
aspect-ratio: 2/1;
max-height: 60vh;
}
.pretix-widget-seating-waitinglist {
margin: 15px 0;
}

View File

@@ -4315,6 +4315,56 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
assert order.email == 'admin@localhost'
assert not order.customer
def test_guest_not_allowed(self):
self.orga.settings.customer_accounts_link_by_email = 'forbidden'
response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
target_status_code=200)
response = self.client.post('/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug), {
'customer_mode': 'guest'
}, follow=True)
self.assertEqual(response.status_code, 200)
def test_guest_attach(self):
self.orga.settings.customer_accounts_link_by_email = 'attach'
response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
target_status_code=200)
self.client.post('/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug), {
'customer_mode': 'guest'
}, follow=True)
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'john@example.org'
}, follow=True)
order = self._finish()
assert order.email == 'john@example.org'
assert order.customer == self.customer
def test_guest_create(self):
self.orga.settings.customer_accounts_link_by_email = 'create'
response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
target_status_code=200)
self.client.post('/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug), {
'customer_mode': 'guest'
}, follow=True)
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'jack@example.org'
}, follow=True)
order = self._finish()
assert order.email == 'jack@example.org'
assert order.customer
c = order.customer
assert c.email == order.email
assert not c.has_usable_password()
assert c.is_active
assert not c.is_verified
def test_guest_even_if_logged_in(self):
self.client.post('/%s/account/login' % self.orga.slug, {
'email': 'john@example.org',
@@ -4410,6 +4460,24 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
assert response.status_code == 200
assert b'alert-danger' in response.content
def test_register_valid_account_previously_autocrated(self):
with scopes_disabled():
c = self.orga.customers.create(email='foo@example.com', is_active=True, is_verified=False)
c.set_unusable_password()
c.save()
response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
target_status_code=200)
response = self.client.post('/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug), {
'customer_mode': 'register',
'register-email': 'foo@example.com',
'register-name_parts_0': 'John Doe',
}, follow=False)
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
target_status_code=200)
assert len(djmail.outbox) == 1
def test_register_valid(self):
response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),

View File

@@ -124,16 +124,40 @@ def test_org_register(env, client):
@pytest.mark.django_db
def test_org_register_duplicate_email(env, client):
signer = signing.TimestampSigner(salt='customer-registration-captcha-127.0.0.1')
with scopes_disabled():
env[0].customers.create(email='john@example.org')
r = client.post('/bigevents/account/register', {
'email': 'john@example.org',
'name_parts_0': 'John Doe',
})
'challenge': signer.sign('1+2'),
'response': '3',
}, REMOTE_ADDR='127.0.0.1')
assert b'already registered' in r.content
assert r.status_code == 200
@pytest.mark.django_db
def test_org_register_duplicate_email_ignored_if_previously_autocreated(env, client):
signer = signing.TimestampSigner(salt='customer-registration-captcha-127.0.0.1')
with scopes_disabled():
c = env[0].customers.create(email='john@example.org', is_active=True, is_verified=False)
c.set_unusable_password()
c.save()
r = client.post('/bigevents/account/register', {
'email': 'john@example.org',
'name_parts_0': 'John Doe',
'challenge': signer.sign('1+2'),
'response': '3',
}, REMOTE_ADDR='127.0.0.1')
assert r.status_code == 302
assert len(djmail.outbox) == 1
with scopes_disabled():
customer = env[0].customers.get(email='john@example.org')
assert not customer.is_verified
assert customer.is_active
@pytest.mark.django_db
def test_org_resetpw(env, client):
with scopes_disabled():
@@ -479,7 +503,7 @@ def test_org_order_list(env, client):
assert o2.code not in content
assert o3.code in content
env[0].settings.customer_accounts_link_by_email = True
env[0].settings.customer_accounts_link_by_email = 'True'
r = client.get('/bigevents/account/')
assert r.status_code == 200