# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-today pretix GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # import logging from decimal import Decimal from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.db.models import Q from django.utils.crypto import get_random_string from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError from pretix.api.auth.devicesecurity import get_all_security_profiles from pretix.api.serializers import AsymmetricField from pretix.api.serializers.fields import PluginsField from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.order import CompatibleJSONField from pretix.api.serializers.settings import SettingsSerializer from pretix.base.auth import get_auth_backends from pretix.base.i18n import get_language_without_region from pretix.base.models import ( Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User, ) from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.permissions import ( get_all_event_permission_groups, get_all_organizer_permission_groups, ) from pretix.base.plugins import ( PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_ORGANIZER, ) from pretix.base.services.mail import mail from pretix.base.settings import validate_organizer_settings from pretix.helpers.permission_migration import ( OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION, ) from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.multidomain.urlreverse import build_absolute_uri logger = logging.getLogger(__name__) class OrganizerSerializer(I18nAwareModelSerializer): public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True) plugins = PluginsField(required=False, source='*') name = serializers.CharField(read_only=True) slug = serializers.CharField(read_only=True) def get_organizer_url(self, organizer): return build_absolute_uri(organizer, 'presale:organizer.index') class Meta: model = Organizer fields = ('name', 'slug', 'public_url', 'plugins') def validate_plugins(self, value): from pretix.base.plugins import get_all_plugins plugins_available = { p.module: p for p in get_all_plugins(organizer=self.instance) if not p.name.startswith('.') and getattr(p, 'visible', True) } settings_holder = self.instance allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID) for plugin in value.get('plugins'): if plugin not in plugins_available: raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin)) if getattr(plugins_available[plugin], 'restricted', False): if plugin not in settings_holder.settings.allowed_restricted_plugins: raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin)) if getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT) not in allowed_levels: raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin)) return value @transaction.atomic def update(self, instance, validated_data): plugins = validated_data.pop('plugins', None) organizer = super().update(instance, validated_data) # Plugins if plugins is not None: organizer.set_active_plugins(plugins) organizer.save() return organizer class SeatingPlanSerializer(I18nAwareModelSerializer): layout = CompatibleJSONField( validators=[SeatingPlanLayoutValidator()] ) class Meta: model = SeatingPlan fields = ('id', 'name', 'layout') class CustomerSerializer(I18nAwareModelSerializer): identifier = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True) last_login = serializers.DateTimeField(read_only=True) date_joined = serializers.DateTimeField(read_only=True) last_modified = serializers.DateTimeField(read_only=True) locale = serializers.ChoiceField(choices=settings.LANGUAGES, default='en') class Meta: model = Customer fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes') def update(self, instance, validated_data): if instance and instance.provider_id: validated_data['external_identifier'] = instance.external_identifier return super().update(instance, validated_data) def validate(self, data): if data.get('name_parts') and not isinstance(data.get('name_parts'), dict): raise ValidationError({'name_parts': ['Invalid data type']}) if data.get('name_parts') and '_scheme' not in data.get('name_parts'): data['name_parts']['_scheme'] = self.context['request'].organizer.settings.name_scheme return data def validate_email(self, value): qs = Customer.objects.filter(organizer=self.context['organizer'], email__iexact=value) if self.instance and self.instance.pk: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): raise ValidationError(_("An account with this email address is already registered.")) return value class CustomerCreateSerializer(CustomerSerializer): send_email = serializers.BooleanField(default=False, required=False, allow_null=True) password = serializers.CharField(write_only=True, required=False, allow_null=True) class Meta: model = Customer fields = CustomerSerializer.Meta.fields + ('send_email', 'password') class MembershipTypeSerializer(I18nAwareModelSerializer): class Meta: model = MembershipType fields = ('id', 'name', 'transferable', 'allow_parallel_usage', 'max_usages') class MembershipSerializer(I18nAwareModelSerializer): customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none()) class Meta: model = Membership fields = ('id', 'testmode', 'customer', 'membership_type', 'date_start', 'date_end', 'attendee_name_parts') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['customer'].queryset = self.context['organizer'].customers.all() self.fields['membership_type'].queryset = self.context['organizer'].membership_types.all() def update(self, instance, validated_data): validated_data['customer'] = instance.customer # no modifying validated_data['testmode'] = instance.testmode # no modifying return super().update(instance, validated_data) class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField): def to_internal_value(self, data): queryset = self.get_queryset() if isinstance(data, int): try: return queryset.get(pk=data) except ObjectDoesNotExist: self.fail('does_not_exist', pk_value=data) elif isinstance(data, str): try: return queryset.get( Q(secret=data) | Q(pseudonymization_id=data) | Q(pk__in=ReusableMedium.objects.filter( organizer=self.context['organizer'], type='barcode', identifier=data )) ) except ObjectDoesNotExist: self.fail('does_not_exist', pk_value=data) 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()) issuer = serializers.SlugRelatedField(slug_field='slug', read_only=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['owner_ticket'].queryset = OrderPosition.objects.filter(order__event__organizer=self.context['organizer']) if 'owner_ticket' in self.context['request'].query_params.getlist('expand'): from pretix.api.serializers.media import ( NestedOrderPositionSerializer, ) self.fields['owner_ticket'] = AsymmetricField( NestedOrderPositionSerializer(read_only=True, context=self.context), self.fields['owner_ticket'], ) def validate(self, data): data = super().validate(data) if 'secret' in data: s = data['secret'] qs = GiftCard.objects.filter( secret=s ).filter( Q(issuer=self.context["organizer"]) | Q(issuer__in=GiftCardAcceptance.objects.filter( acceptor=self.context["organizer"], active=True, ).values_list('issuer', flat=True)) ) if self.instance: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): raise ValidationError( {'secret': _( 'A gift card with the same secret already exists in your or an affiliated organizer account.')} ) return data class Meta: model = GiftCard fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket', 'issuer') class OrderEventSlugField(serializers.RelatedField): def to_representation(self, obj): return obj.event.slug class GiftCardTransactionSerializer(I18nAwareModelSerializer): order = serializers.SlugRelatedField(slug_field='code', read_only=True) acceptor = serializers.SlugRelatedField(slug_field='slug', read_only=True) event = OrderEventSlugField(source='order', read_only=True) class Meta: model = GiftCardTransaction fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor') class EventSlugField(serializers.SlugRelatedField): def get_queryset(self): return self.context['organizer'].events.all() class PermissionMultipleChoiceField(serializers.MultipleChoiceField): def to_internal_value(self, data): return { p: True for p in super().to_internal_value(data) } def to_representation(self, value): return [p for p, v in value.items() if v] class TeamSerializer(serializers.ModelSerializer): limit_events = EventSlugField(slug_field='slug', many=True) limit_event_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True) limit_organizer_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True) # Legacy fields, handled in to_representation and validate can_change_event_settings = serializers.BooleanField(required=False, write_only=True) can_change_items = serializers.BooleanField(required=False, write_only=True) can_view_orders = serializers.BooleanField(required=False, write_only=True) can_change_orders = serializers.BooleanField(required=False, write_only=True) can_checkin_orders = serializers.BooleanField(required=False, write_only=True) can_view_vouchers = serializers.BooleanField(required=False, write_only=True) can_change_vouchers = serializers.BooleanField(required=False, write_only=True) can_create_events = serializers.BooleanField(required=False, write_only=True) can_change_organizer_settings = serializers.BooleanField(required=False, write_only=True) can_change_teams = serializers.BooleanField(required=False, write_only=True) can_manage_gift_cards = serializers.BooleanField(required=False, write_only=True) can_manage_customers = serializers.BooleanField(required=False, write_only=True) can_manage_reusable_media = serializers.BooleanField(required=False, write_only=True) class Meta: model = Team fields = ( 'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'all_event_permissions', 'limit_event_permissions', 'all_organizer_permissions', 'limit_organizer_permissions', 'can_change_event_settings', 'can_change_items', 'can_view_orders', 'can_change_orders', 'can_checkin_orders', 'can_view_vouchers', 'can_change_vouchers', 'can_create_events', 'can_change_organizer_settings', 'can_change_teams', 'can_manage_gift_cards', 'can_manage_customers', 'can_manage_reusable_media' ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) event_perms_flattened = [] organizer_perms_flattened = [] for pg in get_all_event_permission_groups().values(): for action in pg.actions: event_perms_flattened.append(f"{pg.name}:{action}") for pg in get_all_organizer_permission_groups().values(): for action in pg.actions: organizer_perms_flattened.append(f"{pg.name}:{action}") self.fields['limit_event_permissions'].choices = [(p, p) for p in event_perms_flattened] self.fields['limit_organizer_permissions'].choices = [(p, p) for p in organizer_perms_flattened] def to_representation(self, instance): r = super().to_representation(instance) for old, new in OLD_TO_NEW_EVENT_COMPAT.items(): r[old] = instance.all_event_permissions or all(instance.limit_event_permissions.get(n) for n in new) for old, new in OLD_TO_NEW_ORGANIZER_COMPAT.items(): r[old] = instance.all_organizer_permissions or all(instance.limit_organizer_permissions.get(n) for n in new) return r def validate(self, data): old_data_set = any(k.startswith("can_") for k in data) new_data_set = any(k in data for k in [ "all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions" ]) if old_data_set and new_data_set: raise ValidationError("You cannot set deprecated and current permission attributes at the same time.") full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} full_data.update(data) if new_data_set: if full_data.get('limit_event_permissions') and full_data.get('all_event_permissions'): raise ValidationError('Do not set both limit_event_permissions and all_event_permissions.') if full_data.get('limit_organizer_permissions') and full_data.get('all_organizer_permissions'): raise ValidationError('Do not set both limit_organizer_permissions and all_organizer_permissions.') if old_data_set: # Migrate with same logic as in migration 0297_plugable_permissions if all(full_data.get(k) is True for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"): data["all_event_permissions"] = True data["limit_event_permissions"] = {} else: data["all_event_permissions"] = False data["limit_event_permissions"] = {} for k, v in OLD_TO_NEW_EVENT_MIGRATION.items(): if full_data.get(k) is True: data["limit_event_permissions"].update({kk: True for kk in v}) if all(full_data.get(k) is True for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys() if k != "can_checkin_orders"): data["all_organizer_permissions"] = True data["limit_organizer_permissions"] = {} else: data["all_organizer_permissions"] = False data["limit_organizer_permissions"] = {} for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items(): if full_data.get(k) is True: data["limit_organizer_permissions"].update({kk: True for kk in v}) if full_data.get('limit_events') and full_data.get('all_events'): raise ValidationError('Do not set both limit_events and all_events.') full_data.update(data) for pg in get_all_event_permission_groups().values(): requested = ",".join(sorted( a for a in pg.actions if self.instance and full_data["limit_event_permissions"].get(f"{pg.name}:{a}") )) if requested not in (",".join(sorted(opt.actions)) for opt in pg.options): raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are " f"'{'\' or \''.join(','.join(opt.actions) for opt in pg.options)}' but you tried to " f"set '{requested}'.") for pg in get_all_organizer_permission_groups().values(): requested = ",".join(sorted( a for a in pg.actions if self.instance and full_data["limit_organizer_permissions"].get(f"{pg.name}:{a}") )) if requested not in (",".join(sorted(opt.actions)) for opt in pg.options): raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are " f"'{'\' or \''.join(','.join(opt.actions) for opt in pg.options)}' but you tried to " f"set '{requested}'.") return data class DeviceSerializer(serializers.ModelSerializer): limit_events = EventSlugField(slug_field='slug', many=True) device_id = serializers.IntegerField(read_only=True) unique_serial = serializers.CharField(read_only=True) hardware_brand = serializers.CharField(read_only=True) hardware_model = serializers.CharField(read_only=True) os_name = serializers.CharField(read_only=True) os_version = serializers.CharField(read_only=True) software_brand = serializers.CharField(read_only=True) software_version = serializers.CharField(read_only=True) created = serializers.DateTimeField(read_only=True) revoked = serializers.BooleanField(read_only=True) initialized = serializers.DateTimeField(read_only=True) initialization_token = serializers.CharField(read_only=True) security_profile = serializers.ChoiceField(choices=[], required=False, default="full") class Meta: model = Device fields = ( 'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events', 'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model', 'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile' ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()] if not self.context['can_see_tokens']: del self.fields['initialization_token'] class TeamInviteSerializer(serializers.ModelSerializer): class Meta: model = TeamInvite fields = ( 'id', 'email' ) def _send_invite(self, instance): mail( instance.email, _('Account invitation'), 'pretixcontrol/email/invitation.txt', { 'instance': settings.PRETIX_INSTANCE_NAME, 'user': self, 'organizer': self.context['organizer'].name, 'team': instance.team.name, 'url': build_global_uri('control:auth.invite', kwargs={ 'token': instance.token }) }, event=None, locale=get_language_without_region() # TODO: expose? ) def create(self, validated_data): if 'email' in validated_data: try: user = User.objects.get(email__iexact=validated_data['email']) except User.DoesNotExist: if self.context['team'].invites.filter(email__iexact=validated_data['email']).exists(): raise ValidationError(_('This user already has been invited for this team.')) if 'native' not in get_auth_backends(): raise ValidationError('Users need to have a pretix account before they can be invited.') invite = self.context['team'].invites.create(email=validated_data['email']) self._send_invite(invite) invite.team.log_action( 'pretix.team.invite.created', data={ 'email': validated_data['email'] }, **self.context['log_kwargs'] ) return invite else: if self.context['team'].members.filter(pk=user.pk).exists(): raise ValidationError(_('This user already has permissions for this team.')) self.context['team'].members.add(user) self.context['team'].log_action( 'pretix.team.member.added', data={ 'email': user.email, 'user': user.pk, }, **self.context['log_kwargs'] ) return TeamInvite(email=user.email) else: raise ValidationError('No email address given.') class TeamAPITokenSerializer(serializers.ModelSerializer): active = serializers.BooleanField(default=True, read_only=True) class Meta: model = TeamAPIToken fields = ( 'id', 'name', 'active' ) class TeamMemberSerializer(serializers.ModelSerializer): class Meta: model = User fields = ( 'id', 'email', 'fullname', 'require_2fa' ) class OrganizerSettingsSerializer(SettingsSerializer): default_write_permission = 'organizer.settings.general:write' default_fields = [ # These are readable for all users with access to the events, therefore secrets made in the settings store # should not be included! 'customer_accounts', 'customer_accounts_native', 'customer_accounts_link_by_email', 'customer_accounts_require_login_for_order_access', 'invoice_regenerate_allowed', 'contact_mail', 'imprint_url', 'organizer_info_text', 'event_list_type', 'event_list_availability', 'organizer_homepage_text', 'organizer_link_back', 'organizer_logo_image_large', 'giftcard_length', 'giftcard_expiry_years', 'locales', 'region', 'event_team_provisioning', 'primary_color', 'theme_color_success', 'theme_color_danger', 'theme_color_background', 'theme_round_borders', 'primary_font', 'organizer_logo_image_inherit', 'organizer_logo_image', 'privacy_url', 'accessibility_url', 'accessibility_title', 'accessibility_text', 'cookie_consent', 'cookie_consent_dialog_title', 'cookie_consent_dialog_text', 'cookie_consent_dialog_text_secondary', 'cookie_consent_dialog_button_yes', 'cookie_consent_dialog_button_no', 'reusable_media_active', 'reusable_media_type_barcode', 'reusable_media_type_barcode_identifier_length', 'reusable_media_type_nfc_uid', 'reusable_media_type_nfc_uid_autocreate_giftcard', 'reusable_media_type_nfc_uid_autocreate_giftcard_currency', 'reusable_media_type_nfc_mf0aes', 'reusable_media_type_nfc_mf0aes_autocreate_giftcard', 'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency', 'reusable_media_type_nfc_mf0aes_random_uid', ] def __init__(self, *args, **kwargs): self.organizer = kwargs.pop('organizer') super().__init__(*args, **kwargs) def validate(self, data): data = super().validate(data) settings_dict = self.instance.freeze() settings_dict.update(data) validate_organizer_settings(self.organizer, settings_dict) return data def get_new_filename(self, name: str) -> str: nonce = get_random_string(length=8) fname = '%s/%s.%s.%s' % ( self.organizer.slug, name.split('/')[-1], nonce, name.split('.')[-1] ) # TODO: make sure pub is always correct return 'pub/' + fname