diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index de6da4fd4d..df5e00e6d8 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -1943,9 +1943,14 @@ Manipulating individual positions * ``valid_until`` + * ``secret`` + Changing parameters such as ``item`` or ``price`` will **not** automatically trigger creation of a new invoice, you need to take care of that yourself. + Changing ``secret`` does not cause a new PDF ticket to be sent to the customer, nor does it cause the old secret + to be added to the revocation list, even if your ticket generator uses one. + **Example request**: .. sourcecode:: http diff --git a/src/pretix/api/serializers/orderchange.py b/src/pretix/api/serializers/orderchange.py index 5998f7eebd..8c3cb896ad 100644 --- a/src/pretix/api/serializers/orderchange.py +++ b/src/pretix/api/serializers/orderchange.py @@ -251,7 +251,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer): class Meta: model = OrderPosition fields = ( - 'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule', 'valid_from', 'valid_until' + 'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule', 'valid_from', 'valid_until', 'secret' ) def __init__(self, *args, **kwargs): @@ -319,6 +319,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer): tax_rule = validated_data.get('tax_rule', instance.tax_rule) valid_from = validated_data.get('valid_from', instance.valid_from) valid_until = validated_data.get('valid_until', instance.valid_until) + secret = validated_data.get('secret', instance.secret) change_item = None if item != instance.item or variation != instance.variation: @@ -351,6 +352,9 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer): if valid_until != instance.valid_until: ocm.change_valid_until(instance, valid_until) + if secret != instance.secret: + ocm.change_ticket_secret(instance, secret) + if self.context.get('commit', True): ocm.commit() instance.refresh_from_db() diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 197ee22407..863f9bfdf0 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -1564,6 +1564,7 @@ class OrderChangeManager: AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff')) CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff')) RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',)) + ChangeSecretOperation = namedtuple('ChangeSecretOperation', ('position', 'new_secret')) ChangeValidFromOperation = namedtuple('ChangeValidFromOperation', ('position', 'valid_from')) ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until')) AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) @@ -1671,6 +1672,9 @@ class OrderChangeManager: def regenerate_secret(self, position: OrderPosition): self._operations.append(self.RegenerateSecretOperation(position)) + def change_ticket_secret(self, position: OrderPosition, new_secret: str): + self._operations.append(self.ChangeSecretOperation(position, new_secret)) + def change_valid_from(self, position: OrderPosition, new_value: datetime): self._operations.append(self.ChangeValidFromOperation(position, new_value)) @@ -2441,6 +2445,19 @@ class OrderChangeManager: 'position': op.position.pk, 'positionid': op.position.positionid, }) + elif isinstance(op, self.ChangeSecretOperation): + if OrderPosition.all.filter(order__event=self.event, secret=op.new_secret).exists(): + raise OrderError('You cannot assign a position secret that already exists.') + op.position.secret = op.new_secret + op.position.save(update_fields=["secret"]) + if op.position in secret_dirty: + secret_dirty.remove(op.position) + tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk, + 'order': self.order.pk}) + self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={ + 'position': op.position.pk, + 'positionid': op.position.positionid, + }) elif isinstance(op, self.ChangeValidFromOperation): self.order.log_action('pretix.event.order.changed.valid_from', user=self.user, auth=self.auth, data={ 'position': op.position.pk, diff --git a/src/tests/api/test_order_change.py b/src/tests/api/test_order_change.py index a956950f32..ec9394f68d 100644 --- a/src/tests/api/test_order_change.py +++ b/src/tests/api/test_order_change.py @@ -1468,6 +1468,31 @@ def test_position_update_change_price_and_tax_rule(token_client, organizer, even assert op.tax_rule == tr +@pytest.mark.django_db +def test_position_update_secret(token_client, organizer, event, order, item): + with scopes_disabled(): + order.positions.create(item=item, price=Decimal('23.00'), secret='alreadyused') + p = order.positions.first() + psw = p.web_secret + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format( + organizer.slug, event.slug, p.pk, + ), format='json', data={'secret': 'nobodyknows'} + ) + assert resp.status_code == 200 + p.refresh_from_db() + with scopes_disabled(): + assert 'nobodyknows' == p.secret + assert psw == p.web_secret + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format( + organizer.slug, event.slug, p.pk, + ), format='json', data={'secret': 'alreadyused'} + ) + assert resp.status_code == 400 + + @pytest.mark.django_db def test_position_add_simple(token_client, organizer, event, order, quota, item): with scopes_disabled():