mirror of
https://github.com/pretix/pretix.git
synced 2026-05-16 17:03:58 +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(),
|
||||
name='event.items.questions.edit'),
|
||||
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/(?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'),
|
||||
|
||||
@@ -55,7 +55,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
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_countries.fields import Country
|
||||
|
||||
@@ -831,6 +831,11 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
||||
return ret
|
||||
|
||||
|
||||
class QuestionnairesEditor(EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/items/questionnaires.html'
|
||||
|
||||
|
||||
class QuotaList(PaginationMixin, ListView):
|
||||
model = Quota
|
||||
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