forked from CGM_Public/pretix_original
New implementation of sales channels (#4111)
Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
#
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@@ -61,3 +62,57 @@ class CompatibleJSONField(serializers.JSONField):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
class SalesChannelMigrationMixin:
|
||||
"""
|
||||
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
|
||||
"""
|
||||
|
||||
@property
|
||||
def organizer(self):
|
||||
if "organizer" in self.context:
|
||||
return self.context["organizer"]
|
||||
elif "event" in self.context:
|
||||
return self.context["event"].organizer
|
||||
else:
|
||||
raise ValueError("organizer not in context")
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if "sales_channels" in data:
|
||||
all_channels = {
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
}
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError(
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
)
|
||||
|
||||
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||
raise ValidationError(
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
)
|
||||
|
||||
if data["sales_channels"] == all_channels:
|
||||
data["all_sales_channels"] = True
|
||||
data["limit_sales_channels"] = []
|
||||
else:
|
||||
data["all_sales_channels"] = False
|
||||
data["limit_sales_channels"] = data["sales_channels"]
|
||||
del data["sales_channels"]
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
value = super().to_representation(value)
|
||||
if value.get("all_sales_channels"):
|
||||
value["sales_channels"] = sorted([
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
])
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
return value
|
||||
|
||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Seat, Voucher
|
||||
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
voucher = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -221,6 +225,10 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
|
||||
@@ -25,14 +25,20 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||
|
||||
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
@@ -43,6 +49,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
@@ -72,10 +80,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('subevent'):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError(_('Unknown sales channel.'))
|
||||
|
||||
CheckinList.validate_rules(data.get('rules'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -19,18 +19,27 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Discount
|
||||
from pretix.base.models import Discount, SalesChannel
|
||||
|
||||
|
||||
class DiscountSerializer(I18nAwareModelSerializer):
|
||||
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -46,10 +46,14 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models import (
|
||||
Device, Event, SalesChannel, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
@@ -161,7 +165,7 @@ class ValidKeysField(Field):
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
@@ -170,6 +174,13 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
def get_event_url(self, event):
|
||||
return build_absolute_uri(event, 'presale:event.index')
|
||||
@@ -180,7 +191,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||
'sales_channels', 'best_availability_state', 'public_url')
|
||||
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -188,6 +199,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state')
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -42,19 +42,27 @@ from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||
)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
@@ -63,11 +71,12 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = lazy(lambda: self.context['event'].organizer.sales_channels.all(), QuerySet)
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
@@ -76,10 +85,17 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
return value
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
@@ -88,11 +104,12 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
@@ -223,7 +240,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
@@ -232,11 +249,18 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
@@ -259,6 +283,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
if not self.read_only:
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -335,7 +360,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
item = Item.objects.create(**validated_data)
|
||||
if limit_sales_channels:
|
||||
item.limit_sales_channels.add(*limit_sales_channels)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
if require_membership_types:
|
||||
|
||||
@@ -46,13 +46,12 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -714,6 +713,11 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
url = OrderURLField(source='*', read_only=True)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -732,6 +736,10 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
@@ -1033,12 +1041,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
require_approval = serializers.BooleanField(default=False, required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||
self.fields['expires'].required = False
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -1059,11 +1073,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('Expiration date must be in the future.')
|
||||
return expires
|
||||
|
||||
def validate_sales_channel(self, channel):
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError('Unknown sales channel.')
|
||||
return channel
|
||||
|
||||
def validate_code(self, code):
|
||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||
raise ValidationError(
|
||||
@@ -1125,20 +1134,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(errs)
|
||||
return data
|
||||
|
||||
def validate_testmode(self, testmode):
|
||||
if 'sales_channel' in self.initial_data:
|
||||
try:
|
||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
||||
|
||||
if testmode and not sales_channel.testmode_supported:
|
||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
pass
|
||||
|
||||
return testmode
|
||||
|
||||
def create(self, validated_data):
|
||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
@@ -1147,9 +1142,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
|
||||
if not validated_data.get("sales_channel"):
|
||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||
|
||||
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
|
||||
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
|
||||
|
||||
self._send_mail = validated_data.pop('send_email', False)
|
||||
if self._send_mail is None:
|
||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1309,7 +1311,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
seat_usage[seat] += 1
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
||||
sales_channel_id = validated_data['sales_channel'].identifier
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
@@ -1368,6 +1371,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
if validated_data.get('locale', None) is None:
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
if not validated_data.get('expires'):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
|
||||
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -165,6 +165,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
|
||||
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||
type = serializers.CharField(default="api")
|
||||
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ('identifier', 'type', 'label', 'position')
|
||||
|
||||
def validate_type(self, value):
|
||||
if (not self.instance or not self.instance.pk) and value != "api":
|
||||
raise ValidationError(
|
||||
"You can currently only create channels of type 'api' through the API."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.type != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the type of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_identifier(self, value):
|
||||
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
|
||||
raise ValidationError(
|
||||
"Your identifier needs to start with 'api.'."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.identifier != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the identifier of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||
|
||||
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
||||
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
|
||||
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||
|
||||
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
|
||||
if validated_data.get('seat'):
|
||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
||||
if validated_data.get('sales_channel'):
|
||||
sales_channel_id = validated_data.get('sales_channel').identifier
|
||||
else:
|
||||
sales_channel_id = "web"
|
||||
if not validated_data['seat'].is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
sales_channel=sales_channel_id,
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||
):
|
||||
|
||||
@@ -115,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||
qs = qs.prefetch_related(
|
||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.all()
|
||||
return self.request.event.discounts.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
|
||||
@@ -113,7 +113,10 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(sales_channels__contains=value)
|
||||
return queryset.filter(
|
||||
Q(all_sales_channels=True) |
|
||||
Q(limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -135,6 +138,12 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('date_from', 'slug')
|
||||
filterset_class = EventFilter
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {
|
||||
**super().get_serializer_context(),
|
||||
"organizer": self.request.organizer,
|
||||
}
|
||||
|
||||
def get_copy_from_queryset(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_any_permission()
|
||||
@@ -160,6 +169,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'item_meta_properties',
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'seat_category_mappings',
|
||||
to_attr='_seat_category_mappings',
|
||||
@@ -268,8 +278,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.is_public = serializer.validated_data['is_public']
|
||||
if 'testmode' in serializer.validated_data:
|
||||
new_event.testmode = serializer.validated_data['testmode']
|
||||
if 'sales_channels' in serializer.validated_data:
|
||||
new_event.sales_channels = serializer.validated_data['sales_channels']
|
||||
if 'has_subevents' in serializer.validated_data:
|
||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||
if 'date_admission' in serializer.validated_data:
|
||||
@@ -277,6 +285,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.save()
|
||||
if 'timezone' in serializer.validated_data:
|
||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
||||
|
||||
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||
else:
|
||||
serializer.instance.set_defaults()
|
||||
|
||||
@@ -379,7 +391,10 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(event__sales_channels__contains=value)
|
||||
return queryset.filter(
|
||||
Q(event__all_sales_channels=True) |
|
||||
Q(event__limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
|
||||
@@ -87,6 +87,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||
'variations__meta_values', 'variations__meta_values__property',
|
||||
'require_membership_types', 'variations__require_membership_types',
|
||||
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||
).all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
@@ -152,7 +153,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
return self.item.variations.all().prefetch_related(
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'require_membership_types'
|
||||
'require_membership_types',
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@@ -229,7 +229,7 @@ class OrderViewSetMixin:
|
||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('customer')
|
||||
|
||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
|
||||
return qs
|
||||
|
||||
def _positions_prefetch(self, request):
|
||||
@@ -316,6 +316,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
|
||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
|
||||
@@ -43,13 +43,13 @@ from pretix.api.serializers.organizer import (
|
||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
||||
TeamMemberSerializer, TeamSerializer,
|
||||
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||
TeamInvite, User,
|
||||
)
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
@@ -675,3 +675,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class SalesChannelFilter(FilterSet):
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ['type', 'identifier']
|
||||
|
||||
|
||||
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SalesChannelSerializer
|
||||
queryset = SalesChannel.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = SalesChannelFilter
|
||||
lookup_field = 'identifier'
|
||||
lookup_url_kwarg = 'identifier'
|
||||
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.sales_channels.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(
|
||||
organizer=self.request.organizer,
|
||||
type="api"
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.saleschannel.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(
|
||||
type=serializer.instance.type,
|
||||
identifier=serializer.instance.identifier,
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.sales_channel.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
instance.log_action(
|
||||
'pretix.saleschannel.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
Reference in New Issue
Block a user