mirror of
https://github.com/pretix/pretix.git
synced 2026-08-11 10:57:00 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dcddec33 | ||
|
|
176a89cebb | ||
|
|
4d9dfa88fe | ||
|
|
ea792e76b2 | ||
|
|
5c448279f0 |
@@ -139,6 +139,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return qs
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -153,6 +154,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
@@ -64,6 +65,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -78,6 +80,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -87,6 +90,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('You cannot delete this discount because it already has '
|
||||
|
||||
@@ -257,6 +257,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
copy_from = None
|
||||
if 'clone_from' in self.request.GET:
|
||||
@@ -320,6 +321,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
|
||||
@@ -355,6 +357,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
||||
@@ -513,6 +516,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
resp['X-Page-Generated'] = date
|
||||
return resp
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
super().perform_update(serializer)
|
||||
@@ -529,6 +533,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -538,6 +543,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
|
||||
@@ -566,6 +572,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return self.request.event.tax_rules.all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.log_action(
|
||||
@@ -575,6 +582,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -584,6 +592,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
|
||||
@@ -757,6 +766,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
|
||||
}
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.event.log_action(
|
||||
@@ -766,6 +776,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
|
||||
data={"seats": [serializer.instance.pk]},
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def bulk_change_blocked(self, blocked):
|
||||
s = SeatBulkBlockInputSerializer(
|
||||
data=self.request.data,
|
||||
|
||||
@@ -23,6 +23,7 @@ from datetime import timedelta
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
@@ -227,6 +228,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
qs = self.request.event.scheduled_exports
|
||||
return qs.select_related("owner")
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_authenticated:
|
||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||
@@ -257,6 +259,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
))
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
@@ -282,6 +285,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.request.event.log_action(
|
||||
'pretix.event.export.schedule.deleted',
|
||||
@@ -309,6 +313,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
qs = self.request.organizer.scheduled_exports
|
||||
return qs.select_related("owner")
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_authenticated:
|
||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||
@@ -339,6 +344,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
))
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
@@ -389,6 +395,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.export.schedule.deleted',
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
@@ -109,6 +110,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
||||
).all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -123,6 +125,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
@@ -139,6 +142,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
|
||||
@@ -183,6 +187,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = self.item
|
||||
if not item.has_variations:
|
||||
@@ -197,6 +202,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
{'value': serializer.instance.value})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.item.log_action(
|
||||
@@ -207,6 +213,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
{'value': serializer.instance.value})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
|
||||
@@ -249,6 +256,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
serializer.save(base_item=item)
|
||||
@@ -259,6 +267,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.base_item.log_action(
|
||||
@@ -268,6 +277,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.base_item.log_action(
|
||||
@@ -303,6 +313,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
serializer.save(item=item)
|
||||
@@ -313,6 +324,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.item.log_action(
|
||||
@@ -322,6 +334,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.item.log_action(
|
||||
@@ -354,6 +367,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = self.item
|
||||
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
|
||||
@@ -365,6 +379,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.base_item.log_action(
|
||||
@@ -374,6 +389,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.base_item.log_action(
|
||||
@@ -403,6 +419,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return self.request.event.categories.all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -417,6 +434,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -426,6 +444,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
for item in instance.items.all():
|
||||
item.category = None
|
||||
@@ -458,6 +477,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.prefetch_related('options').all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -472,6 +492,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -481,6 +502,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.question.deleted',
|
||||
@@ -509,6 +531,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||
serializer.save(question=q)
|
||||
@@ -519,6 +542,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.question.log_action(
|
||||
@@ -528,6 +552,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.question.log_action(
|
||||
'pretix.event.question.option.deleted',
|
||||
@@ -586,6 +611,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -608,6 +634,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['request'] = self.request
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
@@ -663,6 +690,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
)
|
||||
serializer.instance.rebuild_cache()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.quota.deleted',
|
||||
|
||||
@@ -394,6 +394,7 @@ class TeamViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
|
||||
instance.delete()
|
||||
@@ -693,6 +694,7 @@ class MembershipTypeViewSet(viewsets.ModelViewSet):
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
@@ -833,6 +835,7 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
@@ -62,6 +63,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -70,6 +72,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if serializer.instance.voucher:
|
||||
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
|
||||
@@ -80,6 +83,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if instance.voucher:
|
||||
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
|
||||
@@ -48,6 +49,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
@@ -57,6 +59,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
@@ -67,6 +70,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.request.organizer.log_action(
|
||||
'pretix.webhook.changed',
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import string
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
@@ -307,3 +309,8 @@ class DeviceLastSeen(models.Model):
|
||||
autosummarize=True
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def is_recent(self):
|
||||
# pretixSCAN/pretixPOS sync every 5 minutes, so 7 minutes can be considered "offline"
|
||||
return now() - self.last_seen < timedelta(minutes=7)
|
||||
|
||||
@@ -628,9 +628,14 @@ class Order(LockModel, LoggedModel):
|
||||
def set_expires(self, now_dt=None, subevents=None):
|
||||
now_dt = now_dt or now()
|
||||
tz = ZoneInfo(self.event.settings.timezone)
|
||||
mode = self.event.settings.get('payment_term_mode')
|
||||
|
||||
sales_channel_suffix = "_" + self.sales_channel.identifier.replace(".", "_")
|
||||
if not (mode := self.event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
|
||||
mode = self.event.settings.get('payment_term_mode')
|
||||
sales_channel_suffix = ""
|
||||
|
||||
if mode == 'days':
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
|
||||
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if self.event.settings.get('payment_term_weekdays'):
|
||||
if exp_by_date.weekday() == 5:
|
||||
@@ -638,7 +643,7 @@ class Order(LockModel, LoggedModel):
|
||||
elif exp_by_date.weekday() == 6:
|
||||
exp_by_date += timedelta(days=1)
|
||||
elif mode == 'minutes':
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get('payment_term_minutes', as_type=int))
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
|
||||
else:
|
||||
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
|
||||
|
||||
|
||||
@@ -979,12 +979,12 @@ DEFAULTS = {
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
'form_kwargs': lambda suffix="", parent=0: dict(
|
||||
label=_('Payment term in days'),
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
'data-display-dependency': '#id_payment_term_mode_0',
|
||||
'data-required-if': '#id_payment_term_mode_0'
|
||||
'data-display-dependency': f'#id_payment_term_mode{suffix}_{parent}',
|
||||
'data-required-if': f'#id_payment_term_mode{suffix}_{parent}'
|
||||
},
|
||||
),
|
||||
help_text=_("The number of days after placing an order the user has to pay to preserve their reservation. If "
|
||||
@@ -1023,7 +1023,7 @@ DEFAULTS = {
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
'form_kwargs': lambda suffix="", parent=1: dict(
|
||||
label=_('Payment term in minutes'),
|
||||
help_text=_("The number of minutes after placing an order the user has to pay to preserve their reservation. "
|
||||
"Only use this if you exclusively offer real-time payment methods. Please note that for technical reasons, "
|
||||
@@ -1032,8 +1032,8 @@ DEFAULTS = {
|
||||
MaxValueValidator(1440)],
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
'data-display-dependency': '#id_payment_term_mode_1',
|
||||
'data-required-if': '#id_payment_term_mode_1'
|
||||
'data-display-dependency': f'#id_payment_term_mode{suffix}_{parent}',
|
||||
'data-required-if': f'#id_payment_term_mode{suffix}_{parent}'
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -856,6 +856,50 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'tax_rule_payment',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.term_channel_fields = {}
|
||||
for c in self.obj.organizer.sales_channels.all():
|
||||
if c.type_instance.payment_restrictions_supported and c.identifier != "web":
|
||||
# At the moment, it seems sufficient to allow this for the same channel types as other payment settings
|
||||
# We can always introduce more flags later if needed
|
||||
suffix = '_' + c.identifier.replace(".", "_")
|
||||
self.term_channel_fields[c] = [
|
||||
'payment_term_mode' + suffix,
|
||||
'payment_term_days' + suffix,
|
||||
'payment_term_minutes' + suffix,
|
||||
]
|
||||
self.fields['payment_term_mode' + suffix] = DEFAULTS['payment_term_mode']['form_class'](
|
||||
label=_("Payment term"),
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
choices=(
|
||||
('', _("same as above")),
|
||||
('days', _("different payment term in days")),
|
||||
('minutes', _("different payment term in minutes"))
|
||||
),
|
||||
)
|
||||
self.fields['payment_term_days' + suffix] = DEFAULTS['payment_term_days']['form_class'](
|
||||
required=False,
|
||||
**DEFAULTS['payment_term_days']['form_kwargs'](suffix, 1),
|
||||
)
|
||||
self.fields['payment_term_minutes' + suffix] = DEFAULTS['payment_term_minutes']['form_class'](
|
||||
required=False,
|
||||
**DEFAULTS['payment_term_minutes']['form_kwargs'](suffix, 2),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
for c in self.term_channel_fields.keys():
|
||||
suffix = '_' + c.identifier.replace(".", "_")
|
||||
mode = self.cleaned_data.get(f'payment_term_mode{suffix}')
|
||||
if mode == 'days' and self.cleaned_data.get(f'payment_term_days{suffix}') is None:
|
||||
raise ValidationError({f'payment_term_days{suffix}': _("This field is required.")})
|
||||
if mode == 'minutes' and self.cleaned_data.get(f'payment_term_minutes{suffix}') is None:
|
||||
raise ValidationError({f'payment_term_minutes{suffix}': _("This field is required.")})
|
||||
return data
|
||||
|
||||
def clean_payment_term_days(self):
|
||||
value = self.cleaned_data.get('payment_term_days')
|
||||
if self.cleaned_data.get('payment_term_mode') == 'days' and value is None:
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load getitem %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Payment settings" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal form-plugins">
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
@@ -71,14 +72,37 @@
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
{% bootstrap_field form.payment_term_mode layout="control" %}
|
||||
{% bootstrap_field form.payment_term_days layout="control" %}
|
||||
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
||||
{% bootstrap_field form.payment_term_minutes layout="control" %}
|
||||
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
||||
{% bootstrap_field form.payment_term_last layout="control" %}
|
||||
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
|
||||
{% trans "days" context "unit" as days %}
|
||||
{% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %}
|
||||
{% bootstrap_field form.payment_term_accept_late layout="control" %}
|
||||
{% bootstrap_field form.payment_pending_hidden layout="control" %}
|
||||
|
||||
{% for c, fields in form.term_channel_fields.items %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ c.type_instance.verbose_name }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ c.type_instance.verbose_name }}"></span>
|
||||
{% endif %}
|
||||
{{ c.label }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for f in fields %}
|
||||
{% bootstrap_field form|getitem:f layout="control" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced" %}</legend>
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<span class="fa fa-fw {% if d.last_seen and d.last_seen.is_recent %}fa-check-circle text-success{% else %}fa-circle text-danger{% endif %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% if d.last_seen %}{% blocktrans with time=d.last_seen.last_seen|date:"SHORT_DATETIME_FORMAT" %}Last seen: {{ time }}{% endblocktrans %}{% else %}{% trans "No recent contact" %}{% endif %}"
|
||||
></span>
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
<td>
|
||||
@@ -125,6 +129,7 @@
|
||||
<del>{% endif %}
|
||||
{{ d.name }}
|
||||
{% if d.revoked %}</del>{% endif %}
|
||||
|
||||
{% if d.gate %}
|
||||
<br>
|
||||
<small class="text-muted">{{ d.gate.name }}</small>
|
||||
|
||||
@@ -108,7 +108,9 @@ from pretix.base.services.export import (
|
||||
init_organizer_exporters, multiexport, scheduled_organizer_export,
|
||||
)
|
||||
from pretix.base.services.mail import mail, prefix_subject
|
||||
from pretix.base.services.placeholders import prepare_sample_context_for_preview
|
||||
from pretix.base.services.placeholders import (
|
||||
prepare_sample_context_for_preview,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
||||
@@ -1206,7 +1208,7 @@ class DeviceQueryMixin:
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.devices.prefetch_related(
|
||||
'limit_events', 'gate',
|
||||
).order_by('revoked', '-device_id')
|
||||
).select_related('last_seen').order_by('revoked', '-device_id')
|
||||
|
||||
if 'device' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
|
||||
@@ -677,6 +677,8 @@ class PaypalMethod(BasePaymentProvider):
|
||||
raise PaymentException(_('We had trouble communicating with PayPal'))
|
||||
else:
|
||||
pp_captured_order = response.result
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.save()
|
||||
|
||||
try:
|
||||
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id)
|
||||
|
||||
@@ -357,14 +357,13 @@ def webhook(request, *args, **kwargs):
|
||||
if 'resource_type' not in event_json:
|
||||
return HttpResponse("Invalid body, no resource_type given", status=400)
|
||||
|
||||
if event_json['resource_type'] not in ["checkout-order", "refund", "capture"]:
|
||||
return HttpResponse("Not interested in this resource type", status=200)
|
||||
|
||||
# Retrieve the Charge ID of the refunded payment
|
||||
if event_json['resource_type'] == 'refund':
|
||||
if event_json['resource_type'] == 'checkout-order':
|
||||
payloadid = event_json['resource']['id']
|
||||
elif event_json['resource_type'] == 'refund' or event_json['resource_type'] == 'capture':
|
||||
payloadid = get_link(event_json['resource']['links'], 'up')['href'].split('/')[-1]
|
||||
else:
|
||||
payloadid = event_json['resource']['id']
|
||||
return HttpResponse("Not interested in this resource type", status=200)
|
||||
|
||||
refs = [payloadid]
|
||||
if event_json['resource'].get('supplementary_data', {}).get('related_ids', {}).get('order_id'):
|
||||
@@ -424,6 +423,8 @@ def webhook(request, *args, **kwargs):
|
||||
**event_json,
|
||||
'_order_state': sale.dict(),
|
||||
})
|
||||
payment.info = json.dumps(sale.dict())
|
||||
payment.save()
|
||||
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'):
|
||||
if event_json['resource_type'] == 'refund':
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
@@ -118,6 +119,7 @@ class RuleViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return Rule.objects.filter(event=self.request.event)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
super().perform_create(serializer)
|
||||
serializer.instance.log_action(
|
||||
@@ -128,6 +130,7 @@ class RuleViewSet(viewsets.ModelViewSet):
|
||||
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.log_action(
|
||||
@@ -137,6 +140,7 @@ class RuleViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.plugins.sendmail.rule.deleted',
|
||||
|
||||
@@ -286,6 +286,30 @@ def test_expiry_dst(event):
|
||||
assert (localex.hour, localex.minute) == (23, 59)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_expiry_per_channel(event):
|
||||
today = now()
|
||||
event.settings.set('payment_term_mode', 'minutes')
|
||||
event.settings.set('payment_term_minutes', 30)
|
||||
event.settings.set('payment_term_mode_baz', 'minutes')
|
||||
event.settings.set('payment_term_minutes_baz', 15)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today,
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="baz"),
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 0
|
||||
assert (order.expires - today).seconds == 15 * 60
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_expiring(event):
|
||||
o1 = Order.objects.create(
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
@@ -409,7 +410,8 @@ def test_webhook_mark_paid(env, client, monkeypatch):
|
||||
order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
|
||||
|
||||
pp_order = Result(get_test_order())
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
|
||||
mock_orders_get_request = MagicMock(return_value=pp_order)
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
|
||||
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
|
||||
with scopes_disabled():
|
||||
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
|
||||
@@ -497,7 +499,7 @@ def test_webhook_mark_paid(env, client, monkeypatch):
|
||||
"resource_version": "2.0"
|
||||
}
|
||||
), content_type='application_json')
|
||||
|
||||
mock_orders_get_request.assert_called_once_with('806440346Y391300T')
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_PAID
|
||||
|
||||
|
||||
Reference in New Issue
Block a user