Add identifier field to questions

This commit is contained in:
Raphael Michel
2018-03-12 13:47:29 +01:00
parent 234e0ee764
commit f21da0cc2b
10 changed files with 157 additions and 9 deletions

View File

@@ -142,7 +142,7 @@ class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
class Meta:
model = QuestionOption
fields = ('id', 'answer')
fields = ('id', 'identifier', 'answer')
class QuestionSerializer(I18nAwareModelSerializer):
@@ -151,7 +151,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin')
'ask_during_checkin', 'identifier')
class QuotaSerializer(I18nAwareModelSerializer):

View File

@@ -29,10 +29,23 @@ class InvoiceAdddressSerializer(I18nAwareModelSerializer):
'vat_id_validated', 'internal_reference')
class AnswerQuestionIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return instance.question.identifier
class AnswerQuestionOptionsIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.identifier for o in instance.options.all()]
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'options')
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
class CheckinSerializer(I18nAwareModelSerializer):

View File

@@ -50,7 +50,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'fees'
'positions__answers__questions', 'fees'
).select_related(
'invoice_address'
)
@@ -234,7 +234,7 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
'checkins', 'answers', 'answers__options'
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-03-12 11:19
from __future__ import unicode_literals
from django.db import migrations, models
from django.utils.crypto import get_random_string
def set_identifiers(apps, schema_editor):
Question = apps.get_model('pretixbase', 'Question')
QuestionOption = apps.get_model('pretixbase', 'QuestionOption')
for q in Question.objects.select_related('event'):
if not q.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not Question.objects.filter(event=q.event, identifier=code).exists():
q.identifier = code
q.save()
break
for q in QuestionOption.objects.select_related('question', 'question__event'):
if not q.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not QuestionOption.objects.filter(question__event=q.question.event, identifier=code).exists():
q.identifier = code
q.save()
break
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0084_questionoption_position'),
]
operations = [
migrations.AddField(
model_name='question',
name='identifier',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AddField(
model_name='questionoption',
name='identifier',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AlterField(
model_name='user',
name='locale',
field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'), ('da', 'Danish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50, verbose_name='Language'),
),
migrations.RunPython(set_identifiers, migrations.RunPython.noop)
]

View File

@@ -11,6 +11,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Func, Q, Sum
from django.utils import formats
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -630,6 +631,8 @@ class Question(LoggedModel):
:param items: A set of ``Items`` objects that this question should be applied to
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
:type ask_during_checkin: bool
:param identifier: An arbitrary, internal identifier
:type identifier: str
"""
TYPE_NUMBER = "N"
TYPE_STRING = "S"
@@ -661,6 +664,12 @@ class Question(LoggedModel):
question = I18nTextField(
verbose_name=_("Question")
)
identifier = models.CharField(
max_length=190,
verbose_name=_("Internal identifier"),
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.')
)
help_text = I18nTextField(
verbose_name=_("Help text"),
help_text=_("If the question needs to be explained or clarified, do it here!"),
@@ -706,7 +715,18 @@ class Question(LoggedModel):
if self.event:
self.event.cache.clear()
def clean_identifier(self, code):
if Question.objects.filter(event=self.event, identifier=code).exclude(pk=self.pk).exists():
raise ValidationError(_('This identifier is already used for a different question.'))
def save(self, *args, **kwargs):
if not self.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not Question.objects.filter(event=self.event, identifier=code).exists():
self.identifier = code
break
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -779,12 +799,23 @@ class Question(LoggedModel):
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options')
identifier = models.CharField(max_length=190)
answer = I18nCharField(verbose_name=_('Answer'))
position = models.IntegerField(default=0)
def __str__(self):
return str(self.answer)
def save(self, *args, **kwargs):
if not self.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not QuestionOption.objects.filter(question__event=self.question.event, identifier=code).exists():
self.identifier = code
break
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Question option")
verbose_name_plural = _("Question options")

View File

@@ -41,6 +41,7 @@ class QuestionForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['items'].queryset = self.instance.event.items.all()
self.fields['identifier'].required = False
class Meta:
model = Question
@@ -51,6 +52,7 @@ class QuestionForm(I18nModelForm):
'type',
'required',
'ask_during_checkin',
'identifier',
'items'
]
widgets = {

View File

@@ -23,6 +23,7 @@
{% bootstrap_field form.question layout="control" %}
{% bootstrap_field form.help_text layout="control" %}
{% bootstrap_field form.type layout="control" %}
{% bootstrap_field form.identifier layout="control" %}
{% bootstrap_field form.ask_during_checkin layout="control" %}
{% bootstrap_field form.required layout="control" %}
</fieldset>
@@ -54,10 +55,16 @@
</div>
<div class="row question-option-row">
<div class="col-xs-10">
<span class="text-muted">
{% blocktrans trimmed with id=form.instance.identifier %}
Answer option {{ id }}
{% endblocktrans %}
</span>
{% bootstrap_form_errors form %}
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
</div>
<div class="col-xs-2 text-right">
<span>&nbsp;</span><br>
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
@@ -79,9 +86,13 @@
</div>
<div class="row question-option-row">
<div class="col-xs-10">
<span class="text-muted">
{% trans "New answer option" %}
</span>
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
</div>
<div class="col-xs-2 text-right">
<span>&nbsp;</span><br>
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>

View File

@@ -1187,9 +1187,9 @@ def test_quota_availability(token_client, organizer, event, quota, item):
@pytest.fixture
def question(event, item):
q = event.questions.create(question="T-Shirt size", type="C")
q = event.questions.create(question="T-Shirt size", type="C", identifier="ABC")
q.items.add(item)
q.options.create(answer="XL")
q.options.create(answer="XL", identifier="FOO")
return q
@@ -1199,10 +1199,12 @@ TEST_QUESTION_RES = {
"required": False,
"items": [],
"ask_during_checkin": False,
"identifier": "ABC",
"position": 0,
"options": [
{
"id": 0,
"identifier": "FOO",
"answer": {"en": "XL"}
}
]