mirror of
https://github.com/pretix/pretix.git
synced 2026-10-02 19:24:42 +00:00
CompatQuestionViewset / Serializer
This commit is contained in:
@@ -100,6 +100,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:questionnaire-list'),
|
||||
('GET', 'api-v1:questionnairechild-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
@@ -141,6 +143,8 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:questionnaire-list'),
|
||||
('GET', 'api-v1:questionnairechild-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
@@ -177,6 +181,8 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:questionnaire-list'),
|
||||
('GET', 'api-v1:questionnairechild-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
|
||||
@@ -933,14 +933,14 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'show_quota_left',
|
||||
'show_dates_on_frontpage',
|
||||
'max_items_per_order',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
#'attendee_names_asked',
|
||||
#'attendee_names_required',
|
||||
#'attendee_emails_asked',
|
||||
#'attendee_emails_required',
|
||||
#'attendee_addresses_asked',
|
||||
#'attendee_addresses_required',
|
||||
#'attendee_company_asked',
|
||||
#'attendee_company_required',
|
||||
'ticket_download',
|
||||
'ticket_download_addons',
|
||||
'ticket_download_nonadm',
|
||||
@@ -971,7 +971,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'reusable_media_type_nfc_mf0aes',
|
||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||
'reusable_media_usage_enforced',
|
||||
'system_question_order', # TODO(questionnaires) - remove or replace
|
||||
# 'system_question_order', # TODO(questionnaires) - remove or replace
|
||||
'tax_rule_payment',
|
||||
'tax_rule_cancellation',
|
||||
]
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import os.path
|
||||
from decimal import Decimal
|
||||
|
||||
from i18nfield.rest_framework import I18nField
|
||||
from itertools import zip_longest
|
||||
import logging
|
||||
|
||||
@@ -44,6 +46,8 @@ from django.db.models import QuerySet
|
||||
from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DrfValidationError
|
||||
from rest_framework.serializers import as_serializer_error
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatDecimalField, SalesChannelMigrationMixin,
|
||||
@@ -546,6 +550,100 @@ class LegacyDependencyValueField(serializers.CharField):
|
||||
return [data] if data else []
|
||||
|
||||
|
||||
|
||||
system_datafield_numbers = [
|
||||
QuestionnaireChild.SystemQuestion.ATTENDEE_NAME_PARTS.value,
|
||||
QuestionnaireChild.SystemQuestion.ATTENDEE_EMAIL.value,
|
||||
QuestionnaireChild.SystemQuestion.COMPANY.value,
|
||||
QuestionnaireChild.SystemQuestion.STREET.value,
|
||||
QuestionnaireChild.SystemQuestion.ZIPCODE.value,
|
||||
QuestionnaireChild.SystemQuestion.CITY.value,
|
||||
QuestionnaireChild.SystemQuestion.STATE.value,
|
||||
QuestionnaireChild.SystemQuestion.COUNTRY.value,
|
||||
]
|
||||
|
||||
|
||||
def cook_id(questionnaire_child):
|
||||
df_id = questionnaire_child.user_datafield_id + 100 if questionnaire_child.user_datafield_id else system_datafield_numbers.index(questionnaire_child.system_datafield)
|
||||
assert questionnaire_child.questionnaire_id < 1000000000
|
||||
return questionnaire_child.questionnaire_id * 1000000000 + df_id
|
||||
|
||||
|
||||
def uncook_id(id):
|
||||
id = int(id)
|
||||
if id > 1000000000:
|
||||
qid, dfid = divmod(id, 1000000000)
|
||||
if dfid >= 100:
|
||||
return qid, dfid - 100, None
|
||||
else:
|
||||
return qid, None, system_datafield_numbers[dfid]
|
||||
else:
|
||||
return None, id, None
|
||||
|
||||
|
||||
class CompatQuestionSerializer(I18nAwareModelSerializer):
|
||||
id = serializers.SerializerMethodField('cook_id', read_only=True)
|
||||
question = I18nField(source='label', allow_null=True)
|
||||
type = serializers.CharField(source='user_datafield.type', allow_null=True, default=Question.FieldType.TEXT)
|
||||
#identifier = serializers.CharField(source='user_datafield.identifier', allow_null=True)
|
||||
identifier = serializers.SerializerMethodField('get_identifier', read_only=True)
|
||||
show_during_checkin = serializers.BooleanField(source='user_datafield.show_during_checkin', allow_null=True, default=False)
|
||||
ask_during_checkin = serializers.SerializerMethodField('get_ask_during_checkin', read_only=True)
|
||||
hidden = serializers.SerializerMethodField('get_hidden', read_only=True)
|
||||
print_on_invoice = serializers.BooleanField(source='user_datafield.print_on_invoice', allow_null=True, default=False)
|
||||
valid_number_min = serializers.IntegerField(source='user_datafield.valid_number_min', allow_null=True)
|
||||
valid_number_max = serializers.IntegerField(source='user_datafield.valid_number_max', allow_null=True)
|
||||
valid_date_min = serializers.DateField(source='user_datafield.valid_date_min', allow_null=True)
|
||||
valid_date_max = serializers.DateField(source='user_datafield.valid_date_max', allow_null=True)
|
||||
valid_datetime_min = serializers.DateTimeField(source='user_datafield.valid_datetime_min', allow_null=True)
|
||||
valid_datetime_max = serializers.DateTimeField(source='user_datafield.valid_datetime_max', allow_null=True)
|
||||
valid_string_length_max = serializers.IntegerField(source='user_datafield.valid_string_length_max', allow_null=True)
|
||||
valid_string_length_min = serializers.IntegerField(source='user_datafield.valid_string_length_min', allow_null=True)
|
||||
valid_file_portrait = serializers.BooleanField(source='user_datafield.valid_file_portrait', allow_null=True)
|
||||
dependency_question = serializers.SerializerMethodField('cook_dependency_question_id', read_only=True)
|
||||
dependency_value = serializers.SerializerMethodField('empty', read_only=True)
|
||||
position = serializers.SerializerMethodField('get_position', read_only=True)
|
||||
items = serializers.PrimaryKeyRelatedField(source='questionnaire.items', many=True, queryset=Item.objects.none(), allow_null=True, required=False)
|
||||
|
||||
options = InlineQuestionOptionSerializer(source='user_datafield.options', many=True, required=False, default=[])
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'show_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',
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['items'].child_relation.queryset = kwargs['context']['event'].items.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def cook_id(self, qc):
|
||||
return cook_id(qc)
|
||||
|
||||
def cook_dependency_question_id(self, qc):
|
||||
if qc.dependency_question:
|
||||
return cook_id(qc.dependency_question)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_ask_during_checkin(self, qc):
|
||||
return qc.questionnaire.type == Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
|
||||
def get_hidden(self, qc):
|
||||
return qc.questionnaire.type == Questionnaire.QuestionnaireType.ORDER_POSITION_HIDDEN
|
||||
|
||||
def get_identifier(self, qc):
|
||||
return qc.user_datafield.identifier if qc.user_datafield else f'@{qc.system_datafield}'
|
||||
|
||||
def empty(self, qc):
|
||||
return None
|
||||
|
||||
def get_position(self, qc):
|
||||
return qc.questionnaire.position * 1000000000 + qc.position
|
||||
|
||||
|
||||
class DatafieldSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
@@ -553,7 +651,7 @@ class DatafieldSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'options',
|
||||
fields = ('id', 'type', 'options',
|
||||
'show_during_checkin', 'identifier',
|
||||
'hidden', 'print_on_invoice', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
@@ -649,10 +747,11 @@ class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
question = QuestionRefField(allow_null=True, source='*', queryset=Question.objects.none())
|
||||
dependency_question = QuestionRefField(allow_null=True, required=False, queryset=Question.objects.none())
|
||||
rendered_help_text = RenderedMarkdownField(read_only=True, source='help_text')
|
||||
cooked_id = serializers.SerializerMethodField('cook_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values', 'rendered_help_text')
|
||||
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values', 'rendered_help_text', 'cooked_id')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -666,6 +765,9 @@ class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
return value
|
||||
|
||||
def cook_id(self, qc):
|
||||
return cook_id(qc)
|
||||
|
||||
|
||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
items = serializers.PrimaryKeyRelatedField(many=True, queryset=Item.objects.none(), allow_null=True, required=False)
|
||||
@@ -728,7 +830,7 @@ class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
questionnaire = super().create(validated_data)
|
||||
self.set_children(questionnaire, children_data)
|
||||
try:
|
||||
questionnaire.check_constraints()
|
||||
Questionnaire.check_constraints(questionnaire.event)
|
||||
except ValidationError as e:
|
||||
raise DrfValidationError(as_serializer_error(e))
|
||||
return questionnaire
|
||||
@@ -740,7 +842,7 @@ class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
if children_data is not None:
|
||||
self.set_children(questionnaire, children_data)
|
||||
try:
|
||||
questionnaire.check_constraints()
|
||||
Questionnaire.check_constraints(questionnaire.event)
|
||||
except ValidationError as e:
|
||||
raise DrfValidationError(as_serializer_error(e))
|
||||
return questionnaire
|
||||
|
||||
@@ -82,6 +82,7 @@ event_router.register(r'items', item.ItemViewSet)
|
||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'datafields', item.DatafieldViewSet)
|
||||
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
|
||||
event_router.register(r'questions', item.CompatQuestionViewSet)
|
||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||
event_router.register(r'quotas', item.QuotaViewSet)
|
||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
|
||||
@@ -46,17 +46,17 @@ from rest_framework.response import Response
|
||||
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.item import (
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
CompatQuestionSerializer, ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||
QuestionnaireSerializer, QuestionOptionSerializer, DatafieldSerializer,
|
||||
QuotaSerializer,
|
||||
QuotaSerializer, uncook_id,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
||||
ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire
|
||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
@@ -460,7 +460,113 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class QuestionFilter(FilterSet):
|
||||
class CompatQuestionFilter(FilterSet):
|
||||
identifier = django_filters.CharFilter(field_name='question__identifier')
|
||||
ask_during_checkin = django_filters.BooleanFilter(method='ask_during_checkin_qs')
|
||||
sales_channel = django_filters.CharFilter(method='sales_channel_qs')
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ['required']
|
||||
|
||||
def ask_during_checkin_qs(self, qs, name, value):
|
||||
return qs.filter(
|
||||
questionnaire__type=Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN if value else Questionnaire.QuestionnaireType.ORDER_POSITION_SALE
|
||||
)
|
||||
|
||||
def sales_channel_qs(self, qs, name, value):
|
||||
return qs.filter(
|
||||
Q(questionnaire__all_sales_channels=True) | Q(questionnaire__limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
|
||||
class CompatQuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = CompatQuestionSerializer
|
||||
queryset = QuestionnaireChild.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
filterset_class = CompatQuestionFilter
|
||||
ordering_fields = ('questionnaire__position', 'position', 'id')
|
||||
ordering = ('questionnaire__position', 'position', 'id')
|
||||
permission = None
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return QuestionnaireChild.objects.filter(
|
||||
questionnaire__event=self.request.event,
|
||||
# legacy compat only for position-level questions
|
||||
questionnaire__type__startswith='P',
|
||||
).prefetch_related('user_datafield', 'user_datafield__options', 'questionnaire').all()
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Returns the object the view is displaying.
|
||||
|
||||
You may want to override this if you need to provide non-standard
|
||||
queryset lookups. Eg if objects are referenced using multiple
|
||||
keyword arguments in the url conf.
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Perform the lookup filtering.
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
assert lookup_url_kwarg in self.kwargs, (
|
||||
'Expected view %s to be called with a URL keyword argument '
|
||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||
'attribute on the view correctly.' %
|
||||
(self.__class__.__name__, lookup_url_kwarg)
|
||||
)
|
||||
|
||||
qid, dfid, sys_df = uncook_id(self.kwargs[lookup_url_kwarg])
|
||||
if qid and sys_df:
|
||||
obj = get_object_or_404(queryset, questionnaire__pk=qid, system_datafield=sys_df)
|
||||
elif qid and dfid:
|
||||
obj = get_object_or_404(queryset, questionnaire__pk=qid, user_datafield__pk=dfid)
|
||||
else:
|
||||
obj = get_object_or_404(queryset, user_datafield__pk=dfid)
|
||||
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, obj)
|
||||
|
||||
return obj
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.question.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.question.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.question.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class DatafieldFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ['identifier']
|
||||
@@ -470,7 +576,7 @@ class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = DatafieldSerializer
|
||||
queryset = Question.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
filterset_class = QuestionFilter
|
||||
filterset_class = DatafieldFilter
|
||||
ordering_fields = ('id')
|
||||
ordering = ('id')
|
||||
permission = None
|
||||
@@ -570,9 +676,16 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
with scopes_disabled():
|
||||
class QuestionnaireFilter(FilterSet):
|
||||
sales_channel = django_filters.CharFilter(method='sales_channel_qs')
|
||||
|
||||
class Meta:
|
||||
model = Questionnaire
|
||||
fields = ['type']
|
||||
fields = ['type', 'items', 'sales_channel']
|
||||
|
||||
def sales_channel_qs(self, qs, name, value):
|
||||
return qs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
|
||||
class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
|
||||
@@ -1675,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
elif system_datafield_name == 'state':
|
||||
return self.state
|
||||
elif system_datafield_name == 'country':
|
||||
return self.country
|
||||
return str(self.country)
|
||||
else:
|
||||
raise ValueError('Unknown system question name')
|
||||
|
||||
|
||||
+14
-14
@@ -379,12 +379,12 @@ DEFAULTS = {
|
||||
|
||||
)
|
||||
},
|
||||
'system_question_order': { # TODO(questionnaires) - remove this
|
||||
'default': {},
|
||||
'type': dict,
|
||||
'serializer_class': serializers.DictField,
|
||||
'serializer_kwargs': lambda: dict(read_only=True, allow_empty=True),
|
||||
},
|
||||
# 'system_question_order': { # TODO(questionnaires) - remove this
|
||||
# 'default': {},
|
||||
# 'type': dict,
|
||||
# 'serializer_class': serializers.DictField,
|
||||
# 'serializer_kwargs': lambda: dict(read_only=True, allow_empty=True),
|
||||
# },
|
||||
'order_email_asked_twice': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -4085,14 +4085,14 @@ def validate_event_settings(event, settings_dict):
|
||||
raise ValidationError({
|
||||
'locale': _('Your default locale must also be enabled for your event (see box above).')
|
||||
})
|
||||
if settings_dict.get('attendee_names_required') and not settings_dict.get('attendee_names_asked'):
|
||||
raise ValidationError({
|
||||
'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
|
||||
})
|
||||
if settings_dict.get('attendee_emails_required') and not settings_dict.get('attendee_emails_asked'):
|
||||
raise ValidationError({
|
||||
'attendee_emails_required': _('You have to ask for attendee emails if you want to make them required.')
|
||||
})
|
||||
# if settings_dict.get('attendee_names_required') and not settings_dict.get('attendee_names_asked'):
|
||||
# raise ValidationError({
|
||||
# 'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
|
||||
# })
|
||||
# if settings_dict.get('attendee_emails_required') and not settings_dict.get('attendee_emails_asked'):
|
||||
# raise ValidationError({
|
||||
# 'attendee_emails_required': _('You have to ask for attendee emails if you want to make them required.')
|
||||
# })
|
||||
if settings_dict.get('invoice_address_required') and not settings_dict.get('invoice_address_asked'):
|
||||
raise ValidationError({
|
||||
'invoice_address_required': _('You have to ask for invoice addresses if you want to make them required.')
|
||||
|
||||
Reference in New Issue
Block a user