New implementation of sales channels (#4111)

Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
Raphael Michel
2024-06-30 19:24:30 +02:00
committed by GitHub
parent 95511b0330
commit 4fb5c6bef0
174 changed files with 2902 additions and 616 deletions

View File

@@ -49,7 +49,6 @@ from django.views.generic import FormView, ListView, TemplateView
from i18nfield.strings import LazyI18nString
from pretix.api.views.checkin import _redeem_process
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.base.services.checkin import (
@@ -296,7 +295,9 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
ordering = ('subevent__date_from', 'name', 'pk')
def get_queryset(self):
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
"limit_products", "auto_checkin_sales_channels"
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -305,12 +306,10 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
clists = list(ctx['checkinlists'])
sales_channels = get_all_sales_channels()
for cl in clists:
if cl.subevent:
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels]
ctx['checkinlists'] = clists
ctx['can_change_organizer_settings'] = self.request.user.has_organizer_permission(

View File

@@ -43,7 +43,6 @@ from pretix.control.permissions import (
)
from pretix.helpers.models import modelcopy
from ...base.channels import get_all_sales_channels
from ...helpers.compat import CompatDeleteView
from . import CreateView, PaginationMixin, UpdateView
@@ -190,11 +189,14 @@ class DiscountList(PaginationMixin, ListView):
template_name = 'pretixcontrol/items/discounts.html'
def get_queryset(self):
return self.request.event.discounts.prefetch_related('condition_limit_products')
return self.request.event.discounts.prefetch_related('condition_limit_products', 'limit_sales_channels')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels()
ctx['sales_channels'] = [
c for c in self.request.organizer.sales_channels.all()
if c.type_instance.discounts_supported
]
return ctx

View File

@@ -71,7 +71,6 @@ from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString
from i18nfield.utils import I18nJSONEncoder
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
@@ -559,7 +558,7 @@ class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView):
key=lambda s: s.verbose_name
)
sales_channels = get_all_sales_channels()
sales_channels = {s.identifier: s for s in self.request.organizer.sales_channels.all()}
for p in context['providers']:
p.show_enabled = p.is_enabled
p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
@@ -1468,7 +1467,7 @@ class QuickSetupView(FormView):
admission=True,
personalized=True,
position=i,
sales_channels=list(get_all_sales_channels().keys())
all_sales_channels=True,
)
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:

View File

