diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py
index 13dc73db70..0f7ad92a3d 100644
--- a/src/pretix/base/models/items.py
+++ b/src/pretix/base/models/items.py
@@ -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(
diff --git a/src/pretix/control/templates/pretixcontrol/items/questionnaires.html b/src/pretix/control/templates/pretixcontrol/items/questionnaires.html
index 3b755a0280..9cc3685fa2 100644
--- a/src/pretix/control/templates/pretixcontrol/items/questionnaires.html
+++ b/src/pretix/control/templates/pretixcontrol/items/questionnaires.html
@@ -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" }}
+
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py
index d95c955e5d..a2592d810a 100644
--- a/src/pretix/control/views/item.py
+++ b/src/pretix/control/views/item.py
@@ -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:
diff --git a/src/pretix/static/pretixcontrol/js/ui/questionnaires/helper.js b/src/pretix/static/pretixcontrol/js/ui/questionnaires/helper.js
index 80b7a4dfed..91c4e22ea6 100644
--- a/src/pretix/static/pretixcontrol/js/ui/questionnaires/helper.js
+++ b/src/pretix/static/pretixcontrol/js/ui/questionnaires/helper.js
@@ -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)]))