mirror of
https://github.com/pretix/pretix.git
synced 2026-08-16 11:46:27 +00:00
Squashed commit of the following:
commit2177bebf6aMerge:ea613bf271d08216d8c5Author: Mira Weller <weller@rami.io> Date: Fri Aug 7 21:22:31 2026 +0200 Merge branch 'master' into questions-vue3 commitea613bf271Author: Mira Weller <weller@rami.io> Date: Fri Aug 7 21:21:35 2026 +0200 wip commitc8bed5e3b9Author: Mira Weller <weller@rami.io> Date: Fri Aug 7 10:15:00 2026 +0200 rename get_cart to get_cart_positions (keep old name alive) commit1ec27fff05Author: Mira Weller <weller@rami.io> Date: Fri Aug 7 10:01:38 2026 +0200 filter questionnaires by sales channel commit0294f205d0Author: Mira Weller <weller@rami.io> Date: Thu Aug 6 20:06:05 2026 +0200 refactor: extract build_requested_valid_from_field method commit3424aacdc3Author: Mira Weller <weller@rami.io> Date: Thu Aug 6 20:05:44 2026 +0200 rename presale question forms and mixins commit4012889076Author: Mira Weller <weller@rami.io> Date: Thu Aug 6 19:42:29 2026 +0200 rebase migration commitfe15d71cd2Merge:3ea7ea31b64d9dfa88feAuthor: Mira Weller <weller@rami.io> Date: Thu Aug 6 19:25:44 2026 +0200 Merge branch 'master' into questions-vue3 # Conflicts: # src/pretix/base/forms/questions.py # src/pretix/base/middleware.py # src/pretix/base/templates/pretixbase/redirect.html commit3ea7ea31b6Author: Mira Weller <weller@rami.io> Date: Fri Jul 24 17:37:46 2026 +0200 wip commit5b34a25fb5Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 16:21:11 2026 +0200 use questionnaire data for rendering checkout questions step commit9467f1d08dAuthor: Mira Weller <weller@rami.io> Date: Wed Jun 17 15:54:14 2026 +0200 refactor: extract build_system_question_field method commit85344a7690Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 15:50:41 2026 +0200 refactor: extract build_user_question_field method commit3b47f4aaa7Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 15:49:34 2026 +0200 refactor: cartpos / orderpos commit0309b05121Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 13:52:18 2026 +0100 vite config: allow cors access from pretix.work subdomains commitad092ae0d1Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:35:33 2026 +0200 re-add vue-slicksort dep commit9f42d679ccAuthor: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:28:53 2026 +0200 rebase migration commit9eae9c6de7Merge:84c51e031e694b915d89Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:24:49 2026 +0200 Merge branch 'master' into questions-vue3 # Conflicts: # .github/workflows/tests.yml # .gitignore # Dockerfile # package-lock.json # package.json # src/pretix/base/management/commands/runserver.py # src/pretix/base/middleware.py # src/pretix/base/templatetags/vite.py # src/pretix/control/forms/global_settings.py # src/pretix/control/templates/pretixcontrol/checkin/list_edit.html # src/pretix/control/views/item.py # src/pretix/presale/views/widget.py # src/pretix/settings.py # src/pretix/static/pretixcontrol/js/ui/checkinrules/App.vue # src/pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts # src/pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts # src/pretix/static/pretixcontrol/js/ui/checkinrules/lookup-select2.vue # src/pretix/static/pretixcontrol/js/ui/checkinrules/timefield.vue # src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue # src/pretix/static/pretixpresale/widget/index.html # src/pretix/static/pretixpresale/widget/src/api.ts # src/pretix/static/pretixpresale/widget/src/button.ts # src/pretix/static/pretixpresale/widget/src/components/PriceBox.vue # src/pretix/static/pretixpresale/widget/src/main.ts # src/pretix/static/pretixpresale/widget/src/sharedStore.ts # src/pretix/static/pretixpresale/widget/src/utils.ts # src/pretix/static/pretixpresale/widget/src/widget.ts # src/tests/e2e/conftest.py # vite.config.ts commit84c51e031eMerge:902ee9242e222b453b43Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:20:05 2026 +0200 Merge branch 'master' into questions-vue3 commit902ee9242eAuthor: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:19:26 2026 +0200 css comment commitb758efd4a9Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:19:19 2026 +0200 question dependencies commitc68fe00502Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:18:00 2026 +0200 add data field edit link commitb7da9bb6c1Author: Mira Weller <weller@rami.io> Date: Wed Jun 17 12:16:20 2026 +0200 handle hidden questions commit897120d3daAuthor: Mira Weller <weller@rami.io> Date: Tue May 12 13:04:08 2026 +0200 add ts model definitions commit09abc5725eAuthor: Mira Weller <weller@rami.io> Date: Tue May 12 11:30:12 2026 +0200 reorder and save commit9a7d5abb1fAuthor: Mira Weller <weller@rami.io> Date: Tue May 12 11:29:37 2026 +0200 fix unhandled exception in taxrules API commit3cee5f524bAuthor: Mira Weller <weller@rami.io> Date: Tue May 5 18:12:21 2026 +0200 use slicksort for reordering questionnaires and questions commit3a1a6b988eAuthor: Mira Weller <weller@rami.io> Date: Tue Apr 28 19:23:35 2026 +0200 improve i18n, implement "add text block" dialog commitbdc720f5a2Author: Mira Weller <weller@rami.io> Date: Fri Mar 27 18:57:22 2026 +0100 wip commit583184af38Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 21:45:32 2026 +0100 change existing views commit63d9f7cea8Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 21:45:32 2026 +0100 adapt vue app to new data model (wip) commitc3d6fb1bd6Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 21:44:47 2026 +0100 add Questionnaire and QuestionnaireChild models and their API; migrate existing data commitbb43acd257Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 13:52:28 2026 +0100 vite config: add questionnaires app commite0744951e8Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 13:52:18 2026 +0100 vite config: allow cors access from pretix.work subdomains commitfef1e356f7Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 13:33:24 2026 +0100 fix out-of-the-box experience commit81775f5d2dAuthor: Mira Weller <weller@rami.io> Date: Thu Mar 19 13:33:24 2026 +0100 setup questionnaires vue app, add code from question editor proof of concept commit4bbcc398ccMerge:a924d0a26658840a5fd6Author: Mira Weller <weller@rami.io> Date: Thu Mar 19 13:13:32 2026 +0100 Merge remote-tracking branch 'refs/remotes/origin/master' into questions-vue3
This commit is contained in:
@@ -702,7 +702,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
return super().save(**kwargs)
|
||||
|
||||
def validate_default(self, value):
|
||||
if not value and self.instance.default:
|
||||
if not value and (not self.instance or self.instance.default):
|
||||
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
|
||||
return value
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ from pretix.base.models import (
|
||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
SalesChannel,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
@@ -542,6 +543,7 @@ class LegacyDependencyValueField(serializers.CharField):
|
||||
class QuestionSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
internal_name = serializers.CharField(allow_null=True, source='question', read_only=True)
|
||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -550,7 +552,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
'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_file_portrait')
|
||||
'valid_string_length_max', 'valid_file_portrait', 'internal_name',)
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -626,6 +628,160 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
return question
|
||||
|
||||
|
||||
class QuestionRefField(serializers.PrimaryKeyRelatedField):
|
||||
def to_representation(self, qc):
|
||||
if not qc:
|
||||
return None
|
||||
elif qc.system_question:
|
||||
return qc.system_question
|
||||
elif qc.user_question_id:
|
||||
return qc.user_question_id
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) == int:
|
||||
return {'user_question': super().to_internal_value(data), 'system_question': None}
|
||||
elif type(data) == str or data is None:
|
||||
return {'user_question': None, 'system_question': data}
|
||||
else:
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return self.source == '*'
|
||||
|
||||
|
||||
class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
question = QuestionRefField(source='*', queryset=Question.objects.none())
|
||||
dependency_question = QuestionRefField(allow_null=True, required=False, queryset=Question.objects.none())
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["question"].queryset = self.context["event"].questions.all()
|
||||
self.fields["dependency_question"].queryset = self.context["event"].questions.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
return data
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
|
||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Questionnaire
|
||||
fields = ('id', 'type', 'internal_name', 'items', 'position', 'all_sales_channels', 'limit_sales_channels', 'children')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
#full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
#full_data.update(data)
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
# raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
#if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
#Question.clean_items(event, full_data.get('items') or [])
|
||||
return data
|
||||
|
||||
def validate_children(self, value):
|
||||
prev_questions = {}
|
||||
for child in value:
|
||||
if child.get('dependency_question'):
|
||||
if (child['dependency_question']['user_question'] or child['dependency_question']['system_question']) not in prev_questions:
|
||||
raise ValidationError('A question can only depend on a previous question from the same questionnaire.')
|
||||
|
||||
if child['user_question']:
|
||||
prev_questions[child['user_question']] = child
|
||||
if child['system_question']:
|
||||
prev_questions[child['system_question']] = child
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
children_data = validated_data.pop('children') if 'children' in validated_data else []
|
||||
questionnaire = super().create(validated_data)
|
||||
self.set_children(questionnaire, children_data)
|
||||
return questionnaire
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
children_data = validated_data.pop('children', None)
|
||||
questionnaire = super().update(instance, validated_data)
|
||||
if children_data is not None:
|
||||
self.set_children(questionnaire, children_data)
|
||||
return questionnaire
|
||||
|
||||
def set_children(self, questionnaire, new_data):
|
||||
result = []
|
||||
child_serializer = self.fields['children'].child
|
||||
existing = questionnaire.children.all()
|
||||
for i, d in enumerate(new_data):
|
||||
d['questionnaire'] = questionnaire
|
||||
d['position'] = i + 1
|
||||
d.setdefault('required', False)
|
||||
d.setdefault('help_text', None)
|
||||
d.setdefault('dependency_question', None)
|
||||
d.setdefault('dependency_values', None)
|
||||
updatable = min(len(existing), len(new_data))
|
||||
for i in range(0, updatable):
|
||||
result.append(child_serializer.update(existing[i], new_data[i]))
|
||||
for i in range(updatable, len(new_data)):
|
||||
result.append(child_serializer.create(new_data[i]))
|
||||
for i in range(updatable, len(existing)):
|
||||
existing[i].delete()
|
||||
return result
|
||||
|
||||
|
||||
class QuotaSerializer(I18nAwareModelSerializer):
|
||||
available = serializers.BooleanField(read_only=True)
|
||||
available_number = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -79,7 +79,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'questions', item.QuestionViewSet)
|
||||
event_router.register(r'datafields', item.QuestionViewSet)
|
||||
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
|
||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||
event_router.register(r'quotas', item.QuotaViewSet)
|
||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
|
||||
@@ -48,13 +48,14 @@ from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.item import (
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
||||
QuestionOptionSerializer, QuestionSerializer, QuestionnaireSerializer, QuotaSerializer,
|
||||
)
|
||||
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.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
@@ -563,6 +564,51 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = QuestionnaireSerializer
|
||||
queryset = Questionnaire.objects.none()
|
||||
#filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
#filterset_class = QuestionFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questionnaires.prefetch_related('children').all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.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
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.questionnaire.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user