from decimal import Decimal from django.core.exceptions import ValidationError from django.db import transaction from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from pretix.api.serializers.event import MetaDataField from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.base.models import ( Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation, Question, QuestionOption, Quota, ) class InlineItemVariationSerializer(I18nAwareModelSerializer): price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10, coerce_to_string=True) class Meta: model = ItemVariation fields = ('id', 'value', 'active', 'description', 'position', 'default_price', 'price', 'original_price') class ItemVariationSerializer(I18nAwareModelSerializer): price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10, coerce_to_string=True) class Meta: model = ItemVariation fields = ('id', 'value', 'active', 'description', 'position', 'default_price', 'price', 'original_price') class InlineItemBundleSerializer(serializers.ModelSerializer): class Meta: model = ItemBundle fields = ('bundled_item', 'bundled_variation', 'count', 'designated_price') class InlineItemAddOnSerializer(serializers.ModelSerializer): class Meta: model = ItemAddOn fields = ('addon_category', 'min_count', 'max_count', 'position', 'price_included', 'multi_allowed') class ItemBundleSerializer(serializers.ModelSerializer): class Meta: model = ItemBundle fields = ('id', 'bundled_item', 'bundled_variation', 'count', 'designated_price') def validate(self, data): data = super().validate(data) event = self.context['event'] full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} full_data.update(data) ItemBundle.clean_itemvar(event, full_data.get('bundled_item'), full_data.get('bundled_variation')) item = self.context['item'] if item == full_data.get('bundled_item'): raise ValidationError(_("The bundled item must not be the same item as the bundling one.")) if full_data.get('bundled_item'): if full_data['bundled_item'].bundles.exists(): raise ValidationError(_("The bundled item must not have bundles on its own.")) return data class ItemAddOnSerializer(serializers.ModelSerializer): class Meta: model = ItemAddOn fields = ('id', 'addon_category', 'min_count', 'max_count', 'position', 'price_included', 'multi_allowed') def validate(self, data): data = super().validate(data) ItemAddOn.clean_max_min_count(data.get('max_count'), data.get('min_count')) return data def validate_min_count(self, value): ItemAddOn.clean_min_count(value) return value def validate_max_count(self, value): ItemAddOn.clean_max_count(value) return value def validate_addon_category(self, value): ItemAddOn.clean_categories(self.context['event'], self.context['item'], self.instance, value) return value class ItemTaxRateField(serializers.Field): def to_representation(self, i): if i.tax_rule: return str(Decimal(i.tax_rule.rate)) else: return str(Decimal('0.00')) class ItemSerializer(I18nAwareModelSerializer): addons = InlineItemAddOnSerializer(many=True, required=False) bundles = InlineItemBundleSerializer(many=True, required=False) variations = InlineItemVariationSerializer(many=True, required=False) tax_rate = ItemTaxRateField(source='*', read_only=True) meta_data = MetaDataField(required=False, source='*') class Meta: model = Item fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description', 'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission', 'position', 'picture', 'available_from', 'available_until', 'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling', 'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations', 'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets', 'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data') read_only_fields = ('has_variations', 'picture') def validate(self, data): data = super().validate(data) if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data): raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the ' 'dedicated nested endpoint.')) Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order')) Item.clean_available(data.get('available_from'), data.get('available_until')) if data.get('issue_giftcard'): if data.get('tax_rule') and data.get('tax_rule').rate > 0: raise ValidationError( _("Gift card products should not be associated with non-zero tax rates since sales tax will be " "applied when the gift card is redeemed.") ) if data.get('admission'): raise ValidationError(_( "Gift card products should not be admission products at the same time." )) return data def validate_category(self, value): Item.clean_category(value, self.context['event']) return value def validate_tax_rule(self, value): Item.clean_tax_rule(value, self.context['event']) return value def validate_bundles(self, value): if not self.instance: for b_data in value: ItemBundle.clean_itemvar(self.context['event'], b_data['bundled_item'], b_data['bundled_variation']) return value def validate_addons(self, value): if not self.instance: for addon_data in value: ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category']) ItemAddOn.clean_min_count(addon_data['min_count']) ItemAddOn.clean_max_count(addon_data['max_count']) ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count']) return value @cached_property def item_meta_properties(self): return { p.name: p for p in self.context['request'].event.item_meta_properties.all() } def validate_meta_data(self, value): for key in value['meta_data'].keys(): if key not in self.item_meta_properties: raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key)) return value @transaction.atomic def create(self, validated_data): variations_data = validated_data.pop('variations') if 'variations' in validated_data else {} addons_data = validated_data.pop('addons') if 'addons' in validated_data else {} bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {} meta_data = validated_data.pop('meta_data', None) item = Item.objects.create(**validated_data) for variation_data in variations_data: ItemVariation.objects.create(item=item, **variation_data) for addon_data in addons_data: ItemAddOn.objects.create(base_item=item, **addon_data) for bundle_data in bundles_data: ItemBundle.objects.create(base_item=item, **bundle_data) # Meta data if meta_data is not None: for key, value in meta_data.items(): ItemMetaValue.objects.create( property=self.item_meta_properties.get(key), value=value, item=item ) return item def update(self, instance, validated_data): meta_data = validated_data.pop('meta_data', None) item = super().update(instance, validated_data) # Meta data if meta_data is not None: current = {mv.property: mv for mv in item.meta_values.select_related('property')} for key, value in meta_data.items(): prop = self.item_meta_properties.get(key) if prop in current: current[prop].value = value current[prop].save() else: item.meta_values.create( property=self.item_meta_properties.get(key), value=value ) for prop, current_object in current.items(): if prop.name not in meta_data: current_object.delete() return item class ItemCategorySerializer(I18nAwareModelSerializer): class Meta: model = ItemCategory fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon') class QuestionOptionSerializer(I18nAwareModelSerializer): identifier = serializers.CharField(allow_null=True) class Meta: model = QuestionOption fields = ('id', 'identifier', 'answer', 'position') def validate_identifier(self, value): QuestionOption.clean_identifier(self.context['event'], value, self.instance) return value class InlineQuestionOptionSerializer(I18nAwareModelSerializer): identifier = serializers.CharField(allow_null=True) class Meta: model = QuestionOption fields = ('id', 'identifier', 'answer', 'position') class LegacyDependencyValueField(serializers.CharField): def to_representation(self, obj): return obj[0] if obj else None def to_internal_value(self, data): return [data] if data else [] class QuestionSerializer(I18nAwareModelSerializer): options = InlineQuestionOptionSerializer(many=True, required=False) identifier = serializers.CharField(allow_null=True) dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True) class Meta: model = Question fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position', 'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values', 'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min', 'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max' ) def validate_identifier(self, value): Question._clean_identifier(self.context['event'], value, self.instance) return value def validate_dependency_question(self, value): if value: if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE): raise ValidationError('Question dependencies can only be set to boolean or choice questions.') if value == self.instance: raise ValidationError('A question cannot depend on itself.') return value def validate(self, data): data = super().validate(data) if self.instance and 'options' in data: raise ValidationError(_('Updating options via PATCH/PUT is not supported. Please use the dedicated' ' nested endpoint.')) event = self.context['event'] full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} full_data.update(data) if full_data.get('ask_during_checkin') and full_data.get('dependency_question'): raise ValidationError('Dependencies are not supported during check-in.') dep = full_data.get('dependency_question') if dep: if dep.ask_during_checkin: raise ValidationError(_('Question cannot depend on a question asked during check-in.')) seen_ids = {self.instance.pk} if self.instance else set() while dep: if dep.pk in seen_ids: raise ValidationError(_('Circular dependency between questions detected.')) seen_ids.add(dep.pk) dep = dep.dependency_question if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED: raise ValidationError(_('This type of question cannot be asked during check-in.')) Question.clean_items(event, full_data.get('items')) return data def validate_options(self, value): if not self.instance: known = [] for opt_data in value: if opt_data.get('identifier'): QuestionOption.clean_identifier(self.context['event'], opt_data.get('identifier'), self.instance, known) known.append(opt_data.get('identifier')) return value @transaction.atomic def create(self, validated_data): options_data = validated_data.pop('options') if 'options' in validated_data else [] items = validated_data.pop('items') question = Question.objects.create(**validated_data) question.items.set(items) for opt_data in options_data: QuestionOption.objects.create(question=question, **opt_data) return question class QuotaSerializer(I18nAwareModelSerializer): class Meta: model = Quota fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit') def validate(self, data): data = super().validate(data) event = self.context['event'] full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} full_data.update(data) Quota.clean_variations(full_data.get('items'), full_data.get('variations')) Quota.clean_items(event, full_data.get('items'), full_data.get('variations')) Quota.clean_subevent(event, full_data.get('subevent')) return data