@@ -84,7 +84,6 @@ from pretix.control.permissions import (
from pretix.control.signals import item_forms, item_formsets
from pretix.helpers.models import modelcopy
from ...base.channels import get_all_sales_channels
from ...helpers.compat import CompatDeleteView
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
@@ -106,14 +105,14 @@ class ItemList(ListView):
event=self.request.event
).annotate(
var_count=Count('variations')
).prefetch_related("category").order_by(
).prefetch_related("category", "limit_sales_channels").order_by(
F('category__position').asc(nulls_first=True),
'category', 'position'
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels()
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
return ctx
@@ -1503,7 +1502,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
"Your participants won't be able to buy the bundle unless you remove this "
"item from it."))
ctx['sales_channels'] = get_all_sales_channels()
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
return ctx
@cached_property
@@ -1515,7 +1514,9 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
can_order=True, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related('meta_values', 'require_membership_types'),
queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related(
'meta_values', 'limit_sales_channels', 'require_membership_types'
),
event=self.request.event, prefix="variations"
)),
('addons', inlineformset_factory(

View File

@@ -71,7 +71,6 @@ from django.views.generic import (
)
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.exporter import MultiSheetListExporter
@@ -375,7 +374,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
def get_queryset(self):
qs = Order.objects.filter(
event=self.request.event
).select_related('invoice_address')
).select_related('invoice_address').prefetch_related("sales_channel")
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -420,7 +419,6 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
)
}
scs = get_all_sales_channels()
for o in ctx['orders']:
if o.pk not in annotated:
continue
@@ -433,7 +431,6 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
o.icnt = annotated.get(o.pk)['icnt']
o.sales_channel_obj = scs[o.sales_channel]
if ctx['page_obj'].paginator.count < 1000:
# Performance safeguard: Only count positions if the data set is small
@@ -520,7 +517,6 @@ class OrderDetail(OrderView):
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
ctx['overpaid'] = self.order.pending_sum * -1
ctx['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel)
ctx['download_buttons'] = self.download_buttons
ctx['payment_refund_sum'] = self.order.payment_refund_sum
ctx['pending_sum'] = self.order.pending_sum

View File

@@ -56,7 +56,7 @@ from django.forms import DecimalField
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.functional import cached_property
@@ -71,7 +71,7 @@ from django.views.generic import (
from pretix.api.models import ApiCall, WebHook
from pretix.api.webhooks import manually_retry_all_calls
from pretix.base.auth import get_auth_backends
from pretix.base.channels import get_all_sales_channels
from pretix.base.channels import get_all_sales_channel_types
from pretix.base.exporter import (
MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -87,7 +87,7 @@ from pretix.base.models.giftcards import (
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
)
from pretix.base.models.orders import CancellationRequest
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.payment import PaymentException
from pretix.base.services.export import multiexport, scheduled_organizer_export
from pretix.base.services.mail import SendMailException, mail
@@ -107,8 +107,8 @@ from pretix.control.forms.organizer import (
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
ReusableMediumUpdateForm, SSOClientForm, SSOProviderForm, TeamForm,
WebHookForm,
ReusableMediumUpdateForm, SalesChannelForm, SSOClientForm, SSOProviderForm,
TeamForm, WebHookForm,
)
from pretix.control.forms.rrule import RRuleForm
from pretix.control.logdisplay import OVERVIEW_BANLIST
@@ -2206,7 +2206,7 @@ def meta_property_move_down(request, organizer, property):
@transaction.atomic
@organizer_permission_required("can_change_items")
@organizer_permission_required("can_change_organizer_settings")
@require_http_methods(["POST"])
def reorder_meta_properties(request, organizer):
try:
@@ -2643,7 +2643,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
q |= Q(email__iexact=self.customer.email)
qs = Order.objects.filter(
q
).select_related('event').order_by('-datetime', 'pk')
).select_related('event').prefetch_related('sales_channel').order_by('-datetime', 'pk')
return qs
@cached_property
@@ -2720,7 +2720,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
)
}
scs = get_all_sales_channels()
for o in ctx['orders']:
if o.pk not in annotated:
continue
@@ -2733,7 +2732,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
o.icnt = annotated.get(o.pk)['icnt']
o.sales_channel_obj = scs[o.sales_channel]
ctx["lifetime_spending"] = (
self.get_queryset()
@@ -3040,3 +3038,242 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
'organizer': self.request.organizer.slug,
'pk': self.object.pk,
})
class ChannelListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = SalesChannel
template_name = 'pretixcontrol/organizers/channels.html'
permission = 'can_change_organizer_settings'
context_object_name = 'channels'
def get_queryset(self):
return self.request.organizer.sales_channels.all()
class ChannelEditorMixin:
form_class = SalesChannelForm
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
'event': self.request.organizer,
}
class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, CreateView):
model = SalesChannel
permission = 'can_change_organizer_settings'
template_name = 'pretixcontrol/organizers/channel_add.html'
def get_object(self, queryset=None):
return SalesChannel()
@property
def allowed_types(self):
existing_types = set(self.request.organizer.sales_channels.values_list("type", flat=True))
return {
k: t for k, t in get_all_sales_channel_types().items()
if t.multiple_allowed or t.identifier not in existing_types
}
@cached_property
def selected_type(self):
try:
return self.allowed_types[self.request.GET.get("type")]
except KeyError:
return None
def post(self, request, *args, **kwargs):
if not self.selected_type:
return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
"types": self.allowed_types.values()
})
return super().post(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
if not self.selected_type:
return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
"types": self.allowed_types.values()
})
return super().get(request, *args, **kwargs)
def get_success_url(self):
return reverse('control:organizer.channels', kwargs={
'organizer': self.request.organizer.slug,
})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["type"] = self.selected_type
if self.selected_type.multiple_allowed:
ctx["identifier_prefix"] = self.selected_type.identifier + "."
return ctx
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"type": self.selected_type,
}
def form_valid(self, form):
messages.success(self.request, _('The sales channel has been created.'))
form.instance.organizer = self.request.organizer
form.instance.type = self.selected_type.identifier
form.instance.position = (self.request.organizer.sales_channels.aggregate(m=Max("position"))["m"] or 0) + 1
ret = super().form_valid(form)
form.instance.log_action('pretix.saleschannel.created', user=self.request.user, data={
k: getattr(self.object, k) for k in form.changed_data
})
return ret
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, UpdateView):
model = SalesChannel
permission = 'can_change_organizer_settings'
context_object_name = 'channel'
template_name = 'pretixcontrol/organizers/channel_edit.html'
def get_object(self, queryset=None):
return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
def get_success_url(self):
return reverse('control:organizer.channels', kwargs={
'organizer': self.request.organizer.slug,
})
@cached_property
def type(self):
return get_all_sales_channel_types()[self.object.type]
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["type"] = self.type
return ctx
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"type": self.type,
}
def form_valid(self, form):
if form.has_changed() or self.formset.has_changed():
self.object.log_action('pretix.saleschannel.changed', user=self.request.user, data={
k: getattr(self.object, k)
for k in form.changed_data
})
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class ChannelDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = SalesChannel
template_name = 'pretixcontrol/organizers/channel_delete.html'
permission = 'can_change_organizer_settings'
context_object_name = 'channel'
def get_object(self, queryset=None):
return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
def get_success_url(self):
return reverse('control:organizer.channels', kwargs={
'organizer': self.request.organizer.slug,
})
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx["is_allowed"] = self.get_object().allow_delete
return ctx
@transaction.atomic
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
if not self.object.allow_delete():
messages.error(self.request, _('This channel can not be deleted.'))
return redirect(success_url)
try:
self.object.log_action('pretix.saleschannel.deleted', user=self.request.user)
self.object.delete()
messages.success(request, _('The selected sales channel has been deleted.'))
except ProtectedError:
messages.error(self.request, _('The channel could not be deleted as some constraints (e.g. data created by '
'plug-ins) did not allow it.'))
return redirect(success_url)
def channel_move(request, channel, up=True):
channel = get_object_or_404(request.organizer.sales_channels, identifier=channel)
channels = list(request.organizer.sales_channels.order_by("position"))
index = channels.index(channel)
if index != 0 and up:
channels[index - 1], channels[index] = channels[index], channels[index - 1]
elif index != len(channels) - 1 and not up:
channels[index + 1], channels[index] = channels[index], channels[index + 1]
for i, prop in enumerate(channels):
if prop.position != i:
prop.position = i
prop.save()
prop.log_action(
'pretix.saleschannel.reordered', user=request.user, data={
'position': i,
}
)
messages.success(request, _('The order of sales channels has been updated.'))
@organizer_permission_required("can_change_organizer_settings")
@require_http_methods(["POST"])
def channel_move_up(request, organizer, channel):
channel_move(request, channel, up=True)
return redirect('control:organizer.channels',
organizer=request.organizer.slug)
@organizer_permission_required("can_change_organizer_settings")
@require_http_methods(["POST"])
def channel_move_down(request, organizer, channel):
channel_move(request, channel, up=False)
return redirect('control:organizer.channels',
organizer=request.organizer.slug)
@transaction.atomic
@organizer_permission_required("can_change_organizer_settings")
@require_http_methods(["POST"])
def reorder_channels(request, organizer):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
return HttpResponseBadRequest("expected JSON: {ids:[]}")
input_channels = list(request.organizer.sales_channels.filter(id__in=[i for i in ids if i.isdigit()]))
if len(input_channels) != len(ids):
raise Http404(_("Some of the provided object ids are invalid."))
if len(input_channels) != request.organizer.sales_channels.count():
raise Http404(_("Not all objects have been selected."))
for c in input_channels:
pos = ids.index(str(c.pk))
if pos != c.position: # Save unneccessary UPDATE queries
c.position = pos
c.save(update_fields=['position'])
c.log_action(
'pretix.saleschannel.reordered', user=request.user, data={
'position': pos,
}
)
return HttpResponse()

View File

@@ -98,6 +98,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
sales_channel=self.request.event.organizer.sales_channels.get(identifier="web"),
locale=self.request.event.settings.locale,
expires=now(), code="PREVIEW1234", total=Decimal('119.00'))