forked from CGM_Public/pretix_original
* API: Write operations on orders resource * Add order API endpoint /extend/
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.base.models import Event
|
||||
@@ -46,3 +47,18 @@ class EventPermission(BasePermission):
|
||||
if required_permission and required_permission not in request.orgapermset:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def permission_required(required_permission):
|
||||
def decorator(function):
|
||||
def wrapper(self, request, *args, **kw):
|
||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
||||
if required_permission and required_permission not in request.eventpermset:
|
||||
raise PermissionDenied('You do not have permission to perform this operation.')
|
||||
elif 'organizer' in request.resolver_match.kwargs:
|
||||
if required_permission and required_permission not in request.orgapermset:
|
||||
raise PermissionDenied('You do not have permission to perform this operation.')
|
||||
|
||||
return function(self, request, *args, **kw)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
16
src/pretix/api/exception.py
Normal file
16
src/pretix/api/exception.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import exception_handler, status
|
||||
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
|
||||
if isinstance(exc, LockTimeoutException):
|
||||
response = Response(
|
||||
{'detail': 'The server was too busy to process your request. Please try again.'},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -1,18 +1,28 @@
|
||||
import datetime
|
||||
|
||||
import django_filters
|
||||
import pytz
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import FileResponse
|
||||
from django.utils.timezone import make_aware
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import serializers, status, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.exceptions import APIException, NotFound, PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
|
||||
)
|
||||
from pretix.base.models import Invoice, Order, OrderPosition
|
||||
from pretix.base.models import Invoice, Order, OrderPosition, Quota
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.invoices import invoice_pdf
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderError, cancel_order, extend_order, mark_order_paid,
|
||||
)
|
||||
from pretix.base.services.tickets import (
|
||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||
)
|
||||
@@ -34,6 +44,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_class = OrderFilter
|
||||
lookup_field = 'code'
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.orders.prefetch_related(
|
||||
@@ -71,6 +82,130 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
return resp
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||
try:
|
||||
mark_order_paid(
|
||||
order, manual=True,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except SendMailException:
|
||||
pass
|
||||
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
return Response(
|
||||
{'detail': 'The order is not pending or expired.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_canceled(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
order = self.get_object()
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
return Response(
|
||||
{'detail': 'The order is not pending.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
cancel_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
send_mail=send_mail
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_pending(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status != Order.STATUS_PAID:
|
||||
return Response(
|
||||
{'detail': 'The order is not paid.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.payment_manual = True
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.unpaid',
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_expired(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
return Response(
|
||||
{'detail': 'The order is not pending.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
order.status = Order.STATUS_EXPIRED
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expired',
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
# TODO: Find a way to implement mark_refunded
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def extend(self, request, **kwargs):
|
||||
new_date = request.data.get('expires', None)
|
||||
force = request.data.get('force', False)
|
||||
if not new_date:
|
||||
return Response(
|
||||
{'detail': 'New date is missing.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
df = serializers.DateField()
|
||||
try:
|
||||
new_date = df.to_internal_value(new_date)
|
||||
except:
|
||||
return Response(
|
||||
{'detail': 'New date is invalid.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
tz = pytz.timezone(self.request.event.settings.timezone)
|
||||
new_date = make_aware(datetime.datetime.combine(
|
||||
new_date,
|
||||
datetime.time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
|
||||
order = self.get_object()
|
||||
|
||||
try:
|
||||
extend_order(
|
||||
order,
|
||||
new_date=new_date,
|
||||
force=force,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
except OrderError as e:
|
||||
return Response(
|
||||
{'detail': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(name='order', lookup_expr='code')
|
||||
|
||||
@@ -24,6 +24,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import CachedTicket, InvoiceAddress, OrderFee
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
@@ -76,7 +77,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
|
||||
force: bool=False, send_mail: bool=True, user: User=None, mail_text='',
|
||||
count_waitinglist=True) -> Order:
|
||||
count_waitinglist=True, api_token=None) -> Order:
|
||||
"""
|
||||
Marks an order as paid. This sets the payment provider, info and date and returns
|
||||
the order object.
|
||||
@@ -119,7 +120,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
'date': date or now_dt,
|
||||
'manual': manual,
|
||||
'force': force
|
||||
}, user=user)
|
||||
}, user=user, api_token=api_token)
|
||||
order_paid.send(order.event, order=order)
|
||||
|
||||
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
|
||||
@@ -158,6 +159,46 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
return order
|
||||
|
||||
|
||||
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, api_token=None):
|
||||
"""
|
||||
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
||||
see if this is actually still possible. If ``force`` is set to ``True``, the result of this check
|
||||
will be ignored.
|
||||
"""
|
||||
if new_date < now():
|
||||
raise OrderError(_('The new expiry date needs to be in the future.'))
|
||||
if order.status == Order.STATUS_PENDING:
|
||||
order.expires = new_date
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expirychanged',
|
||||
user=user,
|
||||
api_token=api_token,
|
||||
data={
|
||||
'expires': order.expires,
|
||||
'state_change': False
|
||||
}
|
||||
)
|
||||
else:
|
||||
with order.event.lock() as now_dt:
|
||||
is_available = order._is_still_available(now_dt, count_waitinglist=False)
|
||||
if is_available is True or force is True:
|
||||
order.expires = new_date
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expirychanged',
|
||||
user=user,
|
||||
api_token=api_token,
|
||||
data={
|
||||
'expires': order.expires,
|
||||
'state_change': True
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise OrderError(is_available)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def mark_order_refunded(order, user=None):
|
||||
"""
|
||||
@@ -182,7 +223,7 @@ def mark_order_refunded(order, user=None):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _cancel_order(order, user=None, send_mail: bool=True):
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
@@ -192,13 +233,15 @@ def _cancel_order(order, user=None, send_mail: bool=True):
|
||||
order = Order.objects.get(pk=order)
|
||||
if isinstance(user, int):
|
||||
user = User.objects.get(pk=user)
|
||||
if isinstance(api_token, int):
|
||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
||||
with order.event.lock():
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
raise OrderError(_('You cannot cancel this order.'))
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save()
|
||||
|
||||
order.log_action('pretix.event.order.canceled', user=user)
|
||||
order.log_action('pretix.event.order.canceled', user=user, api_token=api_token)
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
@@ -954,10 +997,10 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True):
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None):
|
||||
try:
|
||||
try:
|
||||
return _cancel_order(order, user, send_mail)
|
||||
return _cancel_order(order, user, send_mail, api_token)
|
||||
except LockTimeoutException:
|
||||
self.retry(exc=OrderError(error_messages['busy']))
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
|
||||
@@ -37,7 +37,8 @@ from pretix.base.services.invoices import (
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException, render_mail
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, cancel_order, mark_order_paid,
|
||||
OrderChangeManager, OrderError, cancel_order, extend_order,
|
||||
mark_order_paid,
|
||||
)
|
||||
from pretix.base.services.stats import order_overview
|
||||
from pretix.base.signals import register_data_exporters
|
||||
@@ -453,32 +454,20 @@ class OrderExtend(OrderView):
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.form.is_valid():
|
||||
if self.order.status == Order.STATUS_PENDING:
|
||||
try:
|
||||
extend_order(
|
||||
self.order,
|
||||
new_date=self.form.cleaned_data.get('expires'),
|
||||
force=self.form.cleaned_data.get('quota_ignore', False),
|
||||
user=self.request.user
|
||||
)
|
||||
messages.success(self.request, _('The payment term has been changed.'))
|
||||
self.order.log_action('pretix.event.order.expirychanged', user=self.request.user, data={
|
||||
'expires': self.order.expires,
|
||||
'state_change': False
|
||||
})
|
||||
self.form.save()
|
||||
else:
|
||||
try:
|
||||
with self.order.event.lock() as now_dt:
|
||||
is_available = self.order._is_still_available(now_dt, count_waitinglist=False)
|
||||
if is_available is True or self.form.cleaned_data.get('quota_ignore', False) is True:
|
||||
self.form.save()
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.save()
|
||||
self.order.log_action('pretix.event.order.expirychanged', user=self.request.user, data={
|
||||
'expires': self.order.expires,
|
||||
'state_change': True
|
||||
})
|
||||
messages.success(self.request, _('The payment term has been changed.'))
|
||||
else:
|
||||
messages.error(self.request, is_available)
|
||||
return self._redirect_here()
|
||||
except LockTimeoutException:
|
||||
messages.error(self.request, _('We were not able to process the request completely as the '
|
||||
'server was too busy.'))
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
return self._redirect_here()
|
||||
except LockTimeoutException:
|
||||
messages.error(self.request, _('We were not able to process the request completely as the '
|
||||
'server was too busy.'))
|
||||
return self._redirect_back()
|
||||
else:
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
@@ -252,6 +252,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
),
|
||||
'EXCEPTION_HANDLER': 'pretix.api.exception.custom_exception_handler',
|
||||
'UNICODE_JSON': False
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user