From 5fd67594742d7e50d6646bc4e5cb08ac5e6d7c67 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Fri, 18 Sep 2026 14:02:30 +0200 Subject: [PATCH] wip2 --- src/pretix/api/serializers/item.py | 7 +-- src/pretix/api/views/item.py | 17 ++++-- src/pretix/base/forms/questions.py | 6 +-- src/pretix/base/models/items.py | 52 +++++++++---------- src/pretix/base/views/mixins.py | 4 +- src/pretix/control/forms/item.py | 3 -- .../pretixcontrol/items/questionnaires.html | 3 ++ src/pretix/control/views/item.py | 4 +- .../static/pretixbase/scss/_dialogs.scss | 2 + src/pretix/static/pretixcontrol/js/ui/main.js | 10 ++-- .../js/ui/questionnaires/DjangoDialog.vue | 17 +++--- .../js/ui/questionnaires/Editor.vue | 9 ++-- .../js/ui/questionnaires/NativeDialog.vue | 8 +-- .../js/ui/questionnaires/QuestionElement.vue | 13 +++-- .../questionnaires/QuestionnaireElement.vue | 40 ++++++++++---- .../pretixcontrol/js/ui/questionnaires/api.ts | 7 +++ .../static/pretixcontrol/scss/main.scss | 1 + 17 files changed, 126 insertions(+), 77 deletions(-) diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index 9fc4409fcb..c27a734498 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -553,9 +553,9 @@ class DatafieldSerializer(I18nAwareModelSerializer): class Meta: model = Question - fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position', - 'ask_during_checkin', 'show_during_checkin', 'identifier', - 'hidden', 'print_on_invoice', 'help_text', 'valid_number_min', + fields = ('id', 'question', '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', 'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'internal_name',) @@ -682,6 +682,7 @@ class QuestionnaireSerializer(I18nAwareModelSerializer): def __init__(self, *args, **kwargs): self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False) + self.fields['limit_sales_channels'].child_relation.queryset = kwargs['context']['event'].organizer.sales_channels.all() super().__init__(*args, **kwargs) def validate(self, data): diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index 2a1221d39d..3982b6fad3 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -463,7 +463,7 @@ with scopes_disabled(): class QuestionFilter(FilterSet): class Meta: model = Question - fields = ['ask_during_checkin', 'required', 'identifier'] + fields = ['identifier'] class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet): @@ -471,8 +471,8 @@ class DatafieldViewSet(ConditionalListView, viewsets.ModelViewSet): queryset = Question.objects.none() filter_backends = (DjangoFilterBackend, TotalOrderingFilter) filterset_class = QuestionFilter - ordering_fields = ('id', 'position') - ordering = ('position', 'id') + ordering_fields = ('id') + ordering = ('id') permission = None write_permission = 'event.items:write' @@ -568,11 +568,18 @@ class QuestionOptionViewSet(viewsets.ModelViewSet): super().perform_destroy(instance) +with scopes_disabled(): + class QuestionnaireFilter(FilterSet): + class Meta: + model = Questionnaire + fields = ['type'] + + class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = QuestionnaireSerializer queryset = Questionnaire.objects.none() - #filter_backends = (DjangoFilterBackend, TotalOrderingFilter) - #filterset_class = QuestionFilter + filter_backends = (DjangoFilterBackend, TotalOrderingFilter) + filterset_class = QuestionnaireFilter ordering_fields = ('id', 'position') ordering = ('position', 'id') permission = None diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 06a931fff8..cd8751ebab 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -50,7 +50,7 @@ from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import MaxValueValidator, MinValueValidator from django.db import ProgrammingError -from django.db.models import Prefetch, QuerySet +from django.db.models import Prefetch, Q, QuerySet from django.forms import Select, widgets from django.forms.widgets import FILE_INPUT_CONTRADICTION from django.utils.formats import date_format @@ -943,8 +943,7 @@ class OrderLevelQuestionsForm(BaseQuestionsForm): """ Takes two additional keyword arguments: - :param checkoutsession: The checkout session the form should be for - :param order: The order the form should be for + :param container: The checkout session or order the form should be for :param event: The event this belongs to """ request = kwargs.pop('request', None) @@ -954,6 +953,7 @@ class OrderLevelQuestionsForm(BaseQuestionsForm): super().__init__(*args, **kwargs) questionnaires = Questionnaire.objects.filter( + Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=container.sales_channel.identifier), event=event, type=Questionnaire.QuestionnaireType.ORDER_SALE, ).order_by('position').prefetch_related( Prefetch('children', QuestionnaireChild.objects.prefetch_related( diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 11c5f4c09f..8aa258596d 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -1671,37 +1671,37 @@ class Question(LoggedModel): ), ], ) - help_text = I18nTextField( - # TODO(questionnaires) : to be removed - verbose_name=_("Help text"), - help_text=_("If the question needs to be explained or clarified, do it here!"), - null=True, blank=True, - ) + #help_text = I18nTextField( + # # TODO(questionnaires) : to be removed + # verbose_name=_("Help text"), + # help_text=_("If the question needs to be explained or clarified, do it here!"), + # null=True, blank=True, + #) type = models.CharField( max_length=5, choices=FieldType.choices, verbose_name=_("Question type") ) - required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild - default=False, - verbose_name=_("Required question") - ) - items = models.ManyToManyField( # TODO(questionnaires) : to be removed, -> Questionnaire - Item, - related_name='questions', - verbose_name=_("Products"), - blank=True, - help_text=_('This question will be asked to buyers of the selected products') - ) - position = models.PositiveIntegerField( # TODO(questionnaires) : to be removed, -> Questionnaire + QuestionnaireChild - default=0, - verbose_name=_("Position") - ) - ask_during_checkin = models.BooleanField( # TODO(questionnaires) : to be removed - verbose_name=_('Ask during check-in instead of in the ticket buying process'), - help_text=_('Not supported by all check-in apps for all question types.'), - default=False - ) + #required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild + # default=False, + # verbose_name=_("Required question") + #) + #items = models.ManyToManyField( # TODO(questionnaires) : to be removed, -> Questionnaire + # Item, + # related_name='questions', + # verbose_name=_("Products"), + # blank=True, + # help_text=_('This question will be asked to buyers of the selected products') + #) + #position = models.PositiveIntegerField( # TODO(questionnaires) : to be removed, -> Questionnaire + QuestionnaireChild + # default=0, + # verbose_name=_("Position") + #) + #ask_during_checkin = models.BooleanField( # TODO(questionnaires) : to be removed + # verbose_name=_('Ask during check-in instead of in the ticket buying process'), + # help_text=_('Not supported by all check-in apps for all question types.'), + # default=False + #) show_during_checkin = models.BooleanField( verbose_name=_('Show answer during check-in'), help_text=_('Not supported by all check-in apps for all question types.'), diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py index af4edc509d..9ed5e8a13f 100644 --- a/src/pretix/base/views/mixins.py +++ b/src/pretix/base/views/mixins.py @@ -37,7 +37,7 @@ from pretix.base.forms.questions import ( ) from pretix.base.models import ( CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer, - QuestionnaireChild, QuestionOption, + Questionnaire, QuestionnaireChild, QuestionOption, ) from pretix.base.models.customers import AttendeeProfile from pretix.base.models.orders import CheckoutSession, Order @@ -350,7 +350,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin): def positions(self): qqs = self.request.event.questionnaires.all() if self.only_user_visible: - qqs = qqs.filter(type='PS') + qqs = qqs.filter(type=Questionnaire.QuestionnaireType.ORDER_POSITION_SALE) else: qqs = qqs.filter(type__startswith='P') qqs = qqs.filter( diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 00c90988b5..5a2b158c0a 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -157,7 +157,6 @@ class QuestionForm(I18nModelForm): del self.fields['show_during_checkin'] del self.fields['print_on_invoice'] self.fields['identifier'].required = False - self.fields['help_text'].widget.attrs['rows'] = 3 def clean_show_during_checkin(self): val = self.cleaned_data.get('show_during_checkin') @@ -191,9 +190,7 @@ class QuestionForm(I18nModelForm): localized_fields = '__all__' fields = [ 'question', - 'help_text', 'type', - 'required', 'show_during_checkin', 'hidden', 'identifier', diff --git a/src/pretix/control/templates/pretixcontrol/items/questionnaires.html b/src/pretix/control/templates/pretixcontrol/items/questionnaires.html index 9cc3685fa2..74a4ec611b 100644 --- a/src/pretix/control/templates/pretixcontrol/items/questionnaires.html +++ b/src/pretix/control/templates/pretixcontrol/items/questionnaires.html @@ -34,6 +34,9 @@ {% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=0 as datafield_edit_url %} {{ datafield_edit_url|json_script:"datafield_edit_url" }} + {% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=0 as datafield_view_url %} + {{ datafield_view_url|json_script:"datafield_view_url" }} + {% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug as datafield_create_url %} {{ datafield_create_url|json_script:"datafield_create_url" }} diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index a2592d810a..b9aa8e3a6d 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -555,7 +555,7 @@ class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView question=self.object, orderposition__isnull=False, ) qs = qs.filter(orderposition__in=opqs) - op_cnt = opqs.filter(item__in=self.object.items.all()).count() + op_cnt = 0 # TODO opqs.filter(item__in=self.object.items.all()).count() if self.object.type == Question.TYPE_FILE: qs = [ @@ -600,7 +600,7 @@ class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView def get_context_data(self, **kwargs): ctx = super().get_context_data() - ctx['items'] = self.object.items.exists() + #ctx['items'] = self.object.items.exists() ctx['has_subevents'] = self.request.event.has_subevents stats = self.get_answer_statistics() ctx['stats'], ctx['total'] = stats diff --git a/src/pretix/static/pretixbase/scss/_dialogs.scss b/src/pretix/static/pretixbase/scss/_dialogs.scss index 1b737812fc..6589e86453 100644 --- a/src/pretix/static/pretixbase/scss/_dialogs.scss +++ b/src/pretix/static/pretixbase/scss/_dialogs.scss @@ -87,6 +87,8 @@ dialog::backdrop { 100% { transform: skewX(0deg); } } +.modal-card.no-padding, .modal-card.no-padding .modal-card-content { padding: 0; } +.modal-card.no-scroll { overflow: hidden; } /* Legacy dialogs (still used for #ajaxerr and #popupmodal) */ diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index eab291826b..99712260a2 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -1139,10 +1139,12 @@ function notify_parent_frame() { }, location.origin) } catch {} $(function () { - window.parent.postMessage({ - type: 'pretix:dialog-loaded', - contentHeight: $('body').height(), - }, location.origin) + setTimeout(() => { + window.parent.postMessage({ + type: 'pretix:dialog-loaded', + contentHeight: $('#page-wrapper > .container-fluid').outerHeight() + 20, + }, location.origin) + }, 100) }) } notify_parent_frame() diff --git a/src/pretix/static/pretixcontrol/js/ui/questionnaires/DjangoDialog.vue b/src/pretix/static/pretixcontrol/js/ui/questionnaires/DjangoDialog.vue index 4de39cadc7..cd845cb4f5 100644 --- a/src/pretix/static/pretixcontrol/js/ui/questionnaires/DjangoDialog.vue +++ b/src/pretix/static/pretixcontrol/js/ui/questionnaires/DjangoDialog.vue @@ -4,7 +4,10 @@ import {gettext} from "./gettextstub"; import NativeDialog from "./NativeDialog.vue"; import {computed, onMounted, onUnmounted, ref, watch} from "vue"; -const props = defineProps(['defaultUrl']) +const props = defineProps({ + 'defaultUrl': {type: String, default: null}, + 'maxWidth': {type: String, default: '43em'}, +}) const emit = defineEmits(['confirm']) const dlgDjangoDialog = ref() const url = ref(props.defaultUrl) @@ -40,20 +43,20 @@ defineExpose({ frameHeight.value = 400 url.value = newUrl dlgDjangoDialog.value.show() - } + }, }); diff --git a/src/pretix/static/pretixcontrol/js/ui/questionnaires/Editor.vue b/src/pretix/static/pretixcontrol/js/ui/questionnaires/Editor.vue index ac85dae180..cb1e568c50 100644 --- a/src/pretix/static/pretixcontrol/js/ui/questionnaires/Editor.vue +++ b/src/pretix/static/pretixcontrol/js/ui/questionnaires/Editor.vue @@ -8,6 +8,7 @@ import {onMounted, onUnmounted, ref} from 'vue'; import { SlickList, SlickItem } from 'vue-slicksort'; import { ProgressBar } from "./ProgressBar"; +const sales_channels_list = await api.getSalesChannels(); const items_list = await api.getItems(); const categories_list = await api.getCategories(); const categories = Object.fromEntries(categories_list.map(cat => [cat.id, cat])); @@ -34,6 +35,7 @@ async function refreshQuestionnaireList () { async function refreshDatafieldList () { order_datafields.value = await api.getDatafields('O') position_datafields.value = (await api.getDatafields('P')).concat(Object.values(SYSTEM_DATAFIELDS)) + console.log('datafield list refreshed') } await Promise.all([refreshQuestionnaireList(), refreshDatafieldList()]) @@ -46,14 +48,12 @@ function saveQuestionnaire(questionnaire) { result = api.createQuestionnaire(questionnaire) } result = result.then(d => { - console.log(questionnaire, 'ok') questionnaire.id = d.id questionnaire.children = d.children questionnaire._err_mes = null questionnaire._loading = false return d; }, err => { - console.log(questionnaire, 'err:', err) questionnaire._err_mes = err questionnaire._loading = false return err; @@ -180,8 +180,8 @@ const preview_mode = ref(false) transform: rotate(0deg); } -.questionnaires-list > .editor-action-row { border-top: 1px solid rgb(175 175 175 / 0.3); } .editor-action-row { padding-top: 10px; width: calc(100% + 190px); padding-right: 170px } +.questionnaires-list > .editor-action-row { border-top: 1px solid rgb(175 175 175 / 0.3); padding-top: 25px; padding-bottom: 10px }