mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
Improve validation
This commit is contained in:
@@ -668,6 +668,7 @@ class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
items = serializers.PrimaryKeyRelatedField(many=True, queryset=Item.objects.none(), allow_null=True, required=False)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
@@ -682,6 +683,7 @@ class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False)
|
||||
self.fields['items'].child_relation.queryset = kwargs['context']['event'].items.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = kwargs['context']['event'].organizer.sales_channels.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -725,6 +727,10 @@ class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
children_data = validated_data.pop('children') if 'children' in validated_data else []
|
||||
questionnaire = super().create(validated_data)
|
||||
self.set_children(questionnaire, children_data)
|
||||
try:
|
||||
questionnaire.check_constraints()
|
||||
except ValidationError as e:
|
||||
raise DrfValidationError(as_serializer_error(e))
|
||||
return questionnaire
|
||||
|
||||
@transaction.atomic
|
||||
@@ -733,6 +739,10 @@ class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
questionnaire = super().update(instance, validated_data)
|
||||
if children_data is not None:
|
||||
self.set_children(questionnaire, children_data)
|
||||
try:
|
||||
questionnaire.check_constraints()
|
||||
except ValidationError as e:
|
||||
raise DrfValidationError(as_serializer_error(e))
|
||||
return questionnaire
|
||||
|
||||
def set_children(self, questionnaire, new_data):
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
|
||||
import calendar
|
||||
import os
|
||||
from itertools import groupby
|
||||
|
||||
import sys
|
||||
import uuid
|
||||
import warnings
|
||||
@@ -2040,6 +2042,41 @@ class Questionnaire(LoggedModel):
|
||||
class Meta:
|
||||
ordering = ('position', 'id')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.internal_name
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
return self.position, self.id
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
def check_constraints(self):
|
||||
errors = set()
|
||||
all_questionnaires = Questionnaire.objects.filter(event=self.event).prefetch_related('children', 'items')
|
||||
for (type, item_id, sdf, udf), fields in groupby(sorted(
|
||||
(q.type, item.id, c.system_datafield or '', c.user_datafield_id or 0, q, item)
|
||||
for q in all_questionnaires
|
||||
for item in q.items.all()
|
||||
for c in q.children.all()
|
||||
), key=lambda d: (d[0], d[1], d[2], d[3])):
|
||||
fields = list(fields)
|
||||
if len(fields) > 1:
|
||||
type, item_id, sdf, udf, q, item = fields[0]
|
||||
questionnaires = set(q for type,item,sdf,udf,q,item in fields)
|
||||
df_label = _('System data field') + f' "{sdf}"' if sdf else _('User-defined data field') + f' "{str(udf)}"'
|
||||
if len(questionnaires) == 1:
|
||||
errors.add(_('{datafield} added twice to questionnaire "{questionnaire}"').format(
|
||||
datafield=df_label, questionnaire=next(str(q) for q in questionnaires)
|
||||
))
|
||||
else:
|
||||
errors.add(_('{datafield} added twice to the same item "{item}" via questionnaires "{questionnaires}"').format(
|
||||
datafield=df_label, item=str(item), questionnaires='" and "'.join(str(q) for q in questionnaires)
|
||||
))
|
||||
if errors:
|
||||
raise ValidationError(list(errors))
|
||||
|
||||
|
||||
class QuestionnaireChild(LoggedModel):
|
||||
class SystemQuestion(models.TextChoices):
|
||||
|
||||
@@ -42,7 +42,7 @@ function addExistingDatafield (field) {
|
||||
_cid: useId(),
|
||||
question: field.id,
|
||||
required: false,
|
||||
label: field.question,
|
||||
label: field.question ?? {en: field.internal_name},
|
||||
help_text: {},
|
||||
dependency_question: null,
|
||||
dependency_values: [],
|
||||
@@ -112,7 +112,7 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
|
||||
<summary class="questionnaire-panel-heading">
|
||||
<div class=" editor-row">
|
||||
<div class="editor-preview-area">
|
||||
<input type="checkbox" @change="e => {setListState(props.questionnaire.items, selected_product, e.target.checked); emit('update')}" v-if="selected_product && !preview_mode" :checked="!isHidden">
|
||||
<input type="checkbox" @change="e => {setListState(props.questionnaire.items, e.target.checked, selected_product); emit('update')}" v-if="selected_product && !preview_mode" :checked="!isHidden">
|
||||
{{ props.questionnaire.internal_name }}
|
||||
<span class="fa fa-warning" v-if="questionnaire._err_mes"></span>
|
||||
<span class="fa fa-cog fa-spin" v-if="questionnaire._loading"></span>
|
||||
@@ -125,7 +125,15 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
|
||||
</div>
|
||||
</summary>
|
||||
<div class="questionnaire-panel-body" v-if="!isHidden">
|
||||
<div class="alert alert-warning" v-if="questionnaire._err_mes">{{ questionnaire._err_mes }}</div>
|
||||
<div class="alert alert-warning" v-if="questionnaire._err_mes">
|
||||
<dl v-if="questionnaire._err_mes.api_error">
|
||||
<template v-for="(messages, key) in questionnaire._err_mes.api_error">
|
||||
<dt>{{ key }}</dt>
|
||||
<dd><ul><li v-for="message in messages">{{ message }}</li></ul></dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p v-else>{{ questionnaire._err_mes }}</p>
|
||||
</div>
|
||||
<div class="form-horizontal" :id="`questionListParent${props.questionnaire.id}`">
|
||||
<SlickList axis="y" v-model:list="props.questionnaire.children" useDragHandle :appendTo="`#questionListParent${props.questionnaire.id}`" @update:list="emit('update')">
|
||||
<SlickItem v-for="(child, index) in props.questionnaire.children" :key="child._cid" :index="index">
|
||||
@@ -167,7 +175,7 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
|
||||
:title="gettext('Add existing data field')">
|
||||
|
||||
<div class="list-group">
|
||||
<a href="javascript:" @click="addExistingDatafield(field)" v-for="field in datafields" class="list-group-item">{{ i18n_any(field.question) }}</a>
|
||||
<a href="javascript:" @click="addExistingDatafield(field)" v-for="field in datafields" class="list-group-item">{{ field.internal_name ?? i18n_any(field.question) }}</a>
|
||||
</div>
|
||||
|
||||
<button @click="dlgAddExisting.close()" class="btn btn-default pull-right">{{ gettext('Cancel') }}</button>
|
||||
|
||||
Reference in New Issue
Block a user