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)]))