Split datafields in datafields and orderdatafields; rename QC datafield ref from 'question' to 'datafield'

This commit is contained in:
Mira Weller
2026-10-01 16:07:29 +02:00
parent a887d9cbad
commit 3f7cd16045
9 changed files with 53 additions and 26 deletions
+6
View File
@@ -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:compatquestion-list'),
('GET', 'api-v1:datafield-list'),
('GET', 'api-v1:questionnaire-list'),
('GET', 'api-v1:questionnairechild-list'),
('GET', 'api-v1:badgelayout-list'),
@@ -143,6 +145,8 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:compatquestion-list'),
('GET', 'api-v1:datafield-list'),
('GET', 'api-v1:questionnaire-list'),
('GET', 'api-v1:questionnairechild-list'),
('GET', 'api-v1:badgelayout-list'),
@@ -181,6 +185,8 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:compatquestion-list'),
('GET', 'api-v1:datafield-list'),
('GET', 'api-v1:questionnaire-list'),
('GET', 'api-v1:questionnairechild-list'),
('GET', 'api-v1:badgelayout-list'),
+5 -5
View File
@@ -712,7 +712,7 @@ class DatafieldSerializer(I18nAwareModelSerializer):
options_data = validated_data.pop('options') if 'options' in validated_data else []
items = validated_data.pop('items', [])
question = Question.objects.create(**validated_data, container_type=Question.ContainerType.ORDERPOSITION)
question = Question.objects.create(**validated_data, container_type=self.context['container_type'])
question.items.set(items)
for opt_data in options_data:
QuestionOption.objects.create(question=question, **opt_data)
@@ -755,18 +755,18 @@ class RenderedMarkdownField(serializers.CharField):
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())
datafield = DatafieldRelatedField(allow_null=True, source='*', queryset=Question.objects.none())
dependency_question = DatafieldRelatedField(allow_null=True, required=False, queryset=Question.objects.none())
rendered_help_text = RenderedMarkdownField(read_only=True, source='help_text')
cooked_id = serializers.IntegerField(read_only=True)
class Meta:
model = QuestionnaireChild
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values', 'rendered_help_text', 'cooked_id')
fields = ('datafield', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values', 'rendered_help_text', 'cooked_id')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["question"].queryset = self.context["event"].questions.all()
self.fields["datafield"].queryset = self.context["event"].questions.all()
self.fields["dependency_question"].queryset = self.context["event"].questions.all()
def validate_dependency_question(self, value):
+2 -1
View File
@@ -80,7 +80,8 @@ event_router.register(r'subevents', event.SubEventViewSet)
event_router.register(r'clone', event.CloneEventViewSet)
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'datafields', item.DatafieldViewSet, basename='datafield')
event_router.register(r'orderdatafields', item.OrderDatafieldViewSet, basename='orderdatafield')
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
event_router.register(r'questions', item.CompatQuestionViewSet)
event_router.register(r'discounts', discount.DiscountViewSet)
+25 -5
View File
@@ -579,7 +579,7 @@ with scopes_disabled():
fields = ['identifier']
class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet):
class BaseDatafieldViewSet(viewsets.ModelViewSet):
serializer_class = DatafieldSerializer
queryset = Question.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
@@ -588,12 +588,24 @@ class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering = ('id')
permission = None
write_permission = 'event.items:write'
container_type = Question.ContainerType.ORDERPOSITION
def get_queryset(self):
return self.request.event.questions.filter(
# the container_type parameter is undocumented, this API is going to change in a later release
container_type=self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION),
).prefetch_related('options').all()
return self.request.event.questions.filter(container_type=self.container_type).prefetch_related('options')
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
qid, dfid, sys_df = QuestionnaireChild.uncook_id(self.kwargs[lookup_url_kwarg])
self.via_questionnaire_id = qid
obj = get_object_or_404(queryset, pk=dfid)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
@transaction.atomic()
def perform_create(self, serializer):
@@ -608,6 +620,7 @@ class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['container_type'] = self.container_type
return ctx
@transaction.atomic()
@@ -630,6 +643,13 @@ class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet):
super().perform_destroy(instance)
class DatafieldViewSet(ConditionalListView, BaseDatafieldViewSet):
pass
class OrderDatafieldViewSet(ConditionalListView, BaseDatafieldViewSet):
container_type = Question.ContainerType.ORDER
class QuestionOptionViewSet(viewsets.ModelViewSet):
serializer_class = QuestionOptionSerializer
queryset = QuestionOption.objects.none()
@@ -16,10 +16,10 @@ const question = ref(props.question);
const dlgEditDatafield = ref()
const df = computed(() => typeof question.value.question === 'number' ?
props.datafields.find(el => el.id === question.value.question) :
typeof question.value.question === 'string' ?
SYSTEM_DATAFIELDS[question.value.question] :
const df = computed(() => typeof question.value.datafield === 'number' ?
props.datafields.find(el => el.id === question.value.datafield) :
typeof question.value.datafield === 'string' ?
SYSTEM_DATAFIELDS[question.value.datafield] :
null);
const dependency_values_options = computed(() => props.datafields.find(el => el.id === question.value.dependency_question)?.options);
@@ -107,7 +107,7 @@ const editor = ref();
</label>
<div class="col-md-9">
<p class="form-control-static">
<template v-if="typeof question.question === 'number'">
<template v-if="typeof question.datafield === 'number'">
{{ df.internal_name }}
<div>
<a class="btn btn-sm btn-default" href="javascript:" @click="dlgEditDatafield.open(getDatafieldEditUrl(df.id))"><span class="fa fa-wrench"></span> Manage data field details</a>
@@ -115,7 +115,7 @@ const editor = ref();
</div>
</template>
<template v-else>
{{ question.question }}
{{ question.datafield }}
</template>
</p>
</div>
@@ -149,7 +149,7 @@ const editor = ref();
<div class="col-md-9">
<select v-model="question.dependency_question" class="form-control">
<option :value="null">{{ gettext('(none)') }}</option>
<option v-for="(qc, index) in possible_dependencies" :value="qc.question">{{ i18n_any(qc.label) }}</option>
<option v-for="(qc, index) in possible_dependencies" :value="qc.datafield">{{ i18n_any(qc.label) }}</option>
</select>
<select v-model="question.dependency_values" class="form-control" multiple>
<option v-for="(qc, index) in dependency_values_options" :value="qc.identifier">{{ i18n_any(qc.answer) }}</option>
@@ -50,7 +50,7 @@ const props = defineProps<{
<div class="checkbox" v-for="channel in sales_channels">
<label>
<input type="checkbox" :checked="questionnaire.all_sales_channels || questionnaire.limit_sales_channels.indexOf(channel.identifier) !== -1"
@change="e => setListState(questionnaire.limit_sales_channels, e.target.checked, channel.identifier)"
@change="e => setListState(questionnaire.limit_sales_channels, (e.target as HTMLInputElement).checked, channel.identifier)"
:disabled="questionnaire.all_sales_channels">
<SalesChannelIcon :sales-channel="channel"/>
{{ i18n_any(channel.label) }}
@@ -67,7 +67,7 @@ const props = defineProps<{
<div class="category-header">{{ category.internal_name || i18n_any(category.name) }}</div>
<div class="checkbox" v-for="item in items">
<label :for="id + '_' + item.id">
<input :id="id + '_' + item.id" type="checkbox" :checked="questionnaire.items.indexOf(item.id) !== -1" @change="e => setListState(questionnaire.items, e.target.checked, item.id)"> {{ item.internal_name || i18n_any(item.name) }}
<input :id="id + '_' + item.id" type="checkbox" :checked="questionnaire.items.indexOf(item.id) !== -1" @change="e => setListState(questionnaire.items, (e.target as HTMLInputElement).checked, item.id)"> {{ item.internal_name || i18n_any(item.name) }}
</label>
</div>
</div>
@@ -43,7 +43,7 @@ watch(() => props.questionnaire.children, () => {
function addExistingDatafield (field) {
props.questionnaire.children.push({
_cid: useId(),
question: field.id,
datafield: field.id,
required: false,
label: field.question ?? {en: field.internal_name},
help_text: {},
@@ -63,7 +63,7 @@ function showAddTextblockDialog () {
function addTextblock () {
props.questionnaire.children.push({
_cid: useId(),
question: null,
datafield: null,
required: false,
label: newTextblockTitle.value,
help_text: newTextblockText.value,
@@ -118,7 +118,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, e.target.checked, selected_product); emit('update')}" v-if="selected_product && !preview_mode" :checked="!isHidden">
<input type="checkbox" @change="e => {setListState(props.questionnaire.items, (e.target as HTMLInputElement).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>
@@ -139,7 +139,7 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
<aside class="editor-action-area"><div class="btn-group">
<DragHandle tag="button" class="btn btn-default" v-if="!preview_mode"><i class="fa fa-arrows"></i></DragHandle>
<button class="btn btn-default" @click="dlgEditor.show()"><i class="fa fa-wrench"></i></button>
<button class="btn btn-default" @click="dlgEditor.show()" :title="`#${questionnaire.id}`"><i class="fa fa-wrench"></i></button>
</div></aside>
</div>
</summary>
@@ -47,7 +47,7 @@ async function api_json_request(resource, method, json_body) {
export async function getDatafields(container_type) {
using pb = ProgressBar.show('loading data fields')
return await api_get_all<Datafield>(`organizers/${organizer_slug}/events/${event_slug}/datafields/?container_type=${container_type}&`);
return await api_get_all<Datafield>(`organizers/${organizer_slug}/events/${event_slug}/${container_type == 'O' ? 'orderdatafields' : 'datafields'}/`);
}
export async function getQuestionnaires() {
@@ -49,7 +49,7 @@ export type Questionnaire = {
}
export type QuestionnaireChild = {
question: string | number,
datafield: string | number,
required: boolean,
label: I18nString,
help_text: I18nString,