Add writable questions API methods

This commit is contained in:
Raphael Michel
2018-03-13 09:09:50 +01:00
parent aef77965e7
commit dffc82781b
12 changed files with 844 additions and 21 deletions
+1
View File
@@ -13,6 +13,7 @@ Resources and endpoints
item_variations
item_add-ons
questions
question_options
quotas
orders
invoices
+1 -1
View File
@@ -148,7 +148,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
+2 -2
View File
@@ -6,7 +6,7 @@ Resource description
Variations of items can be use for products (items) that are available in different sizes, colors or other variations
of the same product.
The addons resource contains the following public fields:
The variations resource contains the following public fields:
.. rst-class:: rest-resource-table
@@ -158,7 +158,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
+7 -5
View File
@@ -56,7 +56,8 @@ checkin_attention boolean If ``True``, th
a product is being scanned.
has_variations boolean Shows whether or not this item has variations.
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable on POST.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
├ id integer Internal ID of the variation
├ default_price money (string) The price set directly for this variation or ``null``
├ price money (string) The price used for this variation. This is either the
@@ -67,7 +68,8 @@ variations list of objects A list with one
Markdown syntax or can be ``null``.
└ position integer An integer, used for sorting
addons list of objects Definition of add-ons that can be chosen for this item.
Only writable on POST.
Only writable during creation,
use separate endpoint to modify this later.
├ addon_category integer Internal ID of the item category the add-on can be
chosen from.
├ min_count integer The minimal number of add-ons that need to be chosen.
@@ -256,7 +258,7 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/
Creates a new item
@@ -315,7 +317,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
@@ -369,7 +371,7 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/
Update an item. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
+233
View File
@@ -0,0 +1,233 @@
Question options
================
Resource description
--------------------
Questions of type "choice" or "multiple choice" can have different options attached.
The options resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the option
position integer An integer, used for sorting
identifier string An arbitrary string that can be used for matching with
other sources.
answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/
Returns a list of all options for a given question.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questions/11/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query boolean active: If set to ``true`` or ``false``, only questions with this value for the field ``active`` will be
returned.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param question: The ``id`` field of the question to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/question does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/(id)/
Returns information on one option, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param question: The ``id`` field of the question to fetch
:param id: The ``id`` field of the option to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/
Creates a new option
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer of the event/question to create a option for
:param event: The ``slug`` field of the event to create a option for
:param question: The ``id`` field of the question to create a option for
:statuscode 201: no error
:statuscode 400: The option could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/(id)/
Update an option. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 3
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:param id: The ``id`` field of the option to modify
:statuscode 200: no error
:statuscode 400: The option could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/options/(id)/
Delete an option.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:param id: The ``id`` field of the option to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+187 -2
View File
@@ -37,8 +37,10 @@ ask_during_checkin boolean If ``True``, th
buying the ticket, but will show up when redeeming
the ticket instead.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects.
available objects. Only writable during creation,
use separate endpoint to modify this later.
├ id integer Internal ID of the option
├ position integer An integer, used for sorting
├ identifier string An arbitrary string that can be used for matching with
other sources.
└ answer multi-lingual string The displayed value of this option
@@ -51,7 +53,8 @@ options list of objects In case of ques
.. versionchanged:: 1.14
The attribute ``identifier`` has been added to both the resource itself and the ``options`` subresource.
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
options resource. The ``position`` attribute has been added to the options resource.
Endpoints
---------
@@ -94,16 +97,19 @@ Endpoints
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
]
@@ -153,16 +159,19 @@ Endpoints
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
@@ -174,3 +183,179 @@ Endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questions/
Creates a new question
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/questions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"options": [
{
"answer": {"en": "S"}
},
{
"answer": {"en": "M"}
},
{
"answer": {"en": "L"}
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
:param event: The ``slug`` field of the event to create an item for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/
Update a question. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``options`` field. If
you need to update/delete options please use the nested dedicated endpoints.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 2
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:statuscode 200: no error
:statuscode 400: The item could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/
Delete a question.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+1 -1
View File
@@ -135,7 +135,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
+55 -3
View File
@@ -132,21 +132,73 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
fields = ('id', 'name', 'description', 'position', 'is_addon')
class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
class QuestionOptionSerializer(I18nAwareModelSerializer):
identifier = serializers.CharField(allow_null=True)
class Meta:
model = QuestionOption
fields = ('id', 'identifier', 'answer')
fields = ('id', 'identifier', 'answer', 'position')
def validate_identifier(self, value):
QuestionOption.clean_identifier(self.context['event'], value, self.instance)
return value
class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
identifier = serializers.CharField(allow_null=True)
class Meta:
model = QuestionOption
fields = ('id', 'identifier', 'answer', 'position')
class QuestionSerializer(I18nAwareModelSerializer):
options = InlineQuestionOptionSerializer(many=True)
options = InlineQuestionOptionSerializer(many=True, required=False)
identifier = serializers.CharField(allow_null=True)
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)
return value
def validate(self, data):
data = super().validate(data)
if self.instance and 'options' in data:
raise ValidationError(_('Updating options via PATCH/PUT is not supported. Please use the dedicated'
' nested endpoint.'))
event = self.context['event']
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
Question.clean_items(event, full_data.get('items'))
return data
def validate_options(self, value):
if not self.instance:
known = []
for opt_data in value:
if opt_data.get('identifier'):
QuestionOption.clean_identifier(self.context['event'], opt_data.get('identifier'), self.instance,
known)
known.append(opt_data.get('identifier'))
return value
@transaction.atomic
def create(self, validated_data):
options_data = validated_data.pop('options') if 'options' in validated_data else []
items = validated_data.pop('items')
question = Question.objects.create(**validated_data)
question.items.set(items)
for opt_data in options_data:
QuestionOption.objects.create(question=question, **opt_data)
return question
class QuotaSerializer(I18nAwareModelSerializer):
+5
View File
@@ -29,6 +29,9 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
question_router = routers.DefaultRouter()
question_router.register(r'options', item.QuestionOptionViewSet)
item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
@@ -44,6 +47,8 @@ urlpatterns = [
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
include(question_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
include(checkinlist_router.urls)),
]
+84 -3
View File
@@ -10,10 +10,12 @@ from rest_framework.response import Response
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
ItemVariationSerializer, QuestionSerializer, QuotaSerializer,
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
QuotaSerializer,
)
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, Quota,
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
@@ -214,7 +216,7 @@ class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
return self.request.event.categories.all()
class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
class QuestionViewSet(viewsets.ModelViewSet):
serializer_class = QuestionSerializer
queryset = Question.objects.none()
filter_backends = (OrderingFilter,)
@@ -225,6 +227,85 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.question.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.question.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.question.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
class QuestionOptionViewSet(viewsets.ModelViewSet):
serializer_class = QuestionOptionSerializer
queryset = QuestionOption.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
return q.options.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
return ctx
def perform_create(self, serializer):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
serializer.save(question=q)
q.log_action(
'pretix.event.question.option.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.question.log_action(
'pretix.event.question.option.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
instance.question.log_action(
'pretix.event.question.option.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={'id': instance.pk}
)
super().perform_destroy(instance)
class QuotaFilter(FilterSet):
class Meta:
+22 -1
View File
@@ -716,7 +716,14 @@ class Question(LoggedModel):
self.event.cache.clear()
def clean_identifier(self, code):
if Question.objects.filter(event=self.event, identifier=code).exclude(pk=self.pk).exists():
Question._clean_identifier(self.event, code, self)
@staticmethod
def _clean_identifier(event, code, instance=None):
qs = Question.objects.filter(event=event, identifier=code)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists():
raise ValidationError(_('This identifier is already used for a different question.'))
def save(self, *args, **kwargs):
@@ -796,6 +803,12 @@ class Question(LoggedModel):
return answer
@staticmethod
def clean_items(event, items):
for item in items:
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options')
@@ -816,6 +829,14 @@ class QuestionOption(models.Model):
break
super().save(*args, **kwargs)
@staticmethod
def clean_identifier(event, code, instance=None, known=[]):
qs = QuestionOption.objects.filter(question__event=event, identifier=code)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists() or code in known:
raise ValidationError(_('The identifier "{}" is already used for a different option.').format(code))
class Meta:
verbose_name = _("Question option")
verbose_name_plural = _("Question options")
+246 -3
View File
@@ -8,7 +8,7 @@ from pytz import UTC
from pretix.base.models import (
CartPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Order,
OrderPosition, Quota,
OrderPosition, Question, QuestionOption, Quota,
)
from pretix.base.models.orders import OrderFee
@@ -843,6 +843,11 @@ def addon(item, category):
return item.addons.create(addon_category=category, min_count=0, max_count=10, position=1)
@pytest.fixture
def option(question):
return question.options.create(answer='XL', identifier='LVETRWVU')
TEST_ADDONS_RES = {
"min_count": 0,
"max_count": 10,
@@ -1200,7 +1205,7 @@ def test_quota_availability(token_client, organizer, event, quota, item):
def question(event, item):
q = event.questions.create(question="T-Shirt size", type="C", identifier="ABC")
q.items.add(item)
q.options.create(answer="XL", identifier="FOO")
q.options.create(answer="XL", identifier="LVETRWVU")
return q
@@ -1215,7 +1220,8 @@ TEST_QUESTION_RES = {
"options": [
{
"id": 0,
"identifier": "FOO",
"position": 0,
"identifier": "LVETRWVU",
"answer": {"en": "XL"}
}
]
@@ -1246,3 +1252,240 @@ def test_question_detail(token_client, organizer, event, question, item):
question.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_question_create(token_client, organizer, event, event2, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
{
"question": "What's your name?",
"type": "S",
"required": True,
"items": [item.pk],
"position": 0,
"ask_during_checkin": False,
"identifier": None
},
format='json'
)
assert resp.status_code == 201
question = Question.objects.get(pk=resp.data['id'])
assert question.question == "What's your name?"
assert question.type == "S"
assert question.identifier is not None
assert len(question.items.all()) == 1
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event2.slug),
{
"question": "What's your name?",
"type": "S",
"required": True,
"items": [item.pk],
"position": 0,
"ask_during_checkin": False,
"identifier": None
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["One or more items do not belong to this event."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
{
"question": "What's your name?",
"type": "S",
"required": True,
"items": [item.pk],
"position": 0,
"ask_during_checkin": False,
"identifier": question.identifier
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"identifier":["This identifier is already used for a different question."]}'
@pytest.mark.django_db
def test_question_update(token_client, organizer, event, question):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk),
{
"question": "What's your shoe size?",
"type": "N",
},
format='json'
)
print(resp.content)
assert resp.status_code == 200
question = Question.objects.get(pk=resp.data['id'])
assert question.question == "What's your shoe size?"
assert question.type == "N"
@pytest.mark.django_db
def test_question_update_options(token_client, organizer, event, question, item):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk),
{
"options": [
]
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating options via PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
@pytest.mark.django_db
def test_question_delete(token_client, organizer, event, question):
resp = token_client.delete('/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk))
assert resp.status_code == 204
assert not event.questions.filter(pk=question.id).exists()
TEST_OPTIONS_RES = {
"identifier": "LVETRWVU",
"answer": {"en": "XL"},
"position": 0
}
@pytest.mark.django_db
def test_options_list(token_client, organizer, event, question, option):
res = dict(TEST_OPTIONS_RES)
res["id"] = option.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/questions/{}/options/'.format(
organizer.slug, event.slug, question.pk)
)
assert resp.status_code == 200
assert res['identifier'] == resp.data['results'][0]['identifier']
assert res['answer'] == resp.data['results'][0]['answer']
@pytest.mark.django_db
def test_options_detail(token_client, organizer, event, question, option):
res = dict(TEST_OPTIONS_RES)
res["id"] = option.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/questions/{}/options/{}/'.format(
organizer.slug, event.slug, question.pk, option.pk
))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_options_create(token_client, organizer, event, question):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/{}/options/'.format(organizer.slug, event.slug, question.pk),
{
"identifier": "DFEMJWMJ",
"answer": "A",
"position": 0
},
format='json'
)
assert resp.status_code == 201
option = QuestionOption.objects.get(pk=resp.data['id'])
assert option.answer == "A"
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/{}/options/'.format(organizer.slug, event.slug, question.pk),
{
"identifier": "DFEMJWMJ",
"answer": "A",
"position": 0
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"identifier":["The identifier \\"DFEMJWMJ\\" is already used for a different option."]}'
@pytest.mark.django_db
def test_options_update(token_client, organizer, event, question, option):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/questions/{}/options/{}/'.format(organizer.slug, event.slug, question.pk, option.pk),
{
"answer": "B",
},
format='json'
)
assert resp.status_code == 200
a = QuestionOption.objects.get(pk=option.pk)
assert a.answer == "B"
@pytest.mark.django_db
def test_options_delete(token_client, organizer, event, question, option):
resp = token_client.delete('/api/v1/organizers/{}/events/{}/questions/{}/options/{}/'.format(
organizer.slug, event.slug, question.pk, option.pk
))
assert resp.status_code == 204
assert not question.options.filter(pk=option.id).exists()
@pytest.mark.django_db
def test_question_create_with_option(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
{
"question": "What's your name?",
"type": "S",
"required": True,
"items": [item.pk],
"position": 0,
"ask_during_checkin": False,
"identifier": None,
"options": [
{
"identifier": None,
"answer": {"en": "A"},
"position": 0,
},
{
"identifier": None,
"answer": {"en": "B"},
"position": 1,
},
]
},
format='json'
)
assert resp.status_code == 201
question = Question.objects.get(pk=resp.data['id'])
assert str(question.options.first().answer) == "A"
assert question.options.first().identifier is not None
assert str(question.options.last().answer) == "B"
assert 2 == question.options.count()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
{
"question": "What's your name?",
"type": "S",
"required": True,
"items": [item.pk],
"position": 0,
"ask_during_checkin": False,
"identifier": None,
"options": [
{
"identifier": "ABC",
"answer": {"en": "A"},
"position": 0,
},
{
"identifier": "ABC",
"answer": {"en": "B"},
"position": 1,
},
]
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"options":["The identifier \\"ABC\\" is already used for a different option."]}'