mirror of
https://github.com/pretix/pretix.git
synced 2026-08-06 10:07:50 +00:00
Add writable questions API methods
This commit is contained in:
@@ -13,6 +13,7 @@ Resources and endpoints
|
||||
item_variations
|
||||
item_add-ons
|
||||
questions
|
||||
question_options
|
||||
quotas
|
||||
orders
|
||||
invoices
|
||||
|
||||
@@ -148,7 +148,7 @@ Endpoints
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -135,7 +135,7 @@ Endpoints
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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)),
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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."]}'
|
||||
|
||||
Reference in New Issue
Block a user