This commit is contained in:
Mira Weller
2026-07-24 17:37:46 +02:00
parent 5b34a25fb5
commit 3ea7ea31b6
12 changed files with 255 additions and 168 deletions
+65 -32
View File
@@ -1426,6 +1426,12 @@ class QuestionAnswer(models.Model):
else: else:
return self.answer return self.answer
def to_dependency_values(self):
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
return [o.identifier for o in self.options.all()]
elif self.question.type in (Question.TYPE_BOOLEAN, Question.TYPE_COUNTRYCODE):
return self.answer
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.orderposition and self.cartposition: if self.orderposition and self.cartposition:
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.') raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
@@ -1570,53 +1576,80 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
def cache_answers(self, all=True): def cache_answers(self, all=True):
""" """
Creates two properties on the object. Creates a new property on the object:
(1) answ: a dictionary of question.id → answer string questions: a list of Question objects, extended by an 'answer' property
(2) questions: a list of Question objects, extended by an 'answer' property
""" """
self.answ = {}
for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
self.answ[a.question_id] = a
# We need to clone our question objects, otherwise we will override the cached # We need to clone our question objects, otherwise we will override the cached
# answers of other items in the same cart if the question objects have been # answers of other items in the same cart if the question objects have been
# selected via prefetch_related # selected via prefetch_related
if not all: if not all:
if hasattr(self.item, 'questions_to_ask'): if hasattr(self.item, 'relevant_questionnaires'):
questions = list(copy.copy(q) for q in self.item.questions_to_ask) children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
else: else:
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False, children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type='PS') for qc in qq.children.all())
hidden=False))
else: else:
questions = list(copy.copy(q) for q in self.item.questions.all()) children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type__startswith='P') for qc in qq.children.all())
question_cache = { qc_cache = {
q.pk: q for q in questions q.pk: q for q in children
} }
def question_is_visible(parentid, qvals): def qc_is_visible(parentid, qvals):
if parentid not in question_cache: if parentid not in qc_cache:
return False return False
parentq = question_cache[parentid] parentqc = qc_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values): if parentqc.dependency_question_id and not qc_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
return False return False
if parentid not in self.answ: answer_values = self.get_dependency_answer_values(parentqc)
return False return any(qval in answer_values for qval in qvals)
return (
('True' in qvals and self.answ[parentid].answer == 'True')
or ('False' in qvals and self.answ[parentid].answer == 'False')
or (any(qval in [o.identifier for o in self.answ[parentid].options.all()] for qval in qvals))
)
self.questions = [] self.questions = []
for q in questions: for qc in children:
if q.id in self.answ: if qc.user_question_id and qc.user_question_id in self.answer_cache:
q.answer = self.answ[q.id] qc.answer = self.answer_cache[qc.user_question_id]
q.answer.question = q # cache object #qc.answer.question = qc # cache object
elif qc.system_question:
qc.answer = self.get_system_answer(qc.system_question)
#qc.answer.question = qc # cache object
else: else:
q.answer = "" qc.answer = ""
if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values): if not qc.dependency_question_id or qc_is_visible(qc.dependency_question_id, qc.dependency_values):
self.questions.append(q) self.questions.append(qc)
@cached_property
def answer_cache(self):
return {
aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())
}
def get_dependency_answer_values(self, qc):
if qc.user_question_id:
if qc.user_question_id not in self.answer_cache:
return None
answer = self.answer_cache[qc.user_question_id]
return answer.to_dependency_values()
elif qc.system_question:
return [self.get_system_answer(qc.system_question)]
else:
raise ValueError('Questionnaire child without question has no answer')
def get_system_answer(self, system_question_name):
if system_question_name == 'attendee_name_parts':
return self.attendee_name_parts
elif system_question_name == 'attendee_email':
return self.attendee_email
elif system_question_name == 'street':
return self.street
elif system_question_name == 'zipcode':
return self.zipcode
elif system_question_name == 'city':
return self.city
elif system_question_name == 'state':
return self.state
elif system_question_name == 'country':
return self.country
else:
raise ValueError('Unknown system question name')
@property @property
def net_price(self): def net_price(self):
@@ -8,7 +8,7 @@
<div class="error-details"> <div class="error-details">
<h1>{% trans "Redirect" %}</h1> <h1>{% trans "Redirect" %}</h1>
<h3> <h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %} {% blocktrans trimmed with host=bold_hostname %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}. The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %} {% endblocktrans %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
+2
View File
@@ -25,6 +25,7 @@ from django.core import signing
from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html
def _is_samesite_referer(request): def _is_samesite_referer(request):
@@ -52,6 +53,7 @@ def redir_view(request):
u = urllib.parse.urlparse(url) u = urllib.parse.urlparse(url)
return render(request, 'pretixbase/redirect.html', { return render(request, 'pretixbase/redirect.html', {
'hostname': u.hostname, 'hostname': u.hostname,
'bold_hostname': format_html("<strong>{}</strong>", u.hostname),
'url': url, 'url': url,
}) })
@@ -602,60 +602,9 @@
{% endif %} {% endif %}
{% if line.has_questions %} {% if line.has_questions %}
<dl> <dl>
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
<dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
<dt>{% trans "Attendee email" %}</dt>
<dd>
{% if line.attendee_email %}
{{ line.attendee_email }}
{% if not line.addon_to %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
{% csrf_token %}
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
class="btn btn-default btn-xs">
<span class="fa fa-envelope-o"></span>
</a>
<button class="btn btn-default btn-xs">
{% trans "Resend link" %}
</button>
</form>
{% endif %}
{% else %}
<em>{% trans "not answered" %}</em>
{% endif %}
</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
<dt>
{% trans "Attendee company" %}
</dt>
<dd>
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
<dt>
{% trans "Attendee address" %}
</dt>
<dd>
{% if line.street or line.zipcode or line.city or line.country %}
{{ line.street|default_if_none:""|linebreaksbr }}<br>
{{ line.zipcode|default_if_none:"" }} {{ line.city|default_if_none:"" }}<br>
{% if line.state %}{{ line.state_for_address }}<br>{% endif %}
{{ line.country.name|default_if_none:"" }}
{% else %}
<em>{% trans "not answered" %}</em>
{% endif %}
</dd>
{% endif %}
{% for q in line.questions %} {% for q in line.questions %}
<dt> <dt>
{{ q.question }} {{ q.label }}
{% if q.ask_during_checkin %} {% if q.ask_during_checkin %}
<span class="fa fa-qrcode text-muted" <span class="fa fa-qrcode text-muted"
data-toggle="tooltip" data-toggle="tooltip"
@@ -683,12 +632,27 @@
{% endif %} {% endif %}
{% elif q.type == "M" %} {% elif q.type == "M" %}
{{ q.answer.to_string_i18n|rich_text_snippet }} {{ q.answer.to_string_i18n|rich_text_snippet }}
{% else %} {% elif q.type %}
{{ q.answer.to_string_i18n|linebreaksbr }} {{ q.answer.to_string_i18n|linebreaksbr }}
{% else %}{# TODO: proper separation of QuestionAnswer objects and system answers...... #}
{{ q.answer|linebreaksbr }}
{% endif %} {% endif %}
{% else %} {% else %}
<em>{% trans "not answered" %}</em> <em>{% trans "not answered" %}</em>
{% endif %} {% endif %}
{% if q.system_question == "attendee_email" and not line.addon_to %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
{% csrf_token %}
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
class="btn btn-default btn-xs">
<span class="fa fa-envelope-o"></span>
</a>
<button class="btn btn-default btn-xs">
{% trans "Resend link" %}
</button>
</form>
{% endif %}
</dd> </dd>
{% endfor %} {% endfor %}
{% for q in line.additional_fields %} {% for q in line.additional_fields %}
+17 -40
View File
@@ -1060,26 +1060,18 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
return False return False
for cp in self._positions_for_questions: for cp in self._positions_for_questions:
answ = { qc_cache = {
aw.question_id: aw for aw in cp.answerlist qc.pk: qc for qq in cp.item.relevant_questionnaires for qc in qq.childlist
}
question_cache = {
q.pk: q for q in cp.item.questions_to_ask
} }
def question_is_visible(parentid, qvals): def question_is_visible(parentid, qvals):
if parentid not in question_cache: if parentid not in qc_cache:
return False return False
parentq = question_cache[parentid] parentqc = qc_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values): if parentqc.dependency_question_id and not question_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
return False return False
if parentid not in answ: answer_values = cp.get_dependency_answer_values(parentqc)
return False return any(qval in answer_values for qval in qvals)
return (
('True' in qvals and answ[parentid].answer == 'True')
or ('False' in qvals and answ[parentid].answer == 'False')
or (any(qval in [o.identifier for o in answ[parentid].options.all()] for qval in qvals))
)
def question_is_required(q): def question_is_required(q):
return ( return (
@@ -1088,31 +1080,16 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
) )
if not self.all_optional: if not self.all_optional:
for q in cp.item.questions_to_ask: for qq in cp.item.relevant_questionnaires:
if question_is_required(q) and q.id not in answ: for qc in qq.childlist:
if warn: if qc.user_question_id and question_is_required(qc) and qc.user_question_id not in cp.answer_cache:
messages.warning(request, _('Please fill in answers to all required questions.')) if warn:
return False messages.warning(request, _('Please fill in answers to all required questions.'))
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_names_required', as_type=bool) \ return False
and not cp.attendee_name_parts: if qc.system_question and question_is_required(qc) and not cp.get_system_answer(qc.system_question):
if warn: if warn:
messages.warning(request, _('Please fill in answers to all required questions.')) messages.warning(request, _('Please fill in answers to all required questions.'))
return False return False
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
and cp.attendee_email is None:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_company_required', as_type=bool) \
and cp.company is None:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_addresses_required', as_type=bool) \
and (cp.street is None and cp.city is None and cp.country is None):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
responses = question_form_fields.send(sender=self.request.event, position=cp) responses = question_form_fields.send(sender=self.request.event, position=cp)
form_data = cp.meta_info_data.get('question_form_data', {}) form_data = cp.meta_info_data.get('question_form_data', {})
@@ -204,7 +204,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% for q in line.questions %} {% for q in line.questions %}
<dt>{{ q.question }}</dt> <dt>{{ q.label }}</dt>
<dd> <dd>
{% if q.answer %} {% if q.answer %}
{% if q.answer.file %} {% if q.answer.file %}
@@ -1,15 +1,32 @@
<script lang="ts"> <script lang="ts">
import QuestionnaireElement from './QuestionnaireElement.vue'; import QuestionnaireElement from './QuestionnaireElement.vue';
import {create_questionnaire, get_datafields, get_items, get_questionnaires, update_questionnaire} from './api'; import * as api from './api';
import { Questionnaire } from './model'; import { Questionnaire } from './model';
import { i18n_any, QUESTION_TYPE } from './helper'; import { i18n_any, QUESTION_TYPE, sort, localeComp, numericComp, groupBy, _ } from './helper';
import {Ref, ref} from 'vue'; import {Ref, ref} from 'vue';
import { SlickList, SlickItem } from 'vue-slicksort'; import { SlickList, SlickItem } from 'vue-slicksort';
const items_list = await get_items(); const items_list = await api.getItems();
const categories_list = await api.getCategories();
const categories = Object.fromEntries(categories_list.map(cat => [cat.id, cat]));
categories['null'] = { position: -1, internal_name: _('Uncategorized') };
sort(items_list, numericComp(item => categories[item.category]?.position), numericComp(item => item.position));
console.log("items_list", items_list);
const grouped_items = [...groupBy(items_list, item => categories[item.category])];
console.log("grouped_items", grouped_items);
const questionnaires: Ref<(Omit<Questionnaire, 'id'> & { _new_id?: number, id?: number })[]> = ref(await get_questionnaires()); const all_questionnaires: (Omit<Questionnaire, 'id'> & { _new_id?: number, id?: number })[] = await api.getQuestionnaires();
const datafields = ref(await get_datafields()); const order_questionnaires = ref(all_questionnaires.filter(q => q.type.startsWith('O')));
const position_questionnaires = ref(all_questionnaires.filter(q => q.type.startsWith('P')));
const datafields = ref(await api.getDatafields());
function saveQuestionnaire(questionnaire) {
if (questionnaire.id) {
api.updateQuestionnaire(questionnaire.id, questionnaire);
} else {
api.createQuestionnaire(questionnaire);
}
}
export default { export default {
components: { components: {
@@ -17,30 +34,38 @@ export default {
}, },
methods: { methods: {
i18n_any, i18n_any,
addQuestionnaire() { addPositionQuestionnaire() {
questionnaires.value.push({ position_questionnaires.value.push({
all_sales_channels: false, children: [], limit_sales_channels: [], position: 0, all_sales_channels: false, children: [], limit_sales_channels: [], position: 0,
items: [], internal_name: "Unnamed questionnaire", type: "PC", items: [], internal_name: "Unnamed questionnaire", type: "PS",
_new_id: Date.now(),
});
},
addOrderQuestionnaire() {
order_questionnaires.value.push({
all_sales_channels: false, children: [], limit_sales_channels: [], position: 0,
items: [], internal_name: "Unnamed questionnaire", type: "OS",
_new_id: Date.now(), _new_id: Date.now(),
}); });
}, },
saveData() { saveData() {
console.log(JSON.parse(JSON.stringify(questionnaires.value))); for (const questionnaire of order_questionnaires.value) {
for (const questionnaire of questionnaires.value) { saveQuestionnaire(questionnaire);
if (questionnaire.id) { }
update_questionnaire(questionnaire.id, questionnaire); for (const questionnaire of position_questionnaires.value) {
} else { saveQuestionnaire(questionnaire);
create_questionnaire(questionnaire);
}
} }
}, },
}, },
data() { data() {
return { return {
questionnaires, order_questionnaires,
position_questionnaires,
datafields, datafields,
items: items_list, items: items_list,
selected_product: ref(""), selected_product: ref(""),
grouped_items,
categories,
} }
} }
} }
@@ -61,30 +86,53 @@ export default {
.debuginfo { font-size: 70%; background: rgba(200, 200, 200, 0.5); } .debuginfo { font-size: 70%; background: rgba(200, 200, 200, 0.5); }
.dependency-info { position: absolute; } .dependency-info { position: absolute; }
.dependency-info > span { } .dependency-info > span { }
.category-header { margin: 8px 0 -5px 0; font-weight: bold; color: #737373; }
</style> </style>
<template> <template>
<p class="filter-row">
Order questionnaires
</p>
<div class="question-editor">
<SlickList axis="y" v-model:list="order_questionnaires" useDragHandle appendTo="#orderQuestionnaireListParent" id="orderQuestionnaireListParent">
<SlickItem v-for="(questionnaire, index) in order_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
<QuestionnaireElement
:questionnaire="questionnaire"
:datafields="datafields"
:grouped_items="null"
:selected_product="null" />
</SlickItem>
</SlickList>
</div>
<p>
<button class="btn btn-default" @click="addOrderQuestionnaire()"><i class="fa fa-plus"></i> Neuen Fragebogen erstellen</button>
<button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> Speichern</button>
</p>
<p class="filter-row"> <p class="filter-row">
Questionnaires for product: Questionnaires for product:
<select v-model="selected_product"> <select v-model="selected_product">
<option value="">(all)</option> <option value="">(all)</option>
<option v-for="item in items" :value="item.id"> <optgroup v-for="[category, items] in grouped_items" :label="category.internal_name || i18n_any(category.name)">
{{ i18n_any(item.name) }} <option v-for="item in items" :value="item.id">
</option> {{ item.internal_name || i18n_any(item.name) }}
</option>
</optgroup>
</select> </select>
</p> </p>
<div class="question-editor"> <div class="question-editor">
<SlickList axis="y" v-model:list="questionnaires" useDragHandle appendTo="#questionnaireListParent" id="questionnaireListParent"> <SlickList axis="y" v-model:list="position_questionnaires" useDragHandle appendTo="#questionnaireListParent" id="questionnaireListParent">
<SlickItem v-for="(questionnaire, index) in questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index"> <SlickItem v-for="(questionnaire, index) in position_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
<QuestionnaireElement <QuestionnaireElement
:questionnaire="questionnaire" :questionnaire="questionnaire"
:datafields="datafields" :datafields="datafields"
:items="items" :grouped_items="grouped_items"
:selected_product="selected_product" /> :selected_product="selected_product" />
</SlickItem> </SlickItem>
</SlickList> </SlickList>
</div> </div>
<p> <p>
<button class="btn btn-default" @click="addQuestionnaire()"><i class="fa fa-plus"></i> Neuen Fragebogen erstellen</button> <button class="btn btn-default" @click="addPositionQuestionnaire()"><i class="fa fa-plus"></i> Neuen Fragebogen erstellen</button>
<button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> Speichern</button> <button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> Speichern</button>
</p> </p>
</template> </template>
@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, useId, defineProps } from 'vue'; import { ref, useId, defineProps } from 'vue';
import {get_event_locales} from "./api"; import {getEventLocales} from "./api";
const locales = get_event_locales(); const locales = getEventLocales();
const props = defineProps(['value', 'id']); const props = defineProps(['value', 'id']);
@@ -4,7 +4,7 @@ import NativeDialog from './NativeDialog.vue';
import I18nTextField from './I18nTextField.vue'; import I18nTextField from './I18nTextField.vue';
import {useId, ref, computed} from 'vue' import {useId, ref, computed} from 'vue'
import { DragHandle } from 'vue-slicksort'; import { DragHandle } from 'vue-slicksort';
import {get_datafield_edit_url} from "./api"; import {getDatafieldEditUrl} from "./api";
const id = useId(); const id = useId();
const props = defineProps(['question', 'datafields', 'editable', 'possible_dependencies']) const props = defineProps(['question', 'datafields', 'editable', 'possible_dependencies'])
@@ -99,7 +99,7 @@ const editor = ref();
<p class="form-control-static"> <p class="form-control-static">
<template v-if="typeof question.question === 'number'"> <template v-if="typeof question.question === 'number'">
{{ df.internal_name }} {{ df.internal_name }}
<a :href="get_datafield_edit_url(df.id)" target="_blank">Manage data field details</a> <a :href="getDatafieldEditUrl(df.id)" target="_blank">Manage data field details</a>
</template> </template>
<template v-else> <template v-else>
{{ question.question }} {{ question.question }}
@@ -117,6 +117,18 @@ const editor = ref();
</select> </select>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-md-3 control-label label-empty">&nbsp;</label>
<div class="col-md-9">
<div class="checkbox">
<label>
<input type="checkbox" v-model="question.required">
{{ gettext('Required question') }}
</label>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label"> <label class="col-md-3 control-label">
{{ gettext('Only visible if...') }} {{ gettext('Only visible if...') }}
@@ -14,7 +14,7 @@ const newTextblockTitle = ref();
const newTextblockText = ref(); const newTextblockText = ref();
const id = useId(); const id = useId();
const props = defineProps(['questionnaire', 'datafields', 'selected_product', 'items']) const props = defineProps(['questionnaire', 'datafields', 'selected_product', 'grouped_items'])
const gettext = (window as any).gettext const gettext = (window as any).gettext
function toggleItem() { function toggleItem() {
@@ -106,15 +106,18 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
<input type="text" class="form-control" v-model="questionnaire.internal_name"/> <input type="text" class="form-control" v-model="questionnaire.internal_name"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" v-if="grouped_items">
<label class="col-md-3 control-label"> <label class="col-md-3 control-label">
{{ gettext('Visible on products') }} {{ gettext('Visible on products') }}
</label> </label>
<div class="col-md-9"> <div class="col-md-9">
<div class="checkbox" v-for="item in items"> <div v-for="[category, items] in grouped_items">
<label :for="id + '_' + item.id"> <div class="category-header">{{ category.internal_name || i18n_any(category.name) }}</div>
<input :id="id + '_' + item.id" type="checkbox" :checked="questionnaire.items.indexOf(item.id) !== -1"> {{ item.internal_name || i18n_any(item.name) }} <div class="checkbox" v-for="item in items">
</label> <label :for="id + '_' + item.id">
<input :id="id + '_' + item.id" type="checkbox" :checked="questionnaire.items.indexOf(item.id) !== -1"> {{ item.internal_name || i18n_any(item.name) }}
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -30,34 +30,38 @@ async function api_json_request(resource, method, json_body) {
})).json(); })).json();
} }
export async function get_datafields() { export async function getDatafields() {
return await api_get_all<Datafield>(`organizers/${organizer_slug}/events/${event_slug}/datafields/`); return await api_get_all<Datafield>(`organizers/${organizer_slug}/events/${event_slug}/datafields/`);
} }
export async function get_questionnaires() { export async function getQuestionnaires() {
return await api_get_all<Questionnaire>(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/`); return await api_get_all<Questionnaire>(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/`);
} }
export async function update_questionnaire(id, data) { export async function updateQuestionnaire(id, data) {
return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/${id}/`, 'PATCH', data); return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/${id}/`, 'PATCH', data);
} }
export async function create_questionnaire(data) { export async function createQuestionnaire(data) {
return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/`, 'POST', data); return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/`, 'POST', data);
} }
export async function get_items() { export async function getItems() {
return await api_get_all<Item>(`organizers/${organizer_slug}/events/${event_slug}/items/`); return await api_get_all<Item>(`organizers/${organizer_slug}/events/${event_slug}/items/`);
} }
export async function getCategories() {
return await api_get_all<Item>(`organizers/${organizer_slug}/events/${event_slug}/categories/`);
}
function get_json_script_value(id) { function get_json_script_value(id) {
return JSON.parse(document.getElementById(id).innerText); return JSON.parse(document.getElementById(id).innerText);
} }
export function get_event_locales() { export function getEventLocales() {
return get_json_script_value('event_locales'); return get_json_script_value('event_locales');
} }
export function get_datafield_edit_url(datafield_id) { export function getDatafieldEditUrl(datafield_id) {
return get_json_script_value('datafield_edit_url').replace('/0/', `/${datafield_id}/`); return get_json_script_value('datafield_edit_url').replace('/0/', `/${datafield_id}/`);
} }
@@ -10,6 +10,50 @@ function freezeRec(o) {
return Object.freeze(Object.fromEntries(Object.entries(o).map(([k, v]) => [k, v && Object.getPrototypeOf(v) === Object.prototype ? freezeRec(v) : v]))) 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 numericComp(fn) {
return function(a, b) {
return fn(a) - fn(b);
}
}
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);
if (result !== 0) {
return result;
}
}
return 0;
});
}
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];
}
lastKey = k; lastArray = [x];
} else {
lastArray.push(x);
}
}
if (lastArray) {
yield [lastKey, lastArray];
}
}
export const QUESTION_TYPE = { export const QUESTION_TYPE = {
NUMBER: "N", NUMBER: "N",
STRING: "S", STRING: "S",
@@ -25,7 +69,7 @@ export const QUESTION_TYPE = {
PHONENUMBER: "TEL", PHONENUMBER: "TEL",
}; };
const _ = x => x; export const _ = x => x;
export const QUESTION_TYPE_LABEL = { export const QUESTION_TYPE_LABEL = {
NUMBER: _("Number"), NUMBER: _("Number"),