forked from CGM_Public/pretix_original
Allow attendees to modify their data (Z#23152886) (#4138)
* Allow attendees to modify their data * Allow attendees to change ticket information * Update src/pretix/control/templates/pretixcontrol/event/settings.html Co-authored-by: Mira <weller@rami.io> * Update src/pretix/presale/views/order.py Co-authored-by: Mira <weller@rami.io> * Update src/pretix/base/services/placeholders.py Co-authored-by: Mira <weller@rami.io> * Tests fix * Fix test --------- Co-authored-by: Mira <weller@rami.io>
This commit is contained in:
@@ -684,8 +684,9 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'locales',
|
'locales',
|
||||||
'locale',
|
'locale',
|
||||||
'region',
|
'region',
|
||||||
'last_order_modification_date',
|
'allow_modifications',
|
||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
'waiting_list_enabled',
|
'waiting_list_enabled',
|
||||||
'waiting_list_auto_disable',
|
'waiting_list_auto_disable',
|
||||||
|
|||||||
@@ -850,6 +850,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.event.settings.allow_modifications not in ("order", "attendee"):
|
||||||
|
return False
|
||||||
|
|
||||||
modify_deadline = self.modify_deadline
|
modify_deadline = self.modify_deadline
|
||||||
if modify_deadline is not None and now() > modify_deadline:
|
if modify_deadline is not None and now() > modify_deadline:
|
||||||
return False
|
return False
|
||||||
@@ -2513,6 +2516,43 @@ class OrderPosition(AbstractPosition):
|
|||||||
reasons[b] = b
|
reasons[b] = b
|
||||||
return reasons
|
return reasons
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_modify_answers(self) -> bool:
|
||||||
|
"""
|
||||||
|
``True`` if the user can change the question answers / attendee names that are
|
||||||
|
related to the position. This checks order status and modification deadlines. It also
|
||||||
|
returns ``False`` if there are no questions that can be answered.
|
||||||
|
"""
|
||||||
|
from .checkin import Checkin
|
||||||
|
|
||||||
|
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.event.settings.allow_modifications != "attendee":
|
||||||
|
return False
|
||||||
|
|
||||||
|
modify_deadline = self.order.modify_deadline
|
||||||
|
if modify_deadline is not None and now() > modify_deadline:
|
||||||
|
return False
|
||||||
|
|
||||||
|
positions = list(
|
||||||
|
self.order.positions.all().annotate(
|
||||||
|
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||||
|
).select_related('item').prefetch_related('item__questions')
|
||||||
|
)
|
||||||
|
if not self.event.settings.allow_modifications_after_checkin:
|
||||||
|
for cp in positions:
|
||||||
|
if cp.has_checkin:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||||
|
for cp in positions:
|
||||||
|
if cp.pk == self.pk or cp.addon_to_id == self.pk:
|
||||||
|
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False # nothing there to modify
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_cart_positions(cls, cp: List, order) -> list:
|
def transform_cart_positions(cls, cp: List, order) -> list:
|
||||||
from . import Voucher
|
from . import Voucher
|
||||||
|
|||||||
@@ -337,6 +337,40 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.modify', kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.modify', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.change', kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.change', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||||
lambda order, event:
|
lambda order, event:
|
||||||
|
|||||||
@@ -1653,6 +1653,28 @@ DEFAULTS = {
|
|||||||
"calendar.")
|
"calendar.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'allow_modifications': {
|
||||||
|
'default': 'order',
|
||||||
|
'type': str,
|
||||||
|
'form_class': forms.ChoiceField,
|
||||||
|
'serializer_class': serializers.ChoiceField,
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
choices=(
|
||||||
|
('no', _('No modifications after order was submitted')),
|
||||||
|
('order', _('Only the person who ordered can make changes')),
|
||||||
|
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Allow customers to modify their information"),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
choices=(
|
||||||
|
('no', _('No modifications after order was submitted')),
|
||||||
|
('order', _('Only the person who ordered can make changes')),
|
||||||
|
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
'allow_modifications_after_checkin': {
|
'allow_modifications_after_checkin': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -1660,6 +1682,8 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Allow customers to modify their information after they checked in."),
|
label=_("Allow customers to modify their information after they checked in."),
|
||||||
|
help_text=_("By default, no more modifications are possible for an order as soon as one of the tickets "
|
||||||
|
"in the order has been checked in.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'last_order_modification_date': {
|
'last_order_modification_date': {
|
||||||
|
|||||||
@@ -580,6 +580,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
|||||||
'banner_text',
|
'banner_text',
|
||||||
'banner_text_bottom',
|
'banner_text_bottom',
|
||||||
'order_email_asked_twice',
|
'order_email_asked_twice',
|
||||||
|
'allow_modifications',
|
||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
'checkout_show_copy_answers_button',
|
'checkout_show_copy_answers_button',
|
||||||
|
|||||||
@@ -113,10 +113,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
|
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
|
||||||
|
|
||||||
<h4>{% trans "Other settings" %}</h4>
|
<h4>{% trans "Form settings" %}</h4>
|
||||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||||
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
||||||
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
|
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
|
||||||
|
|
||||||
|
<h4>{% trans "Changes to existing orders" %}</h4>
|
||||||
|
{% bootstrap_field sform.allow_modifications layout="control" %}
|
||||||
|
<div data-display-dependency='#id_settings-allow_modifications_0' data-inverse>
|
||||||
|
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||||
|
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Texts" %}</legend>
|
<legend>{% trans "Texts" %}</legend>
|
||||||
@@ -225,21 +232,47 @@
|
|||||||
{% bootstrap_field sform.presale_start_show_date layout="control" %}
|
{% bootstrap_field sform.presale_start_show_date layout="control" %}
|
||||||
{% bootstrap_field form.presale_end layout="control" %}
|
{% bootstrap_field form.presale_end layout="control" %}
|
||||||
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
||||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
|
||||||
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
|
||||||
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Display" %}</legend>
|
<legend>{% trans "Display" %}</legend>
|
||||||
|
<h4>{% trans "Date and time" %}</h4>
|
||||||
{% bootstrap_field sform.show_dates_on_frontpage layout="control" %}
|
{% bootstrap_field sform.show_dates_on_frontpage layout="control" %}
|
||||||
{% bootstrap_field sform.show_date_to layout="control" %}
|
{% bootstrap_field sform.show_date_to layout="control" %}
|
||||||
{% bootstrap_field sform.show_times layout="control" %}
|
{% bootstrap_field sform.show_times layout="control" %}
|
||||||
|
<h4>{% trans "Product list" %}</h4>
|
||||||
{% bootstrap_field sform.show_quota_left layout="control" %}
|
{% bootstrap_field sform.show_quota_left layout="control" %}
|
||||||
{% bootstrap_field sform.display_net_prices layout="control" %}
|
{% bootstrap_field sform.display_net_prices layout="control" %}
|
||||||
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
|
|
||||||
{% bootstrap_field sform.show_variations_expanded layout="control" %}
|
{% bootstrap_field sform.show_variations_expanded layout="control" %}
|
||||||
{% bootstrap_field sform.hide_sold_out layout="control" %}
|
{% bootstrap_field sform.hide_sold_out layout="control" %}
|
||||||
|
|
||||||
|
<h4>{% trans "Calendar and list views" context "subevents" %}</h4>
|
||||||
|
{% if sform.frontpage_subevent_ordering %}
|
||||||
|
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_list_type %}
|
||||||
|
{% bootstrap_field sform.event_list_type layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_list_available_only %}
|
||||||
|
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_list_filters %}
|
||||||
|
{% bootstrap_field sform.event_list_filters layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_calendar_future_only %}
|
||||||
|
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
||||||
|
|
||||||
|
|
||||||
|
<h4>{% trans "Order details" %}</h4>
|
||||||
|
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
|
||||||
|
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
||||||
|
|
||||||
|
<h4>{% trans "Other settings" %}</h4>
|
||||||
|
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||||
|
{% propagated request.event org_url "meta_noindex" %}
|
||||||
|
{% bootstrap_field sform.meta_noindex layout="control" %}
|
||||||
|
{% endpropagated %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">
|
<label class="col-md-3 control-label">
|
||||||
{% trans "Footer links" %}<br>
|
{% trans "Footer links" %}<br>
|
||||||
@@ -306,28 +339,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if sform.frontpage_subevent_ordering %}
|
|
||||||
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_list_type %}
|
|
||||||
{% bootstrap_field sform.event_list_type layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_list_available_only %}
|
|
||||||
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_list_filters %}
|
|
||||||
{% bootstrap_field sform.event_list_filters layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_calendar_future_only %}
|
|
||||||
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
|
||||||
|
|
||||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
|
||||||
{% propagated request.event org_url "meta_noindex" %}
|
|
||||||
{% bootstrap_field sform.meta_noindex layout="control" %}
|
|
||||||
{% endpropagated %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Cart" %}</legend>
|
<legend>{% trans "Cart" %}</legend>
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
{% trans "Your items" %}
|
{% trans "Your items" %}
|
||||||
|
{% if position.can_modify_answers %}
|
||||||
|
<a href="{% eventurl event "presale:event.order.position.modify" secret=position.web_secret position=position.positionid order=order.code %}" aria-label="{% trans "Change ordered items" %}" class="h6">
|
||||||
|
<span class="fa fa-edit" aria-hidden="true"></span>{% trans "Change details" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends "pretixpresale/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load rich_text %}
|
||||||
|
{% block title %}{% trans "Modify ticket" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Modify ticket
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
<form class="form-horizontal" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="panel-group" id="questions_accordion">
|
||||||
|
{% for pos, forms in formgroups %}
|
||||||
|
<details class="panel panel-default" open>
|
||||||
|
<summary class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<strong>{{ pos.item.name }}{% if pos.variation %}
|
||||||
|
– {{ pos.variation }}
|
||||||
|
{% endif %}</strong>
|
||||||
|
</h4>
|
||||||
|
</summary>
|
||||||
|
<div id="cp{{ pos.id }}">
|
||||||
|
<div class="panel-body questions-form">
|
||||||
|
{% for form in forms %}
|
||||||
|
{% if form.pos.item != pos.item %}
|
||||||
|
{# Add-Ons #}
|
||||||
|
<legend>+ {{ form.pos.item.name }}{% if form.pos.variation %}
|
||||||
|
– {{ form.pos.variation.value }}
|
||||||
|
{% endif %}</legend>
|
||||||
|
{% endif %}
|
||||||
|
{% bootstrap_form form layout="checkout" %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="row checkout-button-row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a class="btn btn-block btn-default btn-lg"
|
||||||
|
href="{{ view.get_position_url }}">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||||
|
{% trans "Save changes" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -158,6 +158,9 @@ event_patterns = [
|
|||||||
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/change$',
|
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/change$',
|
||||||
pretix.presale.views.order.OrderPositionChange.as_view(),
|
pretix.presale.views.order.OrderPositionChange.as_view(),
|
||||||
name='event.order.position.change'),
|
name='event.order.position.change'),
|
||||||
|
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/modify$',
|
||||||
|
pretix.presale.views.order.OrderPositionModify.as_view(),
|
||||||
|
name='event.order.position.modify'),
|
||||||
|
|
||||||
re_path(r'^ical/?$',
|
re_path(r'^ical/?$',
|
||||||
pretix.presale.views.event.EventIcalDownload.as_view(),
|
pretix.presale.views.event.EventIcalDownload.as_view(),
|
||||||
|
|||||||
@@ -858,6 +858,78 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||||
|
class OrderPositionModify(EventViewMixin, OrderPositionDetailMixin, OrderQuestionsViewMixin, TemplateView):
|
||||||
|
form_class = QuestionsForm
|
||||||
|
invoice_form_class = None
|
||||||
|
template_name = "pretixpresale/event/position_modify.html"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def invoice_form(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def positions(self):
|
||||||
|
return [p for p in super().positions if p.pk == self.position.pk or p.addon_to_id == self.position.pk]
|
||||||
|
|
||||||
|
def get_question_override_sets(self, order_position, index):
|
||||||
|
override_sets = [
|
||||||
|
resp for recv, resp in question_form_fields_overrides.send(
|
||||||
|
self.request.event,
|
||||||
|
position=order_position,
|
||||||
|
request=self.request
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for override in override_sets:
|
||||||
|
for k in override:
|
||||||
|
# We don't want initial values to be modified, they should come from the order directly
|
||||||
|
override[k].pop('initial', None)
|
||||||
|
|
||||||
|
if order_position.used_membership and not order_position.used_membership.membership_type.transferable:
|
||||||
|
override_sets.append({
|
||||||
|
'attendee_name_parts': {
|
||||||
|
'disabled': True
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return override_sets
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
failed = not self.save()
|
||||||
|
if failed:
|
||||||
|
messages.error(self.request,
|
||||||
|
_("We had difficulties processing your input. Please review the errors below."))
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
self.order.log_action('pretix.event.order.modified', {
|
||||||
|
'by_ticket_holder': True,
|
||||||
|
'data': [{
|
||||||
|
k: (f.cleaned_data.get(k).name
|
||||||
|
if isinstance(f.cleaned_data.get(k), File)
|
||||||
|
else f.cleaned_data.get(k))
|
||||||
|
for k in f.changed_data
|
||||||
|
} for f in self.forms]
|
||||||
|
})
|
||||||
|
order_modified.send(sender=self.request.event, order=self.order)
|
||||||
|
|
||||||
|
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
|
||||||
|
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||||
|
CachedCombinedTicket.objects.filter(order=self.order).delete()
|
||||||
|
return redirect(self.get_position_url())
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
self.kwargs = kwargs
|
||||||
|
if not self.position:
|
||||||
|
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||||
|
if not self.position.can_modify_answers:
|
||||||
|
messages.error(request, _('You cannot modify this order'))
|
||||||
|
return redirect(self.get_position_url())
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||||
class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
|
class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||||
template_name = "pretixpresale/event/order_cancel.html"
|
template_name = "pretixpresale/event/order_cancel.html"
|
||||||
|
|||||||
@@ -1263,22 +1263,34 @@ class OrderTestCase(BaseQuotaTestCase):
|
|||||||
self.event.settings.set('invoice_address_asked', False)
|
self.event.settings.set('invoice_address_asked', False)
|
||||||
self.event.settings.set('attendee_names_asked', True)
|
self.event.settings.set('attendee_names_asked', True)
|
||||||
assert self.order.can_modify_answers
|
assert self.order.can_modify_answers
|
||||||
|
assert not self.op1.can_modify_answers
|
||||||
|
|
||||||
|
self.event.settings.set('allow_modifications', 'attendee')
|
||||||
|
assert self.op1.can_modify_answers
|
||||||
|
|
||||||
self.event.settings.set('attendee_names_asked', False)
|
self.event.settings.set('attendee_names_asked', False)
|
||||||
assert not self.order.can_modify_answers
|
assert not self.order.can_modify_answers
|
||||||
|
assert not self.op1.can_modify_answers
|
||||||
self.event.settings.set('invoice_address_asked', True)
|
self.event.settings.set('invoice_address_asked', True)
|
||||||
assert self.order.can_modify_answers
|
assert self.order.can_modify_answers
|
||||||
|
assert not self.op1.can_modify_answers
|
||||||
self.event.settings.set('invoice_address_asked', False)
|
self.event.settings.set('invoice_address_asked', False)
|
||||||
self.event.settings.set('invoice_name_required', True)
|
self.event.settings.set('invoice_name_required', True)
|
||||||
assert self.order.can_modify_answers
|
assert self.order.can_modify_answers
|
||||||
|
assert not self.op1.can_modify_answers
|
||||||
q = Question.objects.create(question='Foo', type=Question.TYPE_BOOLEAN, event=self.event)
|
q = Question.objects.create(question='Foo', type=Question.TYPE_BOOLEAN, event=self.event)
|
||||||
self.item1.questions.add(q)
|
self.item1.questions.add(q)
|
||||||
assert self.order.can_modify_answers
|
assert self.order.can_modify_answers
|
||||||
|
assert self.op1.can_modify_answers
|
||||||
self.order.status = Order.STATUS_CANCELED
|
self.order.status = Order.STATUS_CANCELED
|
||||||
assert not self.order.can_modify_answers
|
assert not self.order.can_modify_answers
|
||||||
|
assert not self.op1.can_modify_answers
|
||||||
self.order.status = Order.STATUS_PAID
|
self.order.status = Order.STATUS_PAID
|
||||||
assert self.order.can_modify_answers
|
assert self.order.can_modify_answers
|
||||||
|
assert self.op1.can_modify_answers
|
||||||
self.event.settings.set('last_order_modification_date', now() - timedelta(days=1))
|
self.event.settings.set('last_order_modification_date', now() - timedelta(days=1))
|
||||||
assert not self.order.can_modify_answers
|
assert not self.order.can_modify_answers
|
||||||
|
assert not self.op1.can_modify_answers
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_can_modify_answers_subevent(self):
|
def test_can_modify_answers_subevent(self):
|
||||||
|
|||||||
@@ -191,6 +191,24 @@ class OrdersTest(BaseOrdersTest):
|
|||||||
self.deleted_pos.positionid, self.deleted_pos.web_secret)
|
self.deleted_pos.positionid, self.deleted_pos.web_secret)
|
||||||
)
|
)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/1/123/modify' % (self.orga.slug, self.event.slug, self.not_my_order.code)
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.deleted_pos.positionid, self.deleted_pos.web_secret)
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/1/123/change' % (self.orga.slug, self.event.slug, self.not_my_order.code)
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.deleted_pos.positionid, self.deleted_pos.web_secret)
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
def test_orders_confirm_email(self):
|
def test_orders_confirm_email(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -424,6 +442,49 @@ class OrdersTest(BaseOrdersTest):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
assert self.order.invoices.count() == 3
|
assert self.order.invoices.count() == 3
|
||||||
|
|
||||||
|
def test_orders_attendee_modify_invalid(self):
|
||||||
|
self.order.status = Order.STATUS_CANCELED
|
||||||
|
self.order.save()
|
||||||
|
self.event.settings.set('allow_modifications', 'attendee')
|
||||||
|
r = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.ticket_pos.positionid, self.ticket_pos.web_secret)
|
||||||
|
)
|
||||||
|
assert r.status_code == 302
|
||||||
|
|
||||||
|
def test_orders_attendee_modify_forbidden(self):
|
||||||
|
self.event.settings.set('allow_modifications', 'order')
|
||||||
|
r = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.ticket_pos.positionid, self.ticket_pos.web_secret)
|
||||||
|
)
|
||||||
|
assert r.status_code == 302
|
||||||
|
|
||||||
|
def test_orders_attendee_modify_attendee_optional(self):
|
||||||
|
self.event.settings.set('allow_modifications', 'attendee')
|
||||||
|
self.event.settings.set('attendee_names_asked', True)
|
||||||
|
self.event.settings.set('attendee_names_required', False)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.ticket_pos.positionid, self.ticket_pos.web_secret)
|
||||||
|
)
|
||||||
|
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||||
|
self.assertEqual(len(doc.select('input[name="%s-attendee_name_parts_0"]' % self.ticket_pos.id)), 1)
|
||||||
|
|
||||||
|
# Not all fields filled out, expect success
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.ticket_pos.positionid, self.ticket_pos.web_secret)
|
||||||
|
)
|
||||||
|
self.assertRedirects(response,
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||||
|
self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||||
|
target_status_code=200)
|
||||||
|
with scopes_disabled():
|
||||||
|
self.ticket_pos = OrderPosition.objects.get(id=self.ticket_pos.id)
|
||||||
|
assert self.ticket_pos.attendee_name in (None, '')
|
||||||
|
|
||||||
def test_orders_cancel_invalid(self):
|
def test_orders_cancel_invalid(self):
|
||||||
self.order.status = Order.STATUS_PAID
|
self.order.status = Order.STATUS_PAID
|
||||||
self.order.save()
|
self.order.save()
|
||||||
|
|||||||
Reference in New Issue
Block a user