mirror of
https://github.com/pretix/pretix.git
synced 2026-08-09 10:37:50 +00:00
wip
This commit is contained in:
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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"> </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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user