Pass choice field options to JS

This commit is contained in:
Mira Weller
2026-09-16 15:07:33 +02:00
parent 9ace35069c
commit d8f444d51d
4 changed files with 107 additions and 112 deletions
+38 -41
View File
@@ -1611,6 +1611,22 @@ class Question(LoggedModel):
class ContainerType(models.TextChoices):
ORDER = "O", _("Order")
ORDERPOSITION = "P", _("Order position")
class FieldType(models.TextChoices):
NUMBER = "N", _("Number")
STRING = "S", _("Text (one line)")
TEXT = "T", _("Multiline text")
BOOLEAN = "B", _("Yes/No")
CHOICE = "C", _("Choose one from a list")
CHOICE_MULTIPLE = "M", _("Choose multiple from a list")
FILE = "F", _("File upload")
DATE = "D", _("Date")
TIME = "H", _("Time")
DATETIME = "W", _("Date and time")
COUNTRYCODE = "CC", _("Country code (ISO 3166-1 alpha-2)")
PHONENUMBER = "TEL", _("Phone number")
# compat
TYPE_NUMBER = "N"
TYPE_STRING = "S"
TYPE_TEXT = "T"
@@ -1623,20 +1639,8 @@ class Question(LoggedModel):
TYPE_DATETIME = "W"
TYPE_COUNTRYCODE = "CC"
TYPE_PHONENUMBER = "TEL"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
(TYPE_TEXT, _("Multiline text")),
(TYPE_BOOLEAN, _("Yes/No")),
(TYPE_CHOICE, _("Choose one from a list")),
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
(TYPE_FILE, _("File upload")),
(TYPE_DATE, _("Date")),
(TYPE_TIME, _("Time")),
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
(TYPE_PHONENUMBER, _("Phone number")),
)
TYPE_CHOICES = FieldType.choices
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
ASK_DURING_CHECKIN_UNSUPPORTED = []
SHOW_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE]
@@ -1652,9 +1656,8 @@ class Question(LoggedModel):
verbose_name=_("Asked on"),
default=ContainerType.ORDERPOSITION,
)
question = I18nTextField(
# TODO(questionnaires) : to be renamed to 'internal_name'
verbose_name=_("Question")
question = I18nTextField( # TODO(questionnaires) : to be renamed to 'internal_name'
verbose_name=_("Internal name"),
)
identifier = models.CharField(
max_length=190,
@@ -1676,7 +1679,7 @@ class Question(LoggedModel):
)
type = models.CharField(
max_length=5,
choices=TYPE_CHOICES,
choices=FieldType.choices,
verbose_name=_("Question type")
)
required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
@@ -1990,18 +1993,12 @@ class QuestionOption(models.Model):
class Questionnaire(LoggedModel):
TYPE_ORDER_SALE = "OS"
TYPE_ORDER_POSITION_SALE = "PS"
TYPE_ORDER_POSITION_ATTENDEE_ONLY = "PA"
TYPE_ORDER_POSITION_CHECKIN = "PC"
TYPE_ORDER_POSITION_HIDDEN = "PH"
TYPE_CHOICES = (
(TYPE_ORDER_SALE, _("Order-wide, before purchase")),
(TYPE_ORDER_POSITION_SALE, _("Per product, before purchase")),
(TYPE_ORDER_POSITION_ATTENDEE_ONLY, _("Per product, via attendee link")),
(TYPE_ORDER_POSITION_CHECKIN, _("Per product, at check-in")),
(TYPE_ORDER_POSITION_HIDDEN, _("Per product, hidden")),
)
class QuestionnaireType(models.TextChoices):
ORDER_SALE = "OS", _("Order-wide, before purchase")
ORDER_POSITION_SALE = "PS", _("Per product, before purchase")
ORDER_POSITION_ATTENDEE_ONLY = "PA", _("Per product, via attendee link")
ORDER_POSITION_CHECKIN = "PC", _("Per product, at check-in")
ORDER_POSITION_HIDDEN = "PH", _("Per product, hidden")
event = models.ForeignKey(
Event,
related_name="questionnaires",
@@ -2013,7 +2010,7 @@ class Questionnaire(LoggedModel):
)
type = models.CharField(
max_length=5,
choices=TYPE_CHOICES,
choices=QuestionnaireType.choices,
verbose_name=_("Questionnaire type")
)
items = models.ManyToManyField(
@@ -2041,15 +2038,15 @@ class Questionnaire(LoggedModel):
class QuestionnaireChild(LoggedModel):
SYSTEM_QUESTION_CHOICES = (
('attendee_name_parts', _('Attendee name')),
('attendee_email', _('Attendee email')),
('company', _('Company')),
('street', _('Street')),
('zipcode', _('ZIP code')),
('city', _('City')),
('country', _('Country')),
)
class SystemQuestion(models.TextChoices):
ATTENDEE_NAME_PARTS = 'attendee_name_parts', _('Attendee name')
ATTENDEE_EMAIL = 'attendee_email', _('Attendee email')
COMPANY = 'company', _('Company')
STREET = 'street', _('Street')
ZIPCODE = 'zipcode', _('ZIP code')
CITY = 'city', _('City')
COUNTRY = 'country', _('Country')
questionnaire = models.ForeignKey(
Questionnaire,
related_name="children",
@@ -2067,7 +2064,7 @@ class QuestionnaireChild(LoggedModel):
)
system_datafield = models.CharField(
max_length=25,
choices=SYSTEM_QUESTION_CHOICES,
choices=SystemQuestion.choices,
null=True, blank=True,
)
required = models.BooleanField(
@@ -28,10 +28,15 @@
{{ request.event.settings.locales|json_script:"event_locales" }}
{{ questionnaire_type_choices|json_script:"questionnaire_type_choices" }}
{{ question_type_choices|json_script:"question_type_choices" }}
{{ system_question_choices|json_script:"system_question_choices" }}
{% 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.add" organizer=request.event.organizer.slug event=request.event.slug as datafield_create_url %}
{{ datafield_create_url|json_script:"datafield_create_url" }}
<div id="questionnaires-editor">
<!-- Vue app mount point -->
</div>
+22 -3
View File
@@ -59,6 +59,7 @@ from django.views.decorators.http import require_http_methods
from django.views.generic import FormView, ListView, TemplateView, View
from django.views.generic.detail import DetailView, SingleObjectMixin
from django_countries.fields import Country
from i18nfield.strings import LazyI18nString
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
@@ -67,11 +68,11 @@ from pretix.api.serializers.item import (
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
OrderPosition, Question, QuestionAnswer, QuestionOption, QuestionnaireChild, Quota,
SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue, Questionnaire
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability
@@ -436,7 +437,7 @@ class QuestionList(ListView):
template_name = 'pretixcontrol/items/questions.html'
def get_queryset(self):
return self.request.event.questions
return self.request.event.questions.all()
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
@@ -706,10 +707,28 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
return ret
def textchoices_to_json(choices, event):
return [(c.name, c.value, i18n_all(event.settings.locales, LazyI18nString.from_gettext(c.label).data)) for c in choices]
def i18n_all(locales, data):
out = {}
for locale in locales:
out[locale] = data[locale]
return out
class QuestionnairesEditor(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/items/questionnaires.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['questionnaire_type_choices'] = textchoices_to_json(Questionnaire.QuestionnaireType, self.request.event)
ctx['system_question_choices'] = textchoices_to_json(QuestionnaireChild.SystemQuestion, self.request.event)
ctx['question_type_choices'] = textchoices_to_json(Question.FieldType, self.request.event)
return ctx
class QuotaQueryMixin:
@@ -1,97 +1,71 @@
/* global gettext, pgettext */
export function i18n_any(data) {
if (!data) return null;
const preferred = document.body.getAttribute("data-pretixlocale");
if (data[preferred]) return data[preferred];
return Object.values(data)[0];
export function i18n_any (data) {
if (!data) return null
if (data instanceof LazyI18nString) data = data.data;
const preferred = document.body.getAttribute('data-pretixlocale')
if (data[preferred]) return data[preferred]
return Object.values(data)[0]
}
function freezeRec(o) {
function freezeRec (o) {
return Object.freeze(Object.fromEntries(Object.entries(o).map(([k, v]) => [k, v && Object.getPrototypeOf(v) === Object.prototype ? freezeRec(v) : v])))
}
export function localeComp(fn) {
return function(a, b) {
return fn(a).localeCompare(fn(b));
export function localeComp (fn) {
return function (a, b) {
return fn(a).localeCompare(fn(b))
}
}
export function numericComp(fn) {
return function(a, b) {
return fn(a) - fn(b);
export function numericComp (fn) {
return function (a, b) {
return fn(a) - fn(b)
}
}
export function pick(key) {
return function(obj) {
return obj[key];
export function pick (key) {
return function (obj) {
return obj[key]
}
}
export function sort(array, ...orderBy) {
array.sort(function(a, b) {
for(let comp of orderBy) {
const result = comp(a, b);
export function sort (array, ...orderBy) {
array.sort(function (a, b) {
for (let comp of orderBy) {
const result = comp(a, b)
if (result !== 0) {
return result;
return result
}
}
return 0;
});
return 0
})
}
export function *groupBy(array, key) {
let lastKey, lastArray;
for(const x of array){
const k = key(x);
export function *groupBy (array, key) {
let lastKey, lastArray
for (const x of array) {
const k = key(x)
if (lastKey !== k || !lastArray) {
if (lastArray) {
yield [lastKey, lastArray];
yield [lastKey, lastArray]
}
lastKey = k; lastArray = [x];
lastKey = k; lastArray = [x]
} else {
lastArray.push(x);
lastArray.push(x)
}
}
if (lastArray) {
yield [lastKey, lastArray];
yield [lastKey, lastArray]
}
}
export const QUESTION_TYPE = {
NUMBER: "N",
STRING: "S",
TEXT: "T",
BOOLEAN: "B",
CHOICE: "C",
CHOICE_MULTIPLE: "M",
FILE: "F",
DATE: "D",
TIME: "H",
DATETIME: "W",
COUNTRYCODE: "CC",
PHONENUMBER: "TEL",
};
function fromJsonScript (id) {
return JSON.parse(document.getElementById(id).textContent)
}
export const _ = x => x;
export const QUESTION_TYPE = Object.fromEntries(fromJsonScript('question_type_choices').map(([name, value, label]) => [name, value]))
export const QUESTION_TYPE_LABEL = Object.fromEntries(fromJsonScript('question_type_choices').map(([name, value, label]) => [name, i18n_any(label)]))
export const QUESTION_TYPE_LABEL = {
NUMBER: _("Number"),
STRING: _("Text (one line)"),
TEXT: _("Multiline text"),
BOOLEAN: _("Yes/No"),
CHOICE: _("Choose one from a list"),
CHOICE_MULTIPLE: _("Choose multiple from a list"),
FILE: _("File upload"),
DATE: _("Date"),
TIME: _("Time"),
DATETIME: _("Date and time"),
COUNTRYCODE: _("Country code (ISO 3166-1 alpha-2)"),
PHONENUMBER: _("Phone number"),
};
export const SYSTEM_DATAFIELDS = freezeRec(Object.fromEntries(fromJsonScript('system_question_choices').map(([name, value, label]) => [
value, { id: value, question: label, type: value === 'country' ? QUESTION_TYPE.COUNTRYCODE : QUESTION_TYPE.STRING }
])))
export const SYSTEM_DATAFIELDS = freezeRec({
'attendee_name_parts': { label: _('Attendee name'), type: QUESTION_TYPE.STRING },
'attendee_email': { label: _('Attendee email'), type: QUESTION_TYPE.STRING },
'company': { label: _('Company'), type: QUESTION_TYPE.STRING },
'street': { label: _('Street'), type: QUESTION_TYPE.STRING },
'zipcode': { label: _('ZIP code'), type: QUESTION_TYPE.STRING },
'city': { label: _('City'), type: QUESTION_TYPE.STRING },
'country': { label: _('Country'), type: QUESTION_TYPE.COUNTRYCODE },
});
export const QUESTIONNAIRE_TYPE = Object.fromEntries(fromJsonScript('questionnaire_type_choices').map(([name, value, label]) => [name, value]))
export const QUESTIONNAIRE_TYPE_LABEL = Object.fromEntries(fromJsonScript('questionnaire_type_choices').map(([name, value, label]) => [name, i18n_any(label)]))