API: Allow to delete order positions

This commit is contained in:
Raphael Michel
2018-08-13 18:09:10 +02:00
parent 723fedc066
commit 779756f1ab
6 changed files with 100 additions and 18 deletions
+31
View File
@@ -1154,6 +1154,37 @@ Order position ticket download
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few :statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds. seconds.
Manipulating individual positions
---------------------------------
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Deletes an order position, identified by its internal ID.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the order position to delete
:statuscode 204: no error
:statuscode 400: This position cannot be deleted (e.g. last position in order)
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position does not exist.
Order payment endpoints Order payment endpoints
----------------------- -----------------------
+20 -4
View File
@@ -9,7 +9,7 @@ from django.http import FileResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import serializers, status, viewsets from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.exceptions import ( from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError, APIException, NotFound, PermissionDenied, ValidationError,
@@ -35,8 +35,8 @@ from pretix.base.services.invoices import (
) )
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderError, cancel_order, extend_order, mark_order_expired, OrderChangeManager, OrderError, cancel_order, extend_order,
mark_order_refunded, mark_order_expired, mark_order_refunded,
) )
from pretix.base.services.tickets import ( from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position, get_cachedticket_for_order, get_cachedticket_for_position,
@@ -340,7 +340,7 @@ class OrderPositionFilter(FilterSet):
} }
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet): class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none() queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -348,6 +348,7 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',) ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filterset_class = OrderPositionFilter filterset_class = OrderPositionFilter
permission = 'can_view_orders' permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_queryset(self): def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related( return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
@@ -388,6 +389,21 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
) )
return resp return resp
def perform_destroy(self, instance):
try:
ocm = OrderChangeManager(
instance.order,
user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth,
notify=False
)
ocm.cancel(instance)
ocm.commit()
except OrderError as e:
raise ValidationError(str(e))
except Quota.QuotaExceededException as e:
raise ValidationError(str(e))
class PaymentViewSet(viewsets.ReadOnlyModelViewSet): class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer serializer_class = OrderPaymentSerializer
+3 -1
View File
@@ -471,7 +471,8 @@ class Order(LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString], def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent', context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None): user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None):
""" """
Sends an email to the user that placed this order. Basically, this method does two things: Sends an email to the user that placed this order. Basically, this method does two things:
@@ -508,6 +509,7 @@ class Order(LoggedModel):
self.log_action( self.log_action(
log_entry_type, log_entry_type,
user=user, user=user,
auth=auth,
data={ data={
'subject': subject, 'subject': subject,
'message': email_content, 'message': email_content,
+14 -13
View File
@@ -607,9 +607,10 @@ class OrderChangeManager:
SplitOperation = namedtuple('SplitOperation', ('position',)) SplitOperation = namedtuple('SplitOperation', ('position',))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',)) RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
def __init__(self, order: Order, user, notify=True): def __init__(self, order: Order, user=None, auth=None, notify=True):
self.order = order self.order = order
self.user = user self.user = user
self.auth = auth
self.split_order = None self.split_order = None
self._committed = False self._committed = False
self._totaldiff = 0 self._totaldiff = 0
@@ -779,7 +780,7 @@ class OrderChangeManager:
fee=None fee=None
) )
try: try:
p.confirm(send_mail=False, count_waitinglist=False, user=self.user) p.confirm(send_mail=False, count_waitinglist=False, user=self.user, auth=self.auth)
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
raise OrderError(self.error_messages['paid_to_free_exceeded']) raise OrderError(self.error_messages['paid_to_free_exceeded'])
@@ -791,7 +792,7 @@ class OrderChangeManager:
fee=None fee=None
) )
try: try:
p.confirm(send_mail=False, count_waitinglist=False, user=self.user) p.confirm(send_mail=False, count_waitinglist=False, user=self.user, auth=self.auth)
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
raise OrderError(self.error_messages['paid_to_free_exceeded']) raise OrderError(self.error_messages['paid_to_free_exceeded'])
@@ -801,7 +802,7 @@ class OrderChangeManager:
for op in self._operations: for op in self._operations:
if isinstance(op, self.ItemOperation): if isinstance(op, self.ItemOperation):
self.order.log_action('pretix.event.order.changed.item', user=self.user, data={ self.order.log_action('pretix.event.order.changed.item', user=self.user, auth=self.auth, data={
'position': op.position.pk, 'position': op.position.pk,
'positionid': op.position.positionid, 'positionid': op.position.positionid,
'old_item': op.position.item.pk, 'old_item': op.position.item.pk,
@@ -820,7 +821,7 @@ class OrderChangeManager:
op.position.tax_rule = op.item.tax_rule op.position.tax_rule = op.item.tax_rule
op.position.save() op.position.save()
elif isinstance(op, self.SubeventOperation): elif isinstance(op, self.SubeventOperation):
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, data={ self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
'position': op.position.pk, 'position': op.position.pk,
'positionid': op.position.positionid, 'positionid': op.position.positionid,
'old_subevent': op.position.subevent.pk, 'old_subevent': op.position.subevent.pk,
@@ -835,7 +836,7 @@ class OrderChangeManager:
op.position.tax_rule = op.position.item.tax_rule op.position.tax_rule = op.position.item.tax_rule
op.position.save() op.position.save()
elif isinstance(op, self.PriceOperation): elif isinstance(op, self.PriceOperation):
self.order.log_action('pretix.event.order.changed.price', user=self.user, data={ self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
'position': op.position.pk, 'position': op.position.pk,
'positionid': op.position.positionid, 'positionid': op.position.positionid,
'old_price': op.position.price, 'old_price': op.position.price,
@@ -849,7 +850,7 @@ class OrderChangeManager:
op.position.save() op.position.save()
elif isinstance(op, self.CancelOperation): elif isinstance(op, self.CancelOperation):
for opa in op.position.addons.all(): for opa in op.position.addons.all():
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, data={ self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': opa.pk, 'position': opa.pk,
'positionid': opa.positionid, 'positionid': opa.positionid,
'old_item': opa.item.pk, 'old_item': opa.item.pk,
@@ -857,7 +858,7 @@ class OrderChangeManager:
'addon_to': opa.addon_to_id, 'addon_to': opa.addon_to_id,
'old_price': opa.price, 'old_price': opa.price,
}) })
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, data={ self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': op.position.pk, 'position': op.position.pk,
'positionid': op.position.positionid, 'positionid': op.position.positionid,
'old_item': op.position.item.pk, 'old_item': op.position.item.pk,
@@ -874,7 +875,7 @@ class OrderChangeManager:
positionid=nextposid, subevent=op.subevent positionid=nextposid, subevent=op.subevent
) )
nextposid += 1 nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, data={ self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
'position': pos.pk, 'position': pos.pk,
'item': op.item.pk, 'item': op.item.pk,
'variation': op.variation.pk if op.variation else None, 'variation': op.variation.pk if op.variation else None,
@@ -890,7 +891,7 @@ class OrderChangeManager:
op.position.save() op.position.save()
CachedTicket.objects.filter(order_position__order=self.order).delete() CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete() CachedCombinedTicket.objects.filter(order=self.order).delete()
self.order.log_action('pretix.event.order.changed.secret', user=self.user, data={ self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
'position': op.position.pk, 'position': op.position.pk,
'positionid': op.position.positionid, 'positionid': op.position.positionid,
}) })
@@ -905,12 +906,12 @@ class OrderChangeManager:
split_order.datetime = now() split_order.datetime = now()
split_order.secret = generate_secret() split_order.secret = generate_secret()
split_order.save() split_order.save()
split_order.log_action('pretix.event.order.changed.split_from', user=self.user, data={ split_order.log_action('pretix.event.order.changed.split_from', user=self.user, auth=self.auth, data={
'original_order': self.order.code 'original_order': self.order.code
}) })
for op in split_positions: for op in split_positions:
self.order.log_action('pretix.event.order.changed.split', user=self.user, data={ self.order.log_action('pretix.event.order.changed.split', user=self.user, auth=self.auth, data={
'position': op.pk, 'position': op.pk,
'positionid': op.positionid, 'positionid': op.positionid,
'old_item': op.item.pk, 'old_item': op.item.pk,
@@ -1080,7 +1081,7 @@ class OrderChangeManager:
try: try:
order.send_mail( order.send_mail(
email_subject, email_template, email_context, email_subject, email_template, email_context,
'pretix.event.order.email.order_changed', self.user 'pretix.event.order.email.order_changed', self.user, auth=self.auth
) )
except SendMailException: except SendMailException:
logger.exception('Order changed email could not be sent') logger.exception('Order changed email could not be sent')
+31
View File
@@ -672,6 +672,37 @@ def test_orderposition_detail(token_client, organizer, event, order, item, quest
assert len(resp.data['downloads']) == 1 assert len(resp.data['downloads']) == 1
@pytest.mark.django_db
def test_orderposition_delete(token_client, organizer, event, order, item, question):
op = order.positions.first()
resp = token_client.delete('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
))
assert resp.status_code == 400
assert resp.data == ['This operation would leave the order empty. Please cancel the order itself instead.']
op2 = OrderPosition.objects.create(
order=order,
item=item,
variation=None,
price=Decimal("23"),
attendee_name="Peter",
secret="foobar",
pseudonymization_id="BAZ",
)
order.refresh_from_db()
assert order.total == Decimal('46')
assert order.positions.count() == 2
resp = token_client.delete('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op2.pk
))
assert resp.status_code == 204
assert order.positions.count() == 1
order.refresh_from_db()
assert order.total == Decimal('23')
@pytest.fixture @pytest.fixture
def invoice(order): def invoice(order):
testtime = datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC) testtime = datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC)
+1
View File
@@ -23,6 +23,7 @@ event_urls = [
event_permission_sub_urls = [ event_permission_sub_urls = [
('get', 'can_view_orders', 'orders/', 200), ('get', 'can_view_orders', 'orders/', 200),
('get', 'can_view_orders', 'orderpositions/', 200), ('get', 'can_view_orders', 'orderpositions/', 200),
('delete', 'can_change_orders', 'orderpositions/1/', 404),
('get', 'can_view_vouchers', 'vouchers/', 200), ('get', 'can_view_vouchers', 'vouchers/', 200),
('get', 'can_view_orders', 'invoices/', 200), ('get', 'can_view_orders', 'invoices/', 200),
('get', 'can_view_orders', 'invoices/1/', 404), ('get', 'can_view_orders', 'invoices/1/', 404),