forked from CGM_Public/pretix_original
Fix a potentially destructive bug in 61ae434ab
This commit is contained in:
@@ -47,7 +47,8 @@ from django.core.cache import cache
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
|
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
|
||||||
|
Value,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.db.transaction import get_connection
|
from django.db.transaction import get_connection
|
||||||
@@ -1701,7 +1702,19 @@ class OrderChangeManager:
|
|||||||
for a in position.addons.all():
|
for a in position.addons.all():
|
||||||
self._operations.append(self.SplitOperation(a))
|
self._operations.append(self.SplitOperation(a))
|
||||||
|
|
||||||
def set_addons(self, addons):
|
def set_addons(self, addons, limit_main_positions=None):
|
||||||
|
"""
|
||||||
|
This is a convenience method to change the add-on products selected on an order. The input structure is similar
|
||||||
|
to CartManager.set_addons. It will automatically compute the correct operations to add, cancel, or change
|
||||||
|
positions on the order. Every existing add-on not in the input will be canceled. Availability of the
|
||||||
|
products is validated (with some exceptions).
|
||||||
|
|
||||||
|
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
||||||
|
``"count"``, and ``"price"``.
|
||||||
|
:param limit_main_positions: By default, the method works on all methods of the order. If you set this to a
|
||||||
|
queryset or a list of positions, all other positions and their add-ons will be kept
|
||||||
|
untouched.
|
||||||
|
"""
|
||||||
if self._operations:
|
if self._operations:
|
||||||
raise ValueError("Setting addons should be the first/only operation")
|
raise ValueError("Setting addons should be the first/only operation")
|
||||||
|
|
||||||
@@ -1713,7 +1726,13 @@ class OrderChangeManager:
|
|||||||
quota_diff = Counter() # Quota -> Number of usages
|
quota_diff = Counter() # Quota -> Number of usages
|
||||||
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
|
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
|
||||||
price_included = defaultdict(dict) # OrderPos -> CategoryID -> bool(price is included)
|
price_included = defaultdict(dict) # OrderPos -> CategoryID -> bool(price is included)
|
||||||
toplevel_op = self.order.positions.filter(
|
if isinstance(limit_main_positions, QuerySet):
|
||||||
|
toplevel_qs = limit_main_positions
|
||||||
|
elif limit_main_positions is not None:
|
||||||
|
toplevel_qs = self.order.positions.filter(pk__in=[p.pk for p in limit_main_positions])
|
||||||
|
else:
|
||||||
|
toplevel_qs = self.order.position
|
||||||
|
toplevel_op = toplevel_qs.filter(
|
||||||
addon_to__isnull=True
|
addon_to__isnull=True
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'addons', 'item__addons', 'item__addons__addon_category'
|
'addons', 'item__addons', 'item__addons__addon_category'
|
||||||
|
|||||||
@@ -1436,7 +1436,7 @@ class OrderChangeMixin:
|
|||||||
'price': price,
|
'price': price,
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
ocm.set_addons(addons_data)
|
ocm.set_addons(addons_data, self.get_position_queryset())
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
messages.error(self.request, str(e))
|
messages.error(self.request, str(e))
|
||||||
form_valid = False
|
form_valid = False
|
||||||
@@ -1543,7 +1543,7 @@ class OrderChange(OrderChangeMixin, EventViewMixin, OrderDetailMixin, TemplateVi
|
|||||||
return self.get_order_url()
|
return self.get_order_url()
|
||||||
|
|
||||||
def get_position_queryset(self):
|
def get_position_queryset(self):
|
||||||
return self.order.positions
|
return self.order.positions.all()
|
||||||
|
|
||||||
def get_price_requirement(self):
|
def get_price_requirement(self):
|
||||||
return self.request.event.settings.change_allow_user_price
|
return self.request.event.settings.change_allow_user_price
|
||||||
|
|||||||
@@ -1514,9 +1514,9 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
|||||||
{
|
{
|
||||||
f'cp_{ticket_pos2.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
f'cp_{ticket_pos2.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||||
},
|
},
|
||||||
follow=False
|
follow=True
|
||||||
)
|
)
|
||||||
assert response.status_code == 302 # nothing changed
|
assert 'did not make any changes' in response.content.decode()
|
||||||
|
|
||||||
def test_attendee_needs_to_keep_price(self):
|
def test_attendee_needs_to_keep_price(self):
|
||||||
self.event.settings.change_allow_user_price = 'any' # ignored, for attendees its always "eq"
|
self.event.settings.change_allow_user_price = 'any' # ignored, for attendees its always "eq"
|
||||||
@@ -1556,3 +1556,46 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
|||||||
follow=True
|
follow=True
|
||||||
)
|
)
|
||||||
assert '€' in response.content.decode()
|
assert '€' in response.content.decode()
|
||||||
|
|
||||||
|
def test_attendee_change_of_addons_does_not_affect_other_positions(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
ticket_pos2 = OrderPosition.objects.create(
|
||||||
|
order=self.order,
|
||||||
|
item=self.ticket,
|
||||||
|
variation=None,
|
||||||
|
price=Decimal("23"),
|
||||||
|
attendee_name_parts={'full_name': "Peter"}
|
||||||
|
)
|
||||||
|
a1 = OrderPosition.objects.create(
|
||||||
|
order=self.order,
|
||||||
|
item=self.workshop1,
|
||||||
|
variation=None,
|
||||||
|
price=Decimal("0"),
|
||||||
|
addon_to=self.ticket_pos,
|
||||||
|
)
|
||||||
|
a2 = OrderPosition.objects.create(
|
||||||
|
order=self.order,
|
||||||
|
item=self.workshop1,
|
||||||
|
variation=None,
|
||||||
|
price=Decimal("0"),
|
||||||
|
addon_to=ticket_pos2,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.event.settings.change_allow_attendee = True
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||||
|
)
|
||||||
|
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||||
|
form_data = extract_form_fields(doc.select('.main-box form')[0])
|
||||||
|
form_data['confirm'] = 'true'
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||||
|
form_data, follow=True
|
||||||
|
)
|
||||||
|
assert 'alert-success' in response.content.decode()
|
||||||
|
|
||||||
|
a1.refresh_from_db()
|
||||||
|
a2.refresh_from_db()
|
||||||
|
assert not a1.canceled
|
||||||
|
assert not a2.canceled
|
||||||
|
|||||||
Reference in New Issue
Block a user