mirror of
https://github.com/pretix/pretix.git
synced 2026-05-17 17:14:04 +00:00
setup questionnaires vue app, add code from question editor proof of concept
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
{% load vite %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% trans "Questionnaires" %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block inside %}
|
||||||
|
<h1>{% trans "Questionnaires" %}</h1>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Questions allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||||
|
example might be to ask your users about dietary requirements.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="questionnaires-editor">
|
||||||
|
<!-- Vue app mount point -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% vite_hmr %}
|
||||||
|
{% vite_asset "src/pretix/static/pretixcontrol/js/ui/questionnaires/index.ts" %}
|
||||||
|
{% endblock %}
|
||||||
@@ -348,6 +348,7 @@ urlpatterns = [
|
|||||||
re_path(r'^questions/(?P<question>\d+)/change$', item.QuestionUpdate.as_view(),
|
re_path(r'^questions/(?P<question>\d+)/change$', item.QuestionUpdate.as_view(),
|
||||||
name='event.items.questions.edit'),
|
name='event.items.questions.edit'),
|
||||||
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
||||||
|
re_path(r'^questionnaires/$', item.QuestionnairesEditor.as_view(), name='event.items.questionnaires'),
|
||||||
re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
||||||
re_path(r'^quotas/(?P<quota>\d+)/$', item.QuotaView.as_view(), name='event.items.quotas.show'),
|
re_path(r'^quotas/(?P<quota>\d+)/$', item.QuotaView.as_view(), name='event.items.quotas.show'),
|
||||||
re_path(r'^quotas/select$', typeahead.quotas_select2, name='event.items.quotas.select2'),
|
re_path(r'^quotas/select$', typeahead.quotas_select2, name='event.items.quotas.select2'),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView, TemplateView
|
||||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
|
|
||||||
@@ -831,6 +831,11 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionnairesEditor(EventPermissionRequiredMixin, TemplateView):
|
||||||
|
permission = 'can_change_items'
|
||||||
|
template_name = 'pretixcontrol/items/questionnaires.html'
|
||||||
|
|
||||||
|
|
||||||
class QuotaList(PaginationMixin, ListView):
|
class QuotaList(PaginationMixin, ListView):
|
||||||
model = Quota
|
model = Quota
|
||||||
context_object_name = 'quotas'
|
context_object_name = 'quotas'
|
||||||
|
|||||||
74
src/pretix/static/pretixcontrol/js/ui/questionnaires/App.vue
Normal file
74
src/pretix/static/pretixcontrol/js/ui/questionnaires/App.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script>
|
||||||
|
import Question from './Question.vue';
|
||||||
|
import {get_questions, get_items} from './api';
|
||||||
|
import { i18n_any, QUESTION_TYPE } from './helper';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const questions_response = await get_questions();
|
||||||
|
const items_response = await get_items();
|
||||||
|
|
||||||
|
const questions = ref(questions_response.results);
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Question
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
i18n_any,
|
||||||
|
addQuestion: function() {
|
||||||
|
questions.value.push({
|
||||||
|
items: [], question: {en:"Untitled question"},
|
||||||
|
type: QUESTION_TYPE.TEXT, help_text: {en:"Help text"},
|
||||||
|
})
|
||||||
|
console.log(questions.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
questions,
|
||||||
|
items: items_response.results,
|
||||||
|
selected_product: ref(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hidden-question { opacity: 0.3; }
|
||||||
|
.hidden-question label { text-decoration: line-through; }
|
||||||
|
|
||||||
|
.question-editor { margin-right: 140px; }
|
||||||
|
.question-edit-buttons { float:right }
|
||||||
|
.question-edit-buttons div { position: absolute; margin-left: 10px; }
|
||||||
|
.question-edit-buttons button { }
|
||||||
|
.form-group { margin-bottom: 30px }
|
||||||
|
</style>
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-default" @click="addQuestion()"><i class="fa fa-plus"></i> Neue Frage erstellen</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="panel panel-default question-editor">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Edit questions for product:
|
||||||
|
<select v-model="selected_product">
|
||||||
|
<option value="">(all)</option>
|
||||||
|
<option v-for="item in items" :value="item.id">
|
||||||
|
{{ i18n_any(item.name) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
|
||||||
|
<Question
|
||||||
|
v-for="question in questions"
|
||||||
|
:question="question"
|
||||||
|
:selected_product="selected_product" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, useId, defineProps } from 'vue';
|
||||||
|
|
||||||
|
defineProps(['value', 'id'])
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="i18n-form-group" :id="id">
|
||||||
|
<textarea cols="40" rows="2" lang="en" dir="ltr" class="form-control" title="Englisch" :id="`${id}_0`" placeholder="Englisch" v-model="value.en"></textarea>
|
||||||
|
<textarea cols="40" rows="2" lang="de" dir="ltr" class="form-control" title="Deutsch" :id="`${id}_1`" placeholder="Deutsch" v-model="value.de"></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, useId } from 'vue';
|
||||||
|
|
||||||
|
const dialog = ref<HTMLDialogElement>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
classes: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
dialog.value?.showModal();
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show: showModal,
|
||||||
|
close: (returnVal?: string): void => dialog.value?.close(returnVal),
|
||||||
|
visible,
|
||||||
|
});
|
||||||
|
const id = useId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<dialog
|
||||||
|
ref="dialog" class="modal-card"
|
||||||
|
@close="visible = false"
|
||||||
|
closedby="any"
|
||||||
|
:aria-labelledby="`${id}-title`"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
v-if="visible"
|
||||||
|
method="dialog" class="modal-card-inner form-horizontal"
|
||||||
|
:class="{
|
||||||
|
[props.classes]: props.classes,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="modal-card-content">
|
||||||
|
<h2 :id="`${id}-title`" class="modal-card-title h3">{{ title }}</h2>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import { i18n_any, QUESTION_TYPE, QUESTION_TYPE_LABEL } from './helper';
|
||||||
|
import NativeDialog from './NativeDialog.vue';
|
||||||
|
import I18nTextField from './I18nTextField.vue';
|
||||||
|
import { useId, ref } from 'vue'
|
||||||
|
const id = useId();
|
||||||
|
const props = defineProps(['question', 'selected_product'])
|
||||||
|
|
||||||
|
function toggleItem() {
|
||||||
|
const i = props.question.items.indexOf(props.selected_product);
|
||||||
|
if (i === -1) {
|
||||||
|
props.question.items.push(props.selected_product);
|
||||||
|
} else {
|
||||||
|
props.question.items.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const editor = ref();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="form-group"
|
||||||
|
:class="{ 'hidden-question': selected_product && question.items.indexOf(selected_product) === -1 }">
|
||||||
|
<div class="question-edit-buttons"><div>
|
||||||
|
<button class="btn btn-default"><i class="fa fa-arrows"></i></button>
|
||||||
|
<button class="btn btn-default" @click="editor.show()"><i class="fa fa-edit"></i></button>
|
||||||
|
<button class="btn btn-default" @click="toggleItem()" v-if="selected_product"><i :class="`fa fa-eye${(question.items.indexOf(selected_product) === -1) ? '-slash':''}`"></i></button>
|
||||||
|
</div></div>
|
||||||
|
<label class="col-md-3 control-label" :for="id" v-if="question.type != QUESTION_TYPE.BOOLEAN">
|
||||||
|
{{ i18n_any(question.question) }}
|
||||||
|
</label>
|
||||||
|
<div v-else class="col-md-3 control-label label-empty"></div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
|
||||||
|
<input :id="id" type="text" v-if="question.type == QUESTION_TYPE.TEXT" class="form-control">
|
||||||
|
<div class="checkbox" v-if="question.type == QUESTION_TYPE.BOOLEAN">
|
||||||
|
<label :for="id">
|
||||||
|
<input :id="id" type="checkbox"> {{ i18n_any(question.question) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input :id="id" type="number" v-if="question.type == QUESTION_TYPE.NUMBER" class="form-control">
|
||||||
|
<input :id="id" type="file" v-if="question.type == QUESTION_TYPE.FILE" class="form-control">
|
||||||
|
<select :id="id"
|
||||||
|
v-if="question.type == QUESTION_TYPE.CHOICE || question.type == QUESTION_TYPE.CHOICE_MULTIPLE"
|
||||||
|
:multiple="question.type == QUESTION_TYPE.CHOICE_MULTIPLE" class="form-control">
|
||||||
|
<option></option>
|
||||||
|
<option v-for="opt in question.options">{{ i18n_any(opt.answer) }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="help-block">{{ i18n_any(question.help_text) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<NativeDialog ref="editor" class="modal-card"
|
||||||
|
title="Edit question">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">
|
||||||
|
Question
|
||||||
|
</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<I18nTextField :value="question.question"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">
|
||||||
|
Question type
|
||||||
|
</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<select v-model="question.type" class="form-control">
|
||||||
|
<option v-for="(label, type) in QUESTION_TYPE_LABEL" :value="QUESTION_TYPE[type]">{{ label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">
|
||||||
|
Help text
|
||||||
|
</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<I18nTextField :value="question.help_text"/>
|
||||||
|
<div class="help-block">Wenn diese Frage noch weitere Erklärung braucht, können Sie sie hier eintragen.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="editor.close()">Close</button>
|
||||||
|
</NativeDialog>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
10
src/pretix/static/pretixcontrol/js/ui/questionnaires/api.ts
Normal file
10
src/pretix/static/pretixcontrol/js/ui/questionnaires/api.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const organizer_slug = document.body.getAttribute('data-organizer'),
|
||||||
|
event_slug = document.body.getAttribute('data-event');
|
||||||
|
|
||||||
|
export async function get_questions() {
|
||||||
|
return await (await fetch(`/api/v1/organizers/${organizer_slug}/events/${event_slug}/questions`)).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_items() {
|
||||||
|
return await (await fetch(`/api/v1/organizers/${organizer_slug}/events/${event_slug}/items`)).json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
export function i18n_any(data) {
|
||||||
|
return Object.values(data)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
const _ = x => x;
|
||||||
|
|
||||||
|
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"),
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#questionnaires-editor')
|
||||||
Reference in New Issue
Block a user