mirror of
https://github.com/pretix/pretix.git
synced 2026-08-27 13:24:41 +00:00
API: Allow to delete order positions
This commit is contained in:
@@ -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
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user