Fix a potentially destructive bug in 61ae434ab

This commit is contained in:
Raphael Michel
2023-03-08 23:48:45 +01:00
parent 8b8ad34d30
commit 3bbed98844
3 changed files with 69 additions and 7 deletions

View File

@@ -47,7 +47,8 @@ from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import transaction
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.transaction import get_connection
@@ -1701,7 +1702,19 @@ class OrderChangeManager:
for a in position.addons.all():
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:
raise ValueError("Setting addons should be the first/only operation")
@@ -1713,7 +1726,13 @@ class OrderChangeManager:
quota_diff = Counter() # Quota -> Number of usages
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
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
).prefetch_related(
'addons', 'item__addons', 'item__addons__addon_category'

View File

@@ -1436,7 +1436,7 @@ class OrderChangeMixin:
'price': price,
})
try:
ocm.set_addons(addons_data)
ocm.set_addons(addons_data, self.get_position_queryset())
except OrderError as e:
messages.error(self.request, str(e))
form_valid = False
@@ -1543,7 +1543,7 @@ class OrderChange(OrderChangeMixin, EventViewMixin, OrderDetailMixin, TemplateVi
return self.get_order_url()
def get_position_queryset(self):
return self.order.positions
return self.order.positions.all()
def get_price_requirement(self):
return self.request.event.settings.change_allow_user_price

View File

@@ -1514,9 +1514,9 @@ class OrderChangeAddonsTest(BaseOrdersTest):
{
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):
self.event.settings.change_allow_user_price = 'any' # ignored, for attendees its always "eq"
@@ -1556,3 +1556,46 @@ class OrderChangeAddonsTest(BaseOrdersTest):
follow=True
)
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