forked from CGM_Public/pretix_original
Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac14154b5d | ||
|
|
3352e743f7 | ||
|
|
79d8c17aaa | ||
|
|
ef7ce21ff3 | ||
|
|
a8bcc6206f | ||
|
|
ddfeafc5e2 | ||
|
|
85cdd40102 | ||
|
|
d412d8536c | ||
|
|
bf1a314076 | ||
|
|
e2b2ff7f6f | ||
|
|
a589c9aa69 | ||
|
|
d5b05e391a | ||
|
|
dc7a20280f | ||
|
|
d36fc45c99 | ||
|
|
990d92e569 | ||
|
|
7d4ef4f9a1 | ||
|
|
7baabcef96 | ||
|
|
ded15ecc3f | ||
|
|
0ad3ec444c | ||
|
|
7939503a11 | ||
|
|
8564f93706 | ||
|
|
f3e550d003 | ||
|
|
6c525b5dcd | ||
|
|
bb10d25561 | ||
|
|
7ec5adb6b4 | ||
|
|
ffb73d61fc | ||
|
|
3ee6c34d08 | ||
|
|
2c26ccbc72 | ||
|
|
cfbde151fa | ||
|
|
e278978ad9 | ||
|
|
a284e0c2f7 | ||
|
|
558c920181 | ||
|
|
7622fe9fc5 | ||
|
|
b32aec682c | ||
|
|
d54d25a432 | ||
|
|
ba19bdb90a | ||
|
|
58d10fac84 | ||
|
|
eecc1def2a | ||
|
|
f75fbc3744 | ||
|
|
07750c1f8c | ||
|
|
9ae0d9b0a1 | ||
|
|
c9f5828eb9 | ||
|
|
35a6a1883c | ||
|
|
080c48327e | ||
|
|
28506538a3 | ||
|
|
d578dedd0c | ||
|
|
c7aa105517 | ||
|
|
3c140c3e4d | ||
|
|
8d94b67aaa | ||
|
|
887cca109f | ||
|
|
239061b688 | ||
|
|
6d067428e0 | ||
|
|
b11f13181e | ||
|
|
e52b8e9a13 | ||
|
|
aa567ab078 | ||
|
|
50daf49cf0 | ||
|
|
a49fb65fac | ||
|
|
adfd1e614d | ||
|
|
a3eef04342 | ||
|
|
cf5e660951 | ||
|
|
671d6acfff | ||
|
|
125e759120 | ||
|
|
e81832e48f | ||
|
|
0b17da3b87 | ||
|
|
8379902adb | ||
|
|
00c4ffc154 | ||
|
|
3473337e7d | ||
|
|
4493178693 | ||
|
|
40452dcefe | ||
|
|
082afadb5b | ||
|
|
9ca61f9ef5 | ||
|
|
28a628ec93 | ||
|
|
12b5e21314 | ||
|
|
95aaccb35e | ||
|
|
ac053b00e8 | ||
|
|
938c7df28a | ||
|
|
6e22ea178b | ||
|
|
253f336509 | ||
|
|
3a7e0da80b | ||
|
|
073860cd5b | ||
|
|
18be4db320 | ||
|
|
5cbcbe6d7e | ||
|
|
1ef3f83e46 | ||
|
|
6ab0a839b1 | ||
|
|
e329753939 | ||
|
|
843751b53f | ||
|
|
1f083a52eb | ||
|
|
879eb6ee9f | ||
|
|
2db1e6b596 | ||
|
|
94f5ba7d1a | ||
|
|
e7458f3032 | ||
|
|
840cee206a | ||
|
|
511cdbbfe2 | ||
|
|
35f1999b3a | ||
|
|
3f55c694b8 | ||
|
|
6df0147fe9 | ||
|
|
5e3b4b126e | ||
|
|
b564fe8a0d | ||
|
|
1dc3a7202a | ||
|
|
cfa01d3c15 | ||
|
|
c4cac468ff | ||
|
|
a034bf9710 | ||
|
|
0336b0a15c | ||
|
|
94a2cfe7fc | ||
|
|
d22d0fdab5 | ||
|
|
6f9f47bfe3 | ||
|
|
b2402cdd39 | ||
|
|
289e1ee315 | ||
|
|
c1d1cbda70 | ||
|
|
17bac3714d | ||
|
|
9699fb8894 | ||
|
|
16809c0136 | ||
|
|
e0408510b8 | ||
|
|
14782c184f | ||
|
|
527f5c5ae6 | ||
|
|
b9aa4f1482 | ||
|
|
5ea6234cfb | ||
|
|
d056e1de1e | ||
|
|
2feb0246ff | ||
|
|
f4be045f08 | ||
|
|
a42ca225e3 | ||
|
|
5fb1bed9d2 | ||
|
|
c0c903d81a | ||
|
|
60b56d61ed | ||
|
|
fe1f334e2e | ||
|
|
fe4d70a9f9 | ||
|
|
93fded58f0 | ||
|
|
9a7b8ca27d | ||
|
|
aa7d75fae4 | ||
|
|
be345ffcc1 | ||
|
|
b3589312f8 | ||
|
|
08bd45d07e | ||
|
|
9af60c4192 | ||
|
|
6c48a94ab3 | ||
|
|
2897be7a10 | ||
|
|
69c0b04be6 | ||
|
|
837e309781 | ||
|
|
dca369ba30 | ||
|
|
b49d66aa68 | ||
|
|
4ce8c82244 | ||
|
|
ab9a530403 | ||
|
|
f9d91178b7 | ||
|
|
d0b8232a36 | ||
|
|
8f21e2368f | ||
|
|
87ec1a9fc5 | ||
|
|
cb1dcab37d | ||
|
|
91980277e1 | ||
|
|
c18b259b27 | ||
|
|
dffc82781b | ||
|
|
aef77965e7 | ||
|
|
f21da0cc2b | ||
|
|
234e0ee764 | ||
|
|
4262bb801e | ||
|
|
093941f8ba | ||
|
|
1cb1c35e2a | ||
|
|
432535e238 | ||
|
|
b40dc9d96d | ||
|
|
cb12e1208b | ||
|
|
b8225bd206 | ||
|
|
880c22eef9 | ||
|
|
b379c8380d | ||
|
|
6a61a113b0 | ||
|
|
4373eae1fe | ||
|
|
d12e4305bd | ||
|
|
7c8a45fd4c | ||
|
|
c28f8f763a | ||
|
|
096de6cddf | ||
|
|
ad52476159 | ||
|
|
e3d11ab681 | ||
|
|
162f37e00f | ||
|
|
d879634810 | ||
|
|
4634f853f1 | ||
|
|
ae861f080b | ||
|
|
d058721243 | ||
|
|
9fdef5eb5d | ||
|
|
b4488bf1e7 | ||
|
|
fa6d6b5438 | ||
|
|
02f53a55cc | ||
|
|
fdf0b6263a | ||
|
|
59fa5112fc | ||
|
|
a8033248ae | ||
|
|
9a6f299b41 | ||
|
|
2f1ee93e86 | ||
|
|
34fa5d6bfc | ||
|
|
357f728043 | ||
|
|
9522ee93dc |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -8,6 +8,7 @@ src/static/fileupload/* linguist-vendored
|
|||||||
src/static/vuejs/* linguist-vendored
|
src/static/vuejs/* linguist-vendored
|
||||||
src/static/select2/* linguist-vendored
|
src/static/select2/* linguist-vendored
|
||||||
src/static/charts/* linguist-vendored
|
src/static/charts/* linguist-vendored
|
||||||
|
src/static/rrule/* linguist-vendored
|
||||||
src/static/iframeresizer/* linguist-vendored
|
src/static/iframeresizer/* linguist-vendored
|
||||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
||||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
||||||
|
|||||||
@@ -43,3 +43,6 @@ addons:
|
|||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
- enchant
|
- enchant
|
||||||
|
branches:
|
||||||
|
except:
|
||||||
|
- /^weblate-.*/
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ Contributing
|
|||||||
If you want to contribute to pretix, please read the `developer documentation`_
|
If you want to contribute to pretix, please read the `developer documentation`_
|
||||||
in our documentation. If you have any further questions, please do not hesitate to ask!
|
in our documentation. If you have any further questions, please do not hesitate to ask!
|
||||||
|
|
||||||
|
.. image:: https://translate.pretix.eu/widgets/pretix/-/pretix/multi-blue.svg
|
||||||
|
:target: https://translate.pretix.eu/engage/pretix/
|
||||||
|
|
||||||
Code of Conduct
|
Code of Conduct
|
||||||
---------------
|
---------------
|
||||||
We have a `Code of Conduct`_ in place that applies to all project contributions,
|
We have a `Code of Conduct`_ in place that applies to all project contributions,
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ Example::
|
|||||||
that are used to print tax amounts in the customer currency on invoices for some currencies. Set to ``off`` to
|
that are used to print tax amounts in the customer currency on invoices for some currencies. Set to ``off`` to
|
||||||
disable this feature. Defaults to ``on``.
|
disable this feature. Defaults to ``on``.
|
||||||
|
|
||||||
|
``audit_comments``
|
||||||
|
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
|
||||||
|
Defaults to ``off``.
|
||||||
|
|
||||||
|
|
||||||
Locale settings
|
Locale settings
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
@@ -268,8 +268,8 @@ to re-build your custom image after you pulled ``pretix/standalone`` if you want
|
|||||||
.. _pretix.eu: https://pretix.eu/
|
.. _pretix.eu: https://pretix.eu/
|
||||||
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
||||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
|
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
|
||||||
.. _redis: http://blog.programster.org/debian-8-install-redis-server/
|
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||||
.. _redis website: http://redis.io/topics/security
|
.. _redis website: https://redis.io/topics/security
|
||||||
.. _redis in docker: https://hub.docker.com/r/_/redis/
|
.. _redis in docker: https://hub.docker.com/r/_/redis/
|
||||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||||
|
|||||||
@@ -298,6 +298,6 @@ example::
|
|||||||
.. _pretix.eu: https://pretix.eu/
|
.. _pretix.eu: https://pretix.eu/
|
||||||
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
||||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
|
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
|
||||||
.. _redis: http://blog.programster.org/debian-8-install-redis-server/
|
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ is_addon boolean If ``True``, it
|
|||||||
defining add-ons for other products.
|
defining add-ons for other products.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.14
|
||||||
|
|
||||||
|
The operations POST, PATCH, PUT and DELETE have been added.
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -106,3 +110,118 @@ Endpoints
|
|||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
: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)/categories/
|
||||||
|
|
||||||
|
Creates a new category
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "Tickets"},
|
||||||
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
|
"position": 1,
|
||||||
|
"is_addon": false
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": {"en": "Tickets"},
|
||||||
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
|
"position": 1,
|
||||||
|
"is_addon": false
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
||||||
|
:param event: The ``slug`` field of the event to create a category for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The category 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)/categories/(id)/
|
||||||
|
|
||||||
|
Update a category. 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/categories/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"is_addon": true
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": {"en": "Tickets"},
|
||||||
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
|
"position": 1,
|
||||||
|
"is_addon": true
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 category to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The category 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)/category/(id)/
|
||||||
|
|
||||||
|
Delete a category.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/categories/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 category 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.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Resources and endpoints
|
|||||||
item_variations
|
item_variations
|
||||||
item_add-ons
|
item_add-ons
|
||||||
questions
|
questions
|
||||||
|
question_options
|
||||||
quotas
|
quotas
|
||||||
orders
|
orders
|
||||||
invoices
|
invoices
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ Endpoints
|
|||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 201 Created
|
||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
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
|
Variations of items can be use for products (items) that are available in different sizes, colors or other variations
|
||||||
of the same product.
|
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
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ Endpoints
|
|||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 201 Created
|
||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ checkin_attention boolean If ``True``, th
|
|||||||
a product is being scanned.
|
a product is being scanned.
|
||||||
has_variations boolean Shows whether or not this item has variations.
|
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.
|
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
|
├ id integer Internal ID of the variation
|
||||||
├ default_price money (string) The price set directly for this variation or ``null``
|
├ 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
|
├ 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``.
|
Markdown syntax or can be ``null``.
|
||||||
└ position integer An integer, used for sorting
|
└ position integer An integer, used for sorting
|
||||||
addons list of objects Definition of add-ons that can be chosen for this item.
|
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
|
├ addon_category integer Internal ID of the item category the add-on can be
|
||||||
chosen from.
|
chosen from.
|
||||||
├ min_count integer The minimal number of add-ons that need to be chosen.
|
├ min_count integer The minimal number of add-ons that need to be chosen.
|
||||||
@@ -256,7 +258,7 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
: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
|
Creates a new item
|
||||||
|
|
||||||
@@ -315,7 +317,7 @@ Endpoints
|
|||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 201 Created
|
||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
@@ -369,7 +371,7 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
: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
|
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
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
|||||||
@@ -129,7 +129,9 @@ downloads list of objects List of ticket
|
|||||||
answers list of objects Answers to user-defined questions
|
answers list of objects Answers to user-defined questions
|
||||||
├ question integer Internal ID of the answered question
|
├ question integer Internal ID of the answered question
|
||||||
├ answer string Text representation of the answer
|
├ answer string Text representation of the answer
|
||||||
└ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
├ question_identifier string The question's ``identifier`` field
|
||||||
|
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||||
|
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
@@ -140,6 +142,10 @@ answers list of objects Answers to user
|
|||||||
|
|
||||||
The attribute ``checkins.list`` has been added.
|
The attribute ``checkins.list`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.14
|
||||||
|
|
||||||
|
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
|
||||||
|
|
||||||
|
|
||||||
Order endpoints
|
Order endpoints
|
||||||
---------------
|
---------------
|
||||||
@@ -222,7 +228,9 @@ Order endpoints
|
|||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
|
"question_identifier": "WY3TP9SL",
|
||||||
"answer": "Foo",
|
"answer": "Foo",
|
||||||
|
"option_idenfiters": [],
|
||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -330,7 +338,9 @@ Order endpoints
|
|||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
|
"question_identifier": "WY3TP9SL",
|
||||||
"answer": "Foo",
|
"answer": "Foo",
|
||||||
|
"option_idenfiters": [],
|
||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -649,7 +659,9 @@ Order position endpoints
|
|||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
|
"question_identifier": "WY3TP9SL",
|
||||||
"answer": "Foo",
|
"answer": "Foo",
|
||||||
|
"option_idenfiters": [],
|
||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -729,7 +741,9 @@ Order position endpoints
|
|||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
|
"question_identifier": "WY3TP9SL",
|
||||||
"answer": "Foo",
|
"answer": "Foo",
|
||||||
|
"option_idenfiters": [],
|
||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
233
doc/api/resources/question_options.rst
Normal file
233
doc/api/resources/question_options.rst
Normal 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.
|
||||||
@@ -31,12 +31,18 @@ type string The expected ty
|
|||||||
required boolean If ``True``, the question needs to be filled out.
|
required boolean If ``True``, the question needs to be filled out.
|
||||||
position integer An integer, used for sorting
|
position integer An integer, used for sorting
|
||||||
items list of integers List of item IDs this question is assigned to.
|
items list of integers List of item IDs this question is assigned to.
|
||||||
|
identifier string An arbitrary string that can be used for matching with
|
||||||
|
other sources.
|
||||||
ask_during_checkin boolean If ``True``, this question will not be asked while
|
ask_during_checkin boolean If ``True``, this question will not be asked while
|
||||||
buying the ticket, but will show up when redeeming
|
buying the ticket, but will show up when redeeming
|
||||||
the ticket instead.
|
the ticket instead.
|
||||||
options list of objects In case of question type ``C`` or ``M``, this lists the
|
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
|
├ 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
|
└ answer multi-lingual string The displayed value of this option
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
@@ -45,6 +51,11 @@ options list of objects In case of ques
|
|||||||
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
|
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
|
||||||
been added.
|
been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.14
|
||||||
|
|
||||||
|
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
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -80,18 +91,25 @@ Endpoints
|
|||||||
"required": false,
|
"required": false,
|
||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"identifier": "WY3TP9SL",
|
||||||
"ask_during_checkin": false,
|
"ask_during_checkin": false,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"identifier": "LVETRWVU",
|
||||||
|
"position": 0,
|
||||||
"answer": {"en": "S"}
|
"answer": {"en": "S"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
"identifier": "DFEMJWMJ",
|
||||||
|
"position": 1,
|
||||||
"answer": {"en": "M"}
|
"answer": {"en": "M"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
"identifier": "W9AH7RDE",
|
||||||
|
"position": 2,
|
||||||
"answer": {"en": "L"}
|
"answer": {"en": "L"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -134,19 +152,26 @@ Endpoints
|
|||||||
"type": "C",
|
"type": "C",
|
||||||
"required": false,
|
"required": false,
|
||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"ask_during_checkin": false,
|
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"identifier": "WY3TP9SL",
|
||||||
|
"ask_during_checkin": false,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"identifier": "LVETRWVU",
|
||||||
|
"position": 1,
|
||||||
"answer": {"en": "S"}
|
"answer": {"en": "S"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
"identifier": "DFEMJWMJ",
|
||||||
|
"position": 2,
|
||||||
"answer": {"en": "M"}
|
"answer": {"en": "M"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
"identifier": "W9AH7RDE",
|
||||||
|
"position": 3,
|
||||||
"answer": {"en": "L"}
|
"answer": {"en": "L"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -158,3 +183,179 @@ Endpoints
|
|||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
: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
|
.. sourcecode:: http
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 201 Created
|
||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
|
|||||||
42
doc/conf.py
42
doc/conf.py
@@ -31,6 +31,13 @@ import django
|
|||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.testutils.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.testutils.settings")
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import enchant
|
||||||
|
HAS_PYENCHANT = True
|
||||||
|
except:
|
||||||
|
HAS_PYENCHANT = False
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
@@ -45,8 +52,9 @@ extensions = [
|
|||||||
'sphinx.ext.coverage',
|
'sphinx.ext.coverage',
|
||||||
'sphinxcontrib.httpdomain',
|
'sphinxcontrib.httpdomain',
|
||||||
'sphinxcontrib.images',
|
'sphinxcontrib.images',
|
||||||
'sphinxcontrib.spelling',
|
|
||||||
]
|
]
|
||||||
|
if HAS_PYENCHANT:
|
||||||
|
extensions.append('sphinxcontrib.spelling')
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
@@ -292,21 +300,25 @@ images_config = {
|
|||||||
'default_image_width': '250px'
|
'default_image_width': '250px'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkcheck_ignore = [
|
||||||
|
r'http://localhost.*', r'.*yourdomain.*', r'https://en.wikipedia.org', 'https://pretix.eu/',
|
||||||
|
]
|
||||||
|
|
||||||
# -- Options for Spelling output ------------------------------------------
|
# -- Options for Spelling output ------------------------------------------
|
||||||
|
if HAS_PYENCHANT:
|
||||||
|
# String specifying the language, as understood by PyEnchant and enchant.
|
||||||
|
# Defaults to en_US for US English.
|
||||||
|
spelling_lang = 'en_US'
|
||||||
|
|
||||||
# String specifying the language, as understood by PyEnchant and enchant.
|
# String specifying a file containing a list of words known to be spelled
|
||||||
# Defaults to en_US for US English.
|
# correctly but that do not appear in the language dictionary selected by
|
||||||
spelling_lang = 'en_US'
|
# spelling_lang. The file should contain one word per line.
|
||||||
|
spelling_word_list_filename='spelling_wordlist.txt'
|
||||||
|
|
||||||
# String specifying a file containing a list of words known to be spelled
|
# Boolean controlling whether suggestions for misspelled words are printed.
|
||||||
# correctly but that do not appear in the language dictionary selected by
|
# Defaults to False.
|
||||||
# spelling_lang. The file should contain one word per line.
|
spelling_show_suggestions=True
|
||||||
spelling_word_list_filename='spelling_wordlist.txt'
|
|
||||||
|
|
||||||
# Boolean controlling whether suggestions for misspelled words are printed.
|
# List of filter classes to be added to the tokenizer that produces words to be checked.
|
||||||
# Defaults to False.
|
from checkin_filter import CheckinFilter
|
||||||
spelling_show_suggestions=True
|
spelling_filters=[CheckinFilter]
|
||||||
|
|
||||||
# List of filter classes to be added to the tokenizer that produces words to be checked.
|
|
||||||
from checkin_filter import CheckinFilter
|
|
||||||
spelling_filters=[CheckinFilter]
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Output registration
|
|||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
The invoice renderer API does not make a lot of usage from signals, however, it
|
The invoice renderer API does not make a lot of usage from signals, however, it
|
||||||
does use a signal to get a list of all available ticket outputs. Your plugin
|
does use a signal to get a list of all available invoice renderers. Your plugin
|
||||||
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
|
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
|
||||||
that we'll provide in this plugin::
|
that we'll provide in this plugin::
|
||||||
|
|
||||||
|
|||||||
@@ -142,5 +142,5 @@ your Django app label.
|
|||||||
.. _Django app: https://docs.djangoproject.com/en/1.7/ref/applications/
|
.. _Django app: https://docs.djangoproject.com/en/1.7/ref/applications/
|
||||||
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
|
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
|
||||||
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/
|
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/
|
||||||
.. _entry point: https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins
|
.. _entry point: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#locating-plugins
|
||||||
.. _cookiecutter: https://cookiecutter.readthedocs.io/en/latest/
|
.. _cookiecutter: https://cookiecutter.readthedocs.io/en/latest/
|
||||||
|
|||||||
@@ -77,6 +77,6 @@ Attribution
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4,
|
This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4,
|
||||||
available at http://contributor-covenant.org/version/1/4/
|
available at https://www.contributor-covenant.org/version/1/4/
|
||||||
|
|
||||||
.. _Contributor Covenant: http://contributor-covenant.org
|
.. _Contributor Covenant: https://www.contributor-covenant.org
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Coding style and quality
|
|||||||
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
|
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
|
||||||
|
|
||||||
|
|
||||||
.. _PEP 8: http://legacy.python.org/dev/peps/pep-0008/
|
.. _PEP 8: https://legacy.python.org/dev/peps/pep-0008/
|
||||||
.. _flake8: https://pypi.python.org/pypi/flake8
|
.. _flake8: https://pypi.python.org/pypi/flake8
|
||||||
.. _Django Coding Style: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/
|
.. _Django Coding Style: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/
|
||||||
.. _translation: https://docs.djangoproject.com/en/1.11/topics/i18n/translation/
|
.. _translation: https://docs.djangoproject.com/en/1.11/topics/i18n/translation/
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ Contents:
|
|||||||
settings
|
settings
|
||||||
background
|
background
|
||||||
email
|
email
|
||||||
|
permissions
|
||||||
logging
|
logging
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ Organizers and events
|
|||||||
.. autoclass:: pretix.base.models.Team
|
.. autoclass:: pretix.base.models.Team
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.models.TeamAPIToken
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.RequiredAction
|
.. autoclass:: pretix.base.models.RequiredAction
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|||||||
194
doc/development/implementation/permissions.rst
Normal file
194
doc/development/implementation/permissions.rst
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
Permissions
|
||||||
|
===========
|
||||||
|
|
||||||
|
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
|
||||||
|
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
|
||||||
|
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
|
||||||
|
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
|
||||||
|
assigned a set of permissions and connected to some or all of the events of the organizer.
|
||||||
|
|
||||||
|
A second way to access pretix is via the REST API, which allows authentication via tokens that are bound to a team,
|
||||||
|
but not to a user. You can read more at :class:`pretix.base.models.TeamAPIToken`. This page will show you how to
|
||||||
|
work with permissions in plugins and within the pretix code base.
|
||||||
|
|
||||||
|
Requiring permissions for a view
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
pretix provides a number of useful mixins and decorators that allow you to specify that a user needs a certain
|
||||||
|
permission level to access a view::
|
||||||
|
|
||||||
|
from pretix.control.permissions import (
|
||||||
|
OrganizerPermissionRequiredMixin, organizer_permission_required
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
|
||||||
|
permission = 'can_change_organizer_settings'
|
||||||
|
# Only users with the permission ``can_change_organizer_settings`` on
|
||||||
|
# this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
|
class MyOtherOrgaView(OrganizerPermissionRequiredMixin, View):
|
||||||
|
permission = None
|
||||||
|
# Only users with *any* permission on this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
|
@organizer_permission_required('can_change_organizer_settings')
|
||||||
|
def my_orga_view(request, organizer, **kwargs):
|
||||||
|
# Only users with the permission ``can_change_organizer_settings`` on
|
||||||
|
# this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
|
@organizer_permission_required()
|
||||||
|
def my_other_orga_view(request, organizer, **kwargs):
|
||||||
|
# Only users with *any* permission on this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
|
Of course, the same is available on event level::
|
||||||
|
|
||||||
|
from pretix.control.permissions import (
|
||||||
|
EventPermissionRequiredMixin, event_permission_required
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyEventView(EventPermissionRequiredMixin, View):
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
# Only users with the permission ``can_change_event_settings`` on
|
||||||
|
# this event can access this
|
||||||
|
|
||||||
|
|
||||||
|
class MyOtherEventView(EventPermissionRequiredMixin, View):
|
||||||
|
permission = None
|
||||||
|
# Only users with *any* permission on this event can access this
|
||||||
|
|
||||||
|
|
||||||
|
@event_permission_required('can_change_event_settings')
|
||||||
|
def my_event_view(request, organizer, **kwargs):
|
||||||
|
# Only users with the permission ``can_change_event_settings`` on
|
||||||
|
# this event can access this
|
||||||
|
|
||||||
|
|
||||||
|
@event_permission_required()
|
||||||
|
def my_other_event_view(request, organizer, **kwargs):
|
||||||
|
# Only users with *any* permission on this event can access this
|
||||||
|
|
||||||
|
You can also require that this view is only accessible by system administrators with an active "admin session"
|
||||||
|
(see below for what this means)::
|
||||||
|
|
||||||
|
from pretix.control.permissions import (
|
||||||
|
AdministratorPermissionRequiredMixin, administrator_permission_required
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyGlobalView(AdministratorPermissionRequiredMixin, View):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
|
@administrator_permission_required
|
||||||
|
def my_global_view(request, organizer, **kwargs):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
In rare cases it might also be useful to expose a feature only to people who have a staff account but do not
|
||||||
|
necessarily have an active admin session::
|
||||||
|
|
||||||
|
from pretix.control.permissions import (
|
||||||
|
StaffMemberRequiredMixin, staff_member_required
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyGlobalView(StaffMemberRequiredMixin, View):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def my_global_view(request, organizer, **kwargs):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Requiring permissions in the REST API
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
When creating your own ``viewset`` using Django REST framework, you just need to set the ``permission`` attribute
|
||||||
|
and pretix will check it automatically for you::
|
||||||
|
|
||||||
|
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
Checking permission in code
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
If you need to work with permissions manually, there are a couple of useful helper methods on the :class:`pretix.base.models.Event`,
|
||||||
|
:class:`pretix.base.models.User` and :class:`pretix.base.models.TeamAPIToken` classes. Here's a quick overview.
|
||||||
|
|
||||||
|
Return all users that are in any team that is connected to this event::
|
||||||
|
|
||||||
|
>>> event.get_users_with_any_permission()
|
||||||
|
<QuerySet: …>
|
||||||
|
|
||||||
|
Return all users that are in a team with a specific permission for this event::
|
||||||
|
|
||||||
|
>>> event.get_users_with_permission('can_change_event_settings')
|
||||||
|
<QuerySet: …>
|
||||||
|
|
||||||
|
Determine if a user has a certain permission for a specific event::
|
||||||
|
|
||||||
|
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
|
||||||
|
True
|
||||||
|
|
||||||
|
Determine if a user has any permission for a specific event::
|
||||||
|
|
||||||
|
>>> user.has_event_permission(organizer, event, request=request)
|
||||||
|
True
|
||||||
|
|
||||||
|
In the two previous commands, the ``request`` argument is optional, but required to support staff sessions (see below).
|
||||||
|
|
||||||
|
The same method exists for organizer-level permissions::
|
||||||
|
|
||||||
|
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
|
||||||
|
True
|
||||||
|
|
||||||
|
Sometimes, it might be more useful to get the set of permissions at once::
|
||||||
|
|
||||||
|
>>> user.get_event_permission_set(organizer, event)
|
||||||
|
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
|
||||||
|
|
||||||
|
>>> user.get_organizer_permission_set(organizer, event)
|
||||||
|
{'can_change_organizer_settings', 'can_create_events'}
|
||||||
|
|
||||||
|
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
|
||||||
|
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
|
||||||
|
|
||||||
|
{% if "can_change_orders" in request.eventpermset %}
|
||||||
|
…
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
You can also do the reverse to get any events a user has access to::
|
||||||
|
|
||||||
|
>>> user.get_events_with_permission('can_change_event_settings', request=request)
|
||||||
|
<QuerySet: …>
|
||||||
|
|
||||||
|
>>> user.get_events_with_any_permission(request=request)
|
||||||
|
<QuerySet: …>
|
||||||
|
|
||||||
|
Most of these methods work identically on :class:`pretix.base.models.TeamAPIToken`.
|
||||||
|
|
||||||
|
Staff sessions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. versionchanged:: 1.14
|
||||||
|
|
||||||
|
In 1.14, the ``User.is_superuser`` attribute has been deprecated and statically set to return ``False``. Staff
|
||||||
|
sessions have been newly introduced.
|
||||||
|
|
||||||
|
System administrators of a pretix instance are identified by the ``is_staff`` attribute on the user model. By default,
|
||||||
|
the regular permission rules apply for users with ``is_staff = True``. The only difference is that such users can
|
||||||
|
temporarily turn on "staff mode" via a button in the user interface that grants them **all permissions** as long as
|
||||||
|
staff mode is active. You can check if a user is in staff mode using their session key:
|
||||||
|
|
||||||
|
>>> user.has_active_staff_session(request.session.session_key)
|
||||||
|
False
|
||||||
|
|
||||||
|
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
|
||||||
|
the user is able to also save a message to comment on what they did in their administrative session. This feature is
|
||||||
|
intended to help compliance with data protection rules as imposed e.g. by GDPR.
|
||||||
@@ -8,5 +8,6 @@ Developer documentation
|
|||||||
setup
|
setup
|
||||||
contribution/index
|
contribution/index
|
||||||
implementation/index
|
implementation/index
|
||||||
|
translation/index
|
||||||
api/index
|
api/index
|
||||||
structure
|
structure
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ and update the ``*.po`` files accordingly::
|
|||||||
|
|
||||||
make localegen
|
make localegen
|
||||||
|
|
||||||
|
However, most of the time you don't need to care about this. Just create your pull request
|
||||||
|
with functionality and English strings only, and we'll push the new translation strings
|
||||||
|
to our translation platform after the merge.
|
||||||
|
|
||||||
To actually see pretix in your language, you have to compile the ``*.po`` files to their
|
To actually see pretix in your language, you have to compile the ``*.po`` files to their
|
||||||
optimized binary ``*.mo`` counterparts::
|
optimized binary ``*.mo`` counterparts::
|
||||||
|
|
||||||
|
|||||||
BIN
doc/development/translation/img/weblate1.png
Normal file
BIN
doc/development/translation/img/weblate1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
doc/development/translation/img/weblate2.png
Normal file
BIN
doc/development/translation/img/weblate2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
doc/development/translation/img/weblate3.png
Normal file
BIN
doc/development/translation/img/weblate3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
doc/development/translation/img/weblate4.png
Normal file
BIN
doc/development/translation/img/weblate4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
BIN
doc/development/translation/img/weblate5.png
Normal file
BIN
doc/development/translation/img/weblate5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
doc/development/translation/img/weblate6.png
Normal file
BIN
doc/development/translation/img/weblate6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
88
doc/development/translation/index.rst
Normal file
88
doc/development/translation/index.rst
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
Translating pretix
|
||||||
|
==================
|
||||||
|
|
||||||
|
pretix has been designed for multi-language capabilities from its start. Organizers can enter their event information
|
||||||
|
in multiple languages at the same time. However, the software interface of pretix also needs to be translated for
|
||||||
|
this to be useful.
|
||||||
|
|
||||||
|
Since we (the developers of pretix) only speak a very limited number of languages, we need help from the community
|
||||||
|
to achieve this goal. To make translating pretix easy not only for software developers, we set up a translation
|
||||||
|
platform at `translate.pretix.eu`_.
|
||||||
|
|
||||||
|
Official and inofficial languages
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
In the pretix project, there are three types of languages:
|
||||||
|
|
||||||
|
Official languages
|
||||||
|
are translated and maintained by the core team behind pretix or as part of long-term partnerships. We are
|
||||||
|
committed to keeping these translations up-to-date with new features or changes in pretix and try to offer
|
||||||
|
support in this language.
|
||||||
|
|
||||||
|
Inofficial languages
|
||||||
|
are contributed and maintained by the Community. We ship them with pretix so you can use them, but we can not
|
||||||
|
guarantee that new or changed features in pretix will be translated in time.
|
||||||
|
|
||||||
|
Incubating languages
|
||||||
|
are currently in the process of being translated. They can not yet be selected in pretix by end users on
|
||||||
|
production installations and are only available in development mode for testing.
|
||||||
|
|
||||||
|
Please contact translate@pretix.eu if you think an incubated language should be promoted to an inofficial one or if
|
||||||
|
you are interested in a partnership to make your language official.
|
||||||
|
|
||||||
|
The current translation status of various languages is:
|
||||||
|
|
||||||
|
.. image:: https://translate.pretix.eu/widgets/pretix/-/multi-blue.svg
|
||||||
|
:target: https://translate.pretix.eu/engage/pretix/?utm_source=widget
|
||||||
|
|
||||||
|
|
||||||
|
Using our translation platform
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
If you visit `translate.pretix.eu`_ for the first time, it admittedly looks pretty bare.
|
||||||
|
|
||||||
|
.. image:: img/weblate1.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
It gets better if you create an account, which you will need to contribute translations. Click on "Register" in the
|
||||||
|
top-right corner to get started:
|
||||||
|
|
||||||
|
.. image:: img/weblate2.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
You can either create an account or choose to log in with your GitHub account, whichever you like more.
|
||||||
|
After creating and activating your account, we recommend that you change your profile and select which languages you
|
||||||
|
can translate to and which languages you understand. You can find your profile settings by clicking on your name in
|
||||||
|
the top-right corner.
|
||||||
|
|
||||||
|
.. image:: img/weblate3.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
Going back to the dashboard by clicking on the logo in the top-left corner, you can select between different lists
|
||||||
|
of translation projects. You can either filter by projects that already have a translation in your language, or you
|
||||||
|
go to the `pretix project page`_ where you can select specific components.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you want to translate pretix to a new language that is not yet listed here, you are very welcome to do so!
|
||||||
|
While you technically can add the language to the portal yourself, we ask you to drop us a short mail to
|
||||||
|
translate@pretix.eu so we can add it to all components at once and also make it selectable in pretix itself.
|
||||||
|
|
||||||
|
.. image:: img/weblate4.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
Once you selected a component of a language, you can start going through strings to translate. You can start of by
|
||||||
|
clicking the "Strings needing action" line in this view:
|
||||||
|
|
||||||
|
.. image:: img/weblate5.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
In the translate view, you can input your translation for a given source string. If you're unsure about your
|
||||||
|
translation, you can also just "Suggest" it or mark it as "Needs editing". If you have no idea, just "Skip". If you
|
||||||
|
scroll down, there is also a "Comments" section to discuss any questions with fellow translators or us developers.
|
||||||
|
|
||||||
|
.. image:: img/weblate6.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
.. _translate.pretix.eu: https://translate.pretix.eu
|
||||||
|
.. _pretix project page: https://translate.pretix.eu/projects/pretix/
|
||||||
0
doc/doc_warnings
Normal file
0
doc/doc_warnings
Normal file
@@ -4,4 +4,5 @@ sphinx-rtd-theme
|
|||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-images
|
sphinxcontrib-images
|
||||||
sphinxcontrib-spelling
|
sphinxcontrib-spelling
|
||||||
pyenchant
|
# See https://github.com/rfk/pyenchant/pull/130
|
||||||
|
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
addon
|
addon
|
||||||
addons
|
addons
|
||||||
api
|
api
|
||||||
|
auditability
|
||||||
auth
|
auth
|
||||||
autobuild
|
autobuild
|
||||||
backend
|
backend
|
||||||
@@ -33,8 +34,10 @@ gettext
|
|||||||
gunicorn
|
gunicorn
|
||||||
hardcoded
|
hardcoded
|
||||||
hostname
|
hostname
|
||||||
|
inofficial
|
||||||
invalidations
|
invalidations
|
||||||
iterable
|
iterable
|
||||||
|
Jimdo
|
||||||
libsass
|
libsass
|
||||||
linters
|
linters
|
||||||
memcached
|
memcached
|
||||||
@@ -77,6 +80,7 @@ renderer
|
|||||||
renderers
|
renderers
|
||||||
reportlab
|
reportlab
|
||||||
screenshot
|
screenshot
|
||||||
|
selectable
|
||||||
serializers
|
serializers
|
||||||
serializers
|
serializers
|
||||||
sexualized
|
sexualized
|
||||||
|
|||||||
@@ -126,4 +126,29 @@ With the checkbox "Use custom SMTP server" you can turn using your SMTP server o
|
|||||||
button "Save and test custom SMTP connection", you can test if the connection and authentication to your SMTP server
|
button "Save and test custom SMTP connection", you can test if the connection and authentication to your SMTP server
|
||||||
succeeds, even before turning that checkbox on.
|
succeeds, even before turning that checkbox on.
|
||||||
|
|
||||||
|
Spam issues
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If you use an email address of your own domain as a sender address and do not use a custom SMTP server, it is very
|
||||||
|
likely that at least some of your emails will go to the spam folders of their recipients. We **strongly recommend**
|
||||||
|
to use your organization's SMTP server in this case, making your email really come from your organization. If you don't
|
||||||
|
want that or cannot do that, you should add the pretix application server to your SPF record.
|
||||||
|
|
||||||
|
If you are using our hosted service at pretix.eu, you can add the following to your SPF record::
|
||||||
|
|
||||||
|
include:_spf.pretix.eu
|
||||||
|
|
||||||
|
A complete record could look like this::
|
||||||
|
|
||||||
|
v=spf1 a mx include:_spf.pretix.eu ~all
|
||||||
|
|
||||||
|
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
|
||||||
|
record for the subdomain ``pretix._domainkey`` with the following contents::
|
||||||
|
|
||||||
|
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
|
||||||
|
|
||||||
|
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
||||||
|
|
||||||
|
|
||||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||||
|
.. _SPF specification: http://www.openspf.org/SPF_Record_Syntax
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ The second snippet should be embedded at the position where the widget should sh
|
|||||||
You can of course embed multiple widgets of multiple events on your page. In this case, please add the first
|
You can of course embed multiple widgets of multiple events on your page. In this case, please add the first
|
||||||
snippet only *once* and the second snippets once *for each event*.
|
snippet only *once* and the second snippets once *for each event*.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Some website builders like Jimdo have trouble with our custom HTML tag. In that case, you can use
|
||||||
|
``<div class="pretix-widget-compat" …></div>`` instead of ``<pretix-widget …></pretix-widget>`` starting with
|
||||||
|
pretix 1.14.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,25 @@ If you created a product and it doesn't show up, please follow the following ste
|
|||||||
quota that is assigned to the series date that you access the shop for.
|
quota that is assigned to the series date that you access the shop for.
|
||||||
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
|
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
|
||||||
your event.
|
your event.
|
||||||
|
|
||||||
|
How can I revert a check-in?
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Neither our apps nor our web interface can currently undo the check-in of a tickets. We know that this is
|
||||||
|
inconvenient for some of you, but we have a good reason for it:
|
||||||
|
|
||||||
|
Our Desktop and Android apps both support an asynchronous mode in which they can scan tickets while staying
|
||||||
|
independent of their internet connection. When scanning with multiple devices, it can of course happen that two
|
||||||
|
devices scan the same ticket without knowing of the other scan. As soon as one of the devices regains connectivity, it
|
||||||
|
will upload its activity and the server marks the ticket as checked in -- regardless of the order in which the two
|
||||||
|
scans were made and uploaded (which could be two different orders).
|
||||||
|
|
||||||
|
If we'd provide a "check out" feature, it would not only be used to fix an accidental scan, but scan at entry and
|
||||||
|
exit to count the current number of people inside etc. In this case, the order of operations matters very much for them
|
||||||
|
to make sense and provide useful results. This makes implementing an asynchronous mode much more complicated.
|
||||||
|
|
||||||
|
In this trade off, we chose offline-capabilities over the check out feature. We plan on solving this problem in the
|
||||||
|
future, but we're not there yet.
|
||||||
|
|
||||||
|
If you're just *testing* the check-in capabilities and want to clean out everything for the real process, you can just
|
||||||
|
delete and re-create the check-in list.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _user-teams:
|
||||||
|
|
||||||
Teams
|
Teams
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
General settings
|
General settings
|
||||||
================
|
================
|
||||||
|
|
||||||
At "Settings" → "Pages", you can configure every aspect related to the payments you want to accept. The upper part
|
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The upper part
|
||||||
of the page shows a number of general settings that affect all payment methods:
|
of the page shows a number of general settings that affect all payment methods:
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/event/settings_payment.png
|
.. thumbnail:: ../../screens/event/settings_payment.png
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
Stripe
|
Stripe
|
||||||
======
|
======
|
||||||
|
|
||||||
|
.. note:: If you use the Hosted version of pretix at pretix.eu, you do not need to copy API keys and create webhooks
|
||||||
|
any more. Instead, you can just click "Connect with Stripe" in pretix and everything will connect
|
||||||
|
automatically.
|
||||||
|
|
||||||
To integrate Stripe with pretix, you first need to have an active Stripe merchant account. If you do not already have a
|
To integrate Stripe with pretix, you first need to have an active Stripe merchant account. If you do not already have a
|
||||||
Stripe account, you can create one on `stripe.com`_. Then, click on "API" in the left navigation of the Stripe
|
Stripe account, you can create one on `stripe.com`_. Then, click on "API" in the left navigation of the Stripe
|
||||||
Dashboard. As you can see in the following screenshot, you will be presented with two sets of API keys, one for test
|
Dashboard. As you can see in the following screenshot, you will be presented with two sets of API keys, one for test
|
||||||
|
|||||||
37
src/.update-locales
Executable file
37
src/.update-locales
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
COMPONENTS="pretix/pretix pretix/pretix-js"
|
||||||
|
DIR=pretix/locale
|
||||||
|
# Renerates .po files used for translating the plugin
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# Lock Weblate
|
||||||
|
for c in $COMPONENTS; do
|
||||||
|
wlc lock $c;
|
||||||
|
done
|
||||||
|
|
||||||
|
# Push changes from Weblate to GitHub
|
||||||
|
for c in $COMPONENTS; do
|
||||||
|
wlc commit $c;
|
||||||
|
done
|
||||||
|
|
||||||
|
# Pull changes from GitHub
|
||||||
|
git pull --rebase
|
||||||
|
|
||||||
|
# Update po files itself
|
||||||
|
make localegen
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git add $DIR/*/*/*.po
|
||||||
|
git add $DIR/*.pot
|
||||||
|
|
||||||
|
git commit -s -m "Update po files
|
||||||
|
[CI skip]"
|
||||||
|
|
||||||
|
# Push changes
|
||||||
|
git push
|
||||||
|
|
||||||
|
# Unlock Weblate
|
||||||
|
for c in $COMPONENTS; do
|
||||||
|
wlc unlock $c;
|
||||||
|
done
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
all: localecompile staticfiles
|
all: localecompile staticfiles
|
||||||
production: localecompile staticfiles compress
|
production: localecompile staticfiles compress
|
||||||
|
LNGS:=`find pretix/locale/ -mindepth 1 -maxdepth 1 -type d -printf "-l %f "`
|
||||||
|
|
||||||
localecompile:
|
localecompile:
|
||||||
./manage.py compilemessages
|
./manage.py compilemessages
|
||||||
|
|
||||||
localegen:
|
localegen:
|
||||||
./manage.py makemessages --all --ignore "pretix/helpers/*"
|
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
|
||||||
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "build/*"
|
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "build/*" $(LNGS)
|
||||||
|
|
||||||
staticfiles: jsi18n
|
staticfiles: jsi18n
|
||||||
./manage.py collectstatic --noinput
|
./manage.py collectstatic --noinput
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.13.0"
|
__version__ = "1.14.0"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
@@ -57,18 +56,3 @@ class EventPermission(BasePermission):
|
|||||||
if required_permission and required_permission not in request.orgapermset:
|
if required_permission and required_permission not in request.orgapermset:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def permission_required(required_permission):
|
|
||||||
def decorator(function):
|
|
||||||
def wrapper(self, request, *args, **kw):
|
|
||||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
|
||||||
if required_permission and required_permission not in request.eventpermset:
|
|
||||||
raise PermissionDenied('You do not have permission to perform this operation.')
|
|
||||||
elif 'organizer' in request.resolver_match.kwargs:
|
|
||||||
if required_permission and required_permission not in request.orgapermset:
|
|
||||||
raise PermissionDenied('You do not have permission to perform this operation.')
|
|
||||||
|
|
||||||
return function(self, request, *args, **kw)
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
if self.instance and ('addons' in data or 'variations' in data):
|
||||||
|
raise ValidationError(_('Updating add-ons or variations via PATCH/PUT is not supported. Please use the '
|
||||||
|
'dedicated nested endpoint.'))
|
||||||
|
|
||||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||||
@@ -101,17 +104,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
Item.clean_tax_rule(value, self.context['event'])
|
Item.clean_tax_rule(value, self.context['event'])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_variations(self, value):
|
|
||||||
if self.instance is not None:
|
|
||||||
raise ValidationError(_('Updating variations via PATCH/PUT is not supported. Please use the dedicated'
|
|
||||||
' nested endpoint.'))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_addons(self, value):
|
def validate_addons(self, value):
|
||||||
if self.instance is not None:
|
if not self.instance:
|
||||||
raise ValidationError(_('Updating add-ons via PATCH/PUT is not supported. Please use the dedicated'
|
|
||||||
' nested endpoint.'))
|
|
||||||
else:
|
|
||||||
for addon_data in value:
|
for addon_data in value:
|
||||||
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
|
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
|
||||||
ItemAddOn.clean_min_count(addon_data['min_count'])
|
ItemAddOn.clean_min_count(addon_data['min_count'])
|
||||||
@@ -138,20 +132,72 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'name', 'description', 'position', 'is_addon')
|
fields = ('id', 'name', 'description', 'position', 'is_addon')
|
||||||
|
|
||||||
|
|
||||||
class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
identifier = serializers.CharField(allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = QuestionOption
|
model = QuestionOption
|
||||||
fields = ('id', '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):
|
class QuestionSerializer(I18nAwareModelSerializer):
|
||||||
options = InlineQuestionOptionSerializer(many=True)
|
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||||
|
identifier = serializers.CharField(allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||||
'ask_during_checkin')
|
'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):
|
class QuotaSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -29,10 +29,23 @@ class InvoiceAdddressSerializer(I18nAwareModelSerializer):
|
|||||||
'vat_id_validated', 'internal_reference')
|
'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):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
|
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||||
|
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = QuestionAnswer
|
model = QuestionAnswer
|
||||||
fields = ('question', 'answer', 'options')
|
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
|
||||||
|
|
||||||
|
|
||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
|||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||||
|
|
||||||
|
question_router = routers.DefaultRouter()
|
||||||
|
question_router.register(r'options', item.QuestionOptionViewSet)
|
||||||
|
|
||||||
item_router = routers.DefaultRouter()
|
item_router = routers.DefaultRouter()
|
||||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||||
item_router.register(r'addons', item.ItemAddOnViewSet)
|
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>[^/]+)/', include(orga_router.urls)),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_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>[^/]+)/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>[^/]+)/',
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||||
include(checkinlist_router.urls)),
|
include(checkinlist_router.urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
|
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
|
||||||
ItemVariationSerializer, QuestionSerializer, QuotaSerializer,
|
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||||
|
QuotaSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
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.base.models.organizer import TeamAPIToken
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
@@ -201,7 +203,7 @@ class ItemCategoryFilter(FilterSet):
|
|||||||
fields = ['is_addon']
|
fields = ['is_addon']
|
||||||
|
|
||||||
|
|
||||||
class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
class ItemCategoryViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ItemCategorySerializer
|
serializer_class = ItemCategorySerializer
|
||||||
queryset = ItemCategory.objects.none()
|
queryset = ItemCategory.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
@@ -209,12 +211,47 @@ class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.categories.all()
|
return self.request.event.categories.all()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(event=self.request.event)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.category.added',
|
||||||
|
user=self.request.user,
|
||||||
|
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
|
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.category.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):
|
||||||
|
for item in instance.items.all():
|
||||||
|
item.category = None
|
||||||
|
item.save()
|
||||||
|
instance.log_action(
|
||||||
|
'pretix.event.category.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||||
|
)
|
||||||
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = QuestionSerializer
|
serializer_class = QuestionSerializer
|
||||||
queryset = Question.objects.none()
|
queryset = Question.objects.none()
|
||||||
filter_backends = (OrderingFilter,)
|
filter_backends = (OrderingFilter,)
|
||||||
@@ -225,6 +262,85 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.questions.prefetch_related('options').all()
|
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 QuotaFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.orders.prefetch_related(
|
return self.request.event.orders.prefetch_related(
|
||||||
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
||||||
'fees'
|
'positions__answers__question', 'fees'
|
||||||
).select_related(
|
).select_related(
|
||||||
'invoice_address'
|
'invoice_address'
|
||||||
)
|
)
|
||||||
@@ -234,7 +234,7 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
|
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
|
||||||
'checkins', 'answers', 'answers__options'
|
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer'
|
'item', 'order', 'order__event', 'order__event__organizer'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated():
|
||||||
if self.request.user.is_superuser:
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||||
return Organizer.objects.all()
|
return Organizer.objects.all()
|
||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class PretixBaseConfig(AppConfig):
|
|||||||
from . import exporters # NOQA
|
from . import exporters # NOQA
|
||||||
from . import invoice # NOQA
|
from . import invoice # NOQA
|
||||||
from . import notifications # NOQA
|
from . import notifications # NOQA
|
||||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .celery_app import app as celery_app # NOQA
|
from .celery_app import app as celery_app # NOQA
|
||||||
|
|||||||
@@ -69,4 +69,5 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1])
|
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1])
|
||||||
return fname
|
# TODO: make sure pub is always correct
|
||||||
|
return 'pub/' + fname
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
|
from pretix.control.forms import SingleLanguageWidget
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
@@ -47,6 +48,9 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
'timezone',
|
'timezone',
|
||||||
'email'
|
'email'
|
||||||
]
|
]
|
||||||
|
widgets = {
|
||||||
|
'locale': SingleLanguageWidget
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
|
|||||||
59
src/pretix/base/migrations/0085_auto_20180312_1119.py
Normal file
59
src/pretix/base/migrations/0085_auto_20180312_1119.py
Normal 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)
|
||||||
|
]
|
||||||
43
src/pretix/base/migrations/0086_auto_20180320_1219.py
Normal file
43
src/pretix/base/migrations/0086_auto_20180320_1219.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-03-20 12:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.invoices
|
||||||
|
import pretix.base.models.orders
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0085_auto_20180312_1119'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cachedcombinedticket',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cachedticket',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.orders.cachedticket_name),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.invoices.invoice_filename),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='question',
|
||||||
|
name='identifier',
|
||||||
|
field=models.CharField(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.', max_length=190, verbose_name='Internal identifier'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='questionanswer',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.orders.answerfile_name),
|
||||||
|
),
|
||||||
|
]
|
||||||
64
src/pretix/base/migrations/0087_auto_20180317_1952.py
Normal file
64
src/pretix/base/migrations/0087_auto_20180317_1952.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.10 on 2018-03-17 19:52
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_is_staff(apps, schema_editor):
|
||||||
|
User = apps.get_model('pretixbase', 'User')
|
||||||
|
User.objects.filter(is_superuser=True).update(is_staff=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0086_auto_20180320_1219'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
set_is_staff,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='is_superuser',
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StaffSession',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date_start', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('date_end', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('session_key', models.CharField(max_length=255)),
|
||||||
|
('comment', models.TextField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StaffSessionAuditLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('url', models.CharField(max_length=255)),
|
||||||
|
('session', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='logs', to='pretixbase.StaffSession')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='staffsession',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(default=None, on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='staffsessionauditlog',
|
||||||
|
name='impersonating',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='staffsessionauditlog',
|
||||||
|
name='method',
|
||||||
|
field=models.CharField(default='GET', max_length=255),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
30
src/pretix/base/migrations/0088_auto_20180328_1217.py
Normal file
30
src/pretix/base/migrations/0088_auto_20180328_1217.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-03-28 12:17
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.items
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0087_auto_20180317_1952'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='staffsession',
|
||||||
|
options={'ordering': ('date_start',)},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='staffsessionauditlog',
|
||||||
|
options={'ordering': ('datetime',)},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='item',
|
||||||
|
name='picture',
|
||||||
|
field=models.ImageField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.items.itempicture_upload_to, verbose_name='Product picture'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -19,7 +19,9 @@ from .orders import (
|
|||||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||||
generate_secret,
|
generate_secret,
|
||||||
)
|
)
|
||||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
from .organizer import (
|
||||||
|
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||||
|
)
|
||||||
from .tax import TaxRule
|
from .tax import TaxRule
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
from .waitinglist import WaitingListEntry
|
from .waitinglist import WaitingListEntry
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Union
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
@@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
|
|
||||||
@@ -36,7 +37,6 @@ class UserManager(BaseUserManager):
|
|||||||
raise Exception("You must provide a password")
|
raise Exception("You must provide a password")
|
||||||
user = self.model(email=email)
|
user = self.model(email=email)
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.is_superuser = True
|
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
@@ -46,6 +46,11 @@ def generate_notifications_token():
|
|||||||
return get_random_string(length=32)
|
return get_random_string(length=32)
|
||||||
|
|
||||||
|
|
||||||
|
class SuperuserPermissionSet:
|
||||||
|
def __contains__(self, item):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||||
"""
|
"""
|
||||||
This is the user model used by pretix for authentication.
|
This is the user model used by pretix for authentication.
|
||||||
@@ -114,6 +119,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_superuser(self):
|
||||||
|
return False
|
||||||
|
|
||||||
def get_short_name(self) -> str:
|
def get_short_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the first of the following user properties that is found to exist:
|
Returns the first of the following user properties that is found to exist:
|
||||||
@@ -194,40 +203,36 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
))
|
))
|
||||||
return self._teamcache['e{}'.format(event.pk)]
|
return self._teamcache['e{}'.format(event.pk)]
|
||||||
|
|
||||||
class SuperuserPermissionSet:
|
def get_event_permission_set(self, organizer, event) -> set:
|
||||||
def __contains__(self, item):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
|
|
||||||
"""
|
"""
|
||||||
Gets a set of permissions (as strings) that a user holds for a particular event
|
Gets a set of permissions (as strings) that a user holds for a particular event
|
||||||
|
|
||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
:param event: The event to check
|
||||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
:return: set
|
||||||
a in b always returns true).
|
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
|
||||||
return self.SuperuserPermissionSet()
|
|
||||||
|
|
||||||
teams = self._get_teams_for_event(organizer, event)
|
teams = self._get_teams_for_event(organizer, event)
|
||||||
return set.union(*[t.permission_set() for t in teams])
|
sets = [t.permission_set() for t in teams]
|
||||||
|
if sets:
|
||||||
|
return set.union(*sets)
|
||||||
|
else:
|
||||||
|
return set()
|
||||||
|
|
||||||
def get_organizer_permission_set(self, organizer) -> Union[set, SuperuserPermissionSet]:
|
def get_organizer_permission_set(self, organizer) -> set:
|
||||||
"""
|
"""
|
||||||
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
||||||
|
|
||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
:return: set
|
||||||
a in b always returns true).
|
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
|
||||||
return self.SuperuserPermissionSet()
|
|
||||||
|
|
||||||
teams = self._get_teams_for_organizer(organizer)
|
teams = self._get_teams_for_organizer(organizer)
|
||||||
return set.union(*[t.permission_set() for t in teams])
|
sets = [t.permission_set() for t in teams]
|
||||||
|
if sets:
|
||||||
|
return set.union(*sets)
|
||||||
|
else:
|
||||||
|
return set()
|
||||||
|
|
||||||
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
|
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||||
to the event ``event``.
|
to the event ``event``.
|
||||||
@@ -235,9 +240,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
:param event: The event to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
return True
|
return True
|
||||||
teams = self._get_teams_for_event(organizer, event)
|
teams = self._get_teams_for_event(organizer, event)
|
||||||
if teams:
|
if teams:
|
||||||
@@ -246,16 +252,17 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_organizer_permission(self, organizer, perm_name=None):
|
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||||
"""
|
"""
|
||||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||||
to the organizer ``organizer``.
|
to the organizer ``organizer``.
|
||||||
|
|
||||||
:param organizer: The organizer to check
|
:param organizer: The organizer to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
return True
|
return True
|
||||||
teams = self._get_teams_for_organizer(organizer)
|
teams = self._get_teams_for_organizer(organizer)
|
||||||
if teams:
|
if teams:
|
||||||
@@ -263,15 +270,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_events_with_any_permission(self):
|
def get_events_with_any_permission(self, request=None):
|
||||||
"""
|
"""
|
||||||
Returns a queryset of events the user has any permissions to.
|
Returns a queryset of events the user has any permissions to.
|
||||||
|
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
:return: Iterable of Events
|
:return: Iterable of Events
|
||||||
"""
|
"""
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if self.is_superuser:
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
return Event.objects.all()
|
return Event.objects.all()
|
||||||
|
|
||||||
return Event.objects.filter(
|
return Event.objects.filter(
|
||||||
@@ -279,15 +287,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_events_with_permission(self, permission):
|
def get_events_with_permission(self, permission, request=None):
|
||||||
"""
|
"""
|
||||||
Returns a queryset of events the user has a specific permissions to.
|
Returns a queryset of events the user has a specific permissions to.
|
||||||
|
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
:return: Iterable of Events
|
:return: Iterable of Events
|
||||||
"""
|
"""
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if self.is_superuser:
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
return Event.objects.all()
|
return Event.objects.all()
|
||||||
|
|
||||||
kwargs = {permission: True}
|
kwargs = {permission: True}
|
||||||
@@ -297,6 +306,56 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def has_active_staff_session(self, session_key=None):
|
||||||
|
"""
|
||||||
|
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
||||||
|
with the given session key.
|
||||||
|
"""
|
||||||
|
return self.get_active_staff_session(session_key) is not None
|
||||||
|
|
||||||
|
def get_active_staff_session(self, session_key=None):
|
||||||
|
if not self.is_staff:
|
||||||
|
return None
|
||||||
|
if not hasattr(self, '_staff_session_cache'):
|
||||||
|
self._staff_session_cache = {}
|
||||||
|
if session_key not in self._staff_session_cache:
|
||||||
|
qs = StaffSession.objects.filter(
|
||||||
|
user=self, date_end__isnull=True
|
||||||
|
)
|
||||||
|
if session_key:
|
||||||
|
qs = qs.filter(session_key=session_key)
|
||||||
|
sess = qs.first()
|
||||||
|
if sess:
|
||||||
|
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||||
|
sess.date_end = now()
|
||||||
|
sess.save()
|
||||||
|
sess = None
|
||||||
|
|
||||||
|
self._staff_session_cache[session_key] = sess
|
||||||
|
return self._staff_session_cache[session_key]
|
||||||
|
|
||||||
|
|
||||||
|
class StaffSession(models.Model):
|
||||||
|
user = models.ForeignKey('User')
|
||||||
|
date_start = models.DateTimeField(auto_now_add=True)
|
||||||
|
date_end = models.DateTimeField(null=True, blank=True)
|
||||||
|
session_key = models.CharField(max_length=255)
|
||||||
|
comment = models.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('date_start',)
|
||||||
|
|
||||||
|
|
||||||
|
class StaffSessionAuditLog(models.Model):
|
||||||
|
session = models.ForeignKey('StaffSession', related_name='logs')
|
||||||
|
datetime = models.DateTimeField(auto_now_add=True)
|
||||||
|
url = models.CharField(max_length=255)
|
||||||
|
method = models.CharField(max_length=255)
|
||||||
|
impersonating = models.ForeignKey('User', null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('datetime',)
|
||||||
|
|
||||||
|
|
||||||
class U2FDevice(Device):
|
class U2FDevice(Device):
|
||||||
json_data = models.TextField()
|
json_data = models.TextField()
|
||||||
|
|||||||
@@ -437,7 +437,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if s.value.startswith('file://'):
|
if s.value.startswith('file://'):
|
||||||
fi = default_storage.open(s.value[7:], 'rb')
|
fi = default_storage.open(s.value[7:], 'rb')
|
||||||
nonce = get_random_string(length=8)
|
nonce = get_random_string(length=8)
|
||||||
fname = '%s/%s/%s.%s.%s' % (
|
# TODO: make sure pub is always correct
|
||||||
|
fname = 'pub/%s/%s/%s.%s.%s' % (
|
||||||
self.organizer.slug, self.slug, s.key, nonce, s.value.split('.')[-1]
|
self.organizer.slug, self.slug, s.key, nonce, s.value.split('.')[-1]
|
||||||
)
|
)
|
||||||
newname = default_storage.save(fname, fi)
|
newname = default_storage.save(fname, fi)
|
||||||
@@ -553,9 +554,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
return User.objects.annotate(twp=Exists(team_with_perm)).filter(
|
return User.objects.annotate(twp=Exists(team_with_perm)).filter(twp=True)
|
||||||
Q(is_superuser=True) | Q(twp=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def allow_delete(self):
|
def allow_delete(self):
|
||||||
return not self.orders.exists() and not self.invoices.exists()
|
return not self.orders.exists() and not self.invoices.exists()
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class Invoice(models.Model):
|
|||||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
||||||
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
||||||
|
|
||||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||||
internal_reference = models.TextField(blank=True)
|
internal_reference = models.TextField(blank=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Func, Q, Sum
|
from django.db.models import F, Func, Q, Sum
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import is_naive, make_aware, now
|
from django.utils.timezone import is_naive, make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
@@ -85,7 +86,7 @@ class ItemCategory(LoggedModel):
|
|||||||
|
|
||||||
|
|
||||||
def itempicture_upload_to(instance, filename: str) -> str:
|
def itempicture_upload_to(instance, filename: str) -> str:
|
||||||
return '%s/%s/item-%s-%s.%s' % (
|
return 'pub/%s/%s/item-%s-%s.%s' % (
|
||||||
instance.event.organizer.slug, instance.event.slug, instance.id,
|
instance.event.organizer.slug, instance.event.slug, instance.id,
|
||||||
str(uuid.uuid4()), filename.split('.')[-1]
|
str(uuid.uuid4()), filename.split('.')[-1]
|
||||||
)
|
)
|
||||||
@@ -247,7 +248,7 @@ class Item(LoggedModel):
|
|||||||
)
|
)
|
||||||
picture = models.ImageField(
|
picture = models.ImageField(
|
||||||
verbose_name=_("Product picture"),
|
verbose_name=_("Product picture"),
|
||||||
null=True, blank=True,
|
null=True, blank=True, max_length=255,
|
||||||
upload_to=itempicture_upload_to
|
upload_to=itempicture_upload_to
|
||||||
)
|
)
|
||||||
available_from = models.DateTimeField(
|
available_from = models.DateTimeField(
|
||||||
@@ -630,6 +631,8 @@ class Question(LoggedModel):
|
|||||||
:param items: A set of ``Items`` objects that this question should be applied to
|
: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.
|
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||||
:type ask_during_checkin: bool
|
:type ask_during_checkin: bool
|
||||||
|
:param identifier: An arbitrary, internal identifier
|
||||||
|
:type identifier: str
|
||||||
"""
|
"""
|
||||||
TYPE_NUMBER = "N"
|
TYPE_NUMBER = "N"
|
||||||
TYPE_STRING = "S"
|
TYPE_STRING = "S"
|
||||||
@@ -661,6 +664,12 @@ class Question(LoggedModel):
|
|||||||
question = I18nTextField(
|
question = I18nTextField(
|
||||||
verbose_name=_("Question")
|
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(
|
help_text = I18nTextField(
|
||||||
verbose_name=_("Help text"),
|
verbose_name=_("Help text"),
|
||||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||||
@@ -706,7 +715,25 @@ class Question(LoggedModel):
|
|||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|
||||||
|
def clean_identifier(self, code):
|
||||||
|
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):
|
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)
|
super().save(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
@@ -776,15 +803,40 @@ class Question(LoggedModel):
|
|||||||
|
|
||||||
return answer
|
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):
|
class QuestionOption(models.Model):
|
||||||
question = models.ForeignKey('Question', related_name='options')
|
question = models.ForeignKey('Question', related_name='options')
|
||||||
|
identifier = models.CharField(max_length=190)
|
||||||
answer = I18nCharField(verbose_name=_('Answer'))
|
answer = I18nCharField(verbose_name=_('Answer'))
|
||||||
position = models.IntegerField(default=0)
|
position = models.IntegerField(default=0)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.answer)
|
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)
|
||||||
|
|
||||||
|
@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:
|
class Meta:
|
||||||
verbose_name = _("Question option")
|
verbose_name = _("Question option")
|
||||||
verbose_name_plural = _("Question options")
|
verbose_name_plural = _("Question options")
|
||||||
|
|||||||
@@ -471,7 +471,8 @@ class QuestionAnswer(models.Model):
|
|||||||
)
|
)
|
||||||
answer = models.TextField()
|
answer = models.TextField()
|
||||||
file = models.FileField(
|
file = models.FileField(
|
||||||
null=True, blank=True, upload_to=answerfile_name
|
null=True, blank=True, upload_to=answerfile_name,
|
||||||
|
max_length=255
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -969,7 +970,7 @@ class CachedTicket(models.Model):
|
|||||||
provider = models.CharField(max_length=255)
|
provider = models.CharField(max_length=255)
|
||||||
type = models.CharField(max_length=255)
|
type = models.CharField(max_length=255)
|
||||||
extension = models.CharField(max_length=255)
|
extension = models.CharField(max_length=255)
|
||||||
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name)
|
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name, max_length=255)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -978,7 +979,7 @@ class CachedCombinedTicket(models.Model):
|
|||||||
provider = models.CharField(max_length=255)
|
provider = models.CharField(max_length=255)
|
||||||
type = models.CharField(max_length=255)
|
type = models.CharField(max_length=255)
|
||||||
extension = models.CharField(max_length=255)
|
extension = models.CharField(max_length=255)
|
||||||
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name)
|
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name, max_length=255)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class TeamAPIToken(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.team.permission_set() if self.team.organizer == organizer else set()
|
return self.team.permission_set() if self.team.organizer == organizer else set()
|
||||||
|
|
||||||
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
|
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if this token is part of a team that grants access of type ``perm_name``
|
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||||
to the event ``event``.
|
to the event ``event``.
|
||||||
@@ -272,6 +272,7 @@ class TeamAPIToken(models.Model):
|
|||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
:param event: The event to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
|
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
||||||
@@ -279,13 +280,14 @@ class TeamAPIToken(models.Model):
|
|||||||
)
|
)
|
||||||
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
|
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
|
||||||
|
|
||||||
def has_organizer_permission(self, organizer, perm_name=None):
|
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||||
"""
|
"""
|
||||||
Checks if this token is part of a team that grants access of type ``perm_name``
|
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||||
to the organizer ``organizer``.
|
to the organizer ``organizer``.
|
||||||
|
|
||||||
:param organizer: The organizer to check
|
:param organizer: The organizer to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
|
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -113,7 +114,7 @@ class TaxRule(LoggedModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.eu_reverse_charge and not self.home_country:
|
if self.eu_reverse_charge and not self.home_country:
|
||||||
raise ValueError(_('You need to set your home country to use the reverse charge feature.'))
|
raise ValidationError(_('You need to set your home country to use the reverse charge feature.'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.price_includes_tax:
|
if self.price_includes_tax:
|
||||||
@@ -124,6 +125,10 @@ class TaxRule(LoggedModel):
|
|||||||
s += ' ({})'.format(_('reverse charge enabled'))
|
s += ' ({})'.format(_('reverse charge enabled'))
|
||||||
return str(s)
|
return str(s)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_custom_rules(self):
|
||||||
|
return self.custom_rules and self.custom_rules != '[]'
|
||||||
|
|
||||||
def tax(self, base_price, base_price_is='auto'):
|
def tax(self, base_price, base_price_is='auto'):
|
||||||
if self.rate == Decimal('0.00'):
|
if self.rate == Decimal('0.00'):
|
||||||
return TaxedPrice(
|
return TaxedPrice(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def _generate_random_code(prefix=None):
|
|||||||
def generate_code(prefix=None):
|
def generate_code(prefix=None):
|
||||||
while True:
|
while True:
|
||||||
code = _generate_random_code(prefix=prefix)
|
code = _generate_random_code(prefix=prefix)
|
||||||
if not Voucher.objects.filter(code=code).exists():
|
if not Voucher.objects.filter(code__iexact=code).exists():
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
@@ -278,11 +278,9 @@ class Voucher(LoggedModel):
|
|||||||
if old_instance.quota:
|
if old_instance.quota:
|
||||||
quotas.add(old_instance.quota)
|
quotas.add(old_instance.quota)
|
||||||
elif old_instance.variation:
|
elif old_instance.variation:
|
||||||
quotas |= set(old_instance.variation.quotas.filter(
|
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
||||||
subevent=old_instance.subevent))
|
|
||||||
elif old_instance.item:
|
elif old_instance.item:
|
||||||
quotas |= set(old_instance.item.quotas.filter(
|
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
||||||
subevent=old_instance.subevent))
|
|
||||||
return quotas
|
return quotas
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -313,7 +311,7 @@ class Voucher(LoggedModel):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_voucher_code(data, event, pk):
|
def clean_voucher_code(data, event, pk):
|
||||||
if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
|
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
|
||||||
raise ValidationError(_('A voucher with this code already exists.'))
|
raise ValidationError(_('A voucher with this code already exists.'))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ def register_default_notification_types(sender, **kwargs):
|
|||||||
_('Order canceled'),
|
_('Order canceled'),
|
||||||
_('Order {order.code} has been canceled.')
|
_('Order {order.code} has been canceled.')
|
||||||
),
|
),
|
||||||
|
ParametrizedOrderNotificationType(
|
||||||
|
sender,
|
||||||
|
'pretix.event.order.expired',
|
||||||
|
_('Order expired'),
|
||||||
|
_('Order {order.code} has been marked as expired.'),
|
||||||
|
),
|
||||||
ParametrizedOrderNotificationType(
|
ParametrizedOrderNotificationType(
|
||||||
sender,
|
sender,
|
||||||
'pretix.event.order.modified',
|
'pretix.event.order.modified',
|
||||||
|
|||||||
@@ -169,6 +169,22 @@ class BasePaymentProvider:
|
|||||||
label=_('Enable payment method'),
|
label=_('Enable payment method'),
|
||||||
required=False,
|
required=False,
|
||||||
)),
|
)),
|
||||||
|
('_availability_date',
|
||||||
|
RelativeDateField(
|
||||||
|
label=_('Available until'),
|
||||||
|
help_text=_('Users will not be able to choose this payment provider after the given date.'),
|
||||||
|
required=False,
|
||||||
|
)),
|
||||||
|
('_invoice_text',
|
||||||
|
I18nFormField(
|
||||||
|
label=_('Text on invoices'),
|
||||||
|
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
|
||||||
|
'This will only be used if the invoice is generated before the order is paid. If the '
|
||||||
|
'invoice is generated later, it will show a text stating that it has already been paid.'),
|
||||||
|
required=False,
|
||||||
|
widget=I18nTextarea,
|
||||||
|
widget_kwargs={'attrs': {'rows': '2'}}
|
||||||
|
)),
|
||||||
('_fee_abs',
|
('_fee_abs',
|
||||||
forms.DecimalField(
|
forms.DecimalField(
|
||||||
label=_('Additional fee'),
|
label=_('Additional fee'),
|
||||||
@@ -187,12 +203,6 @@ class BasePaymentProvider:
|
|||||||
localize=True,
|
localize=True,
|
||||||
required=False,
|
required=False,
|
||||||
)),
|
)),
|
||||||
('_availability_date',
|
|
||||||
RelativeDateField(
|
|
||||||
label=_('Available until'),
|
|
||||||
help_text=_('Users will not be able to choose this payment provider after the given date.'),
|
|
||||||
required=False,
|
|
||||||
)),
|
|
||||||
('_fee_reverse_calc',
|
('_fee_reverse_calc',
|
||||||
forms.BooleanField(
|
forms.BooleanField(
|
||||||
label=_('Calculate the fee from the total value including the fee.'),
|
label=_('Calculate the fee from the total value including the fee.'),
|
||||||
@@ -202,16 +212,6 @@ class BasePaymentProvider:
|
|||||||
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
|
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
|
||||||
required=False
|
required=False
|
||||||
)),
|
)),
|
||||||
('_invoice_text',
|
|
||||||
I18nFormField(
|
|
||||||
label=_('Text on invoices'),
|
|
||||||
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
|
|
||||||
'This will only be used if the invoice is generated before the order is paid. If the '
|
|
||||||
'invoice is generated later, it will show a text stating that it has already been paid.'),
|
|
||||||
required=False,
|
|
||||||
widget=I18nTextarea,
|
|
||||||
widget_kwargs={'attrs': {'rows': '2'}}
|
|
||||||
)),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def settings_content_render(self, request: HttpRequest) -> str:
|
def settings_content_render(self, request: HttpRequest) -> str:
|
||||||
@@ -220,7 +220,7 @@ class BasePaymentProvider:
|
|||||||
page, this method is called. It may return HTML containing additional information
|
page, this method is called. It may return HTML containing additional information
|
||||||
that is displayed below the form fields configured in ``settings_form_fields``.
|
that is displayed below the form fields configured in ``settings_form_fields``.
|
||||||
"""
|
"""
|
||||||
pass
|
return ""
|
||||||
|
|
||||||
def render_invoice_text(self, order: Order) -> str:
|
def render_invoice_text(self, order: Order) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class RelativeDateWrapper:
|
|||||||
else:
|
else:
|
||||||
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||||
|
|
||||||
|
oldoffset = base_date.utcoffset()
|
||||||
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
|
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
|
||||||
if self.data.time:
|
if self.data.time:
|
||||||
new_date = new_date.replace(
|
new_date = new_date.replace(
|
||||||
@@ -78,6 +79,9 @@ class RelativeDateWrapper:
|
|||||||
minute=self.data.time.minute,
|
minute=self.data.time.minute,
|
||||||
second=self.data.time.second
|
second=self.data.time.second
|
||||||
)
|
)
|
||||||
|
new_date = new_date.astimezone(tz)
|
||||||
|
newoffset = new_date.utcoffset()
|
||||||
|
new_date += oldoffset - newoffset
|
||||||
return new_date
|
return new_date
|
||||||
|
|
||||||
def to_string(self) -> str:
|
def to_string(self) -> str:
|
||||||
@@ -149,6 +153,11 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
('absolute', _('Fixed date:')),
|
('absolute', _('Fixed date:')),
|
||||||
('relative', _('Relative date:')),
|
('relative', _('Relative date:')),
|
||||||
]
|
]
|
||||||
|
if kwargs.get('limit_choices'):
|
||||||
|
limit = kwargs.pop('limit_choices')
|
||||||
|
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
|
||||||
|
else:
|
||||||
|
choices = BASE_CHOICES
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
fields = (
|
fields = (
|
||||||
@@ -163,7 +172,7 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=BASE_CHOICES,
|
choices=choices,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
forms.TimeField(
|
forms.TimeField(
|
||||||
@@ -171,7 +180,7 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if 'widget' not in kwargs:
|
if 'widget' not in kwargs:
|
||||||
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
|
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices)
|
||||||
kwargs.pop('max_length', 0)
|
kwargs.pop('max_length', 0)
|
||||||
kwargs.pop('empty_value', 0)
|
kwargs.pop('empty_value', 0)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|||||||
19
src/pretix/base/services/auth.py
Normal file
19
src/pretix/base/services/auth.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import Max, Q
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from pretix.base.models.auth import StaffSession
|
||||||
|
|
||||||
|
from ..signals import periodic_task
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signal=periodic_task)
|
||||||
|
def close_inactive_staff_sessions(sender, **kwargs):
|
||||||
|
StaffSession.objects.annotate(last_used=Max('logs__datetime')).filter(
|
||||||
|
Q(last_used__lte=now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_RELATIVE)) & Q(date_end__isnull=True)
|
||||||
|
).update(
|
||||||
|
date_end=now()
|
||||||
|
)
|
||||||
@@ -295,7 +295,7 @@ class CartManager:
|
|||||||
|
|
||||||
if i.get('voucher'):
|
if i.get('voucher'):
|
||||||
try:
|
try:
|
||||||
voucher = self.event.vouchers.get(code=i.get('voucher').strip())
|
voucher = self.event.vouchers.get(code__iexact=i.get('voucher').strip())
|
||||||
except Voucher.DoesNotExist:
|
except Voucher.DoesNotExist:
|
||||||
raise CartError(error_messages['voucher_invalid'])
|
raise CartError(error_messages['voucher_invalid'])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -265,11 +265,20 @@ def build_preview_invoice_pdf(event):
|
|||||||
invoice.save()
|
invoice.save()
|
||||||
invoice.lines.all().delete()
|
invoice.lines.all().delete()
|
||||||
|
|
||||||
InvoiceLine.objects.create(
|
if event.tax_rules.exists():
|
||||||
invoice=invoice, description=_("Sample product A"),
|
for i, tr in enumerate(event.tax_rules.all()):
|
||||||
gross_value=119, tax_value=19,
|
tax = tr.tax(Decimal('100.00'))
|
||||||
tax_rate=19
|
InvoiceLine.objects.create(
|
||||||
)
|
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||||
|
gross_value=tax.gross, tax_value=tax.tax,
|
||||||
|
tax_rate=tax.rate
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
InvoiceLine.objects.create(
|
||||||
|
invoice=invoice, description=_("Sample product A"),
|
||||||
|
gross_value=100, tax_value=0, tax_rate=0
|
||||||
|
)
|
||||||
|
|
||||||
return event.invoice_renderer.generate(invoice)
|
return event.invoice_renderer.generate(invoice)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
for fee in fees:
|
for fee in fees:
|
||||||
fee.order = order
|
fee.order = order
|
||||||
fee._calculate_tax()
|
fee._calculate_tax()
|
||||||
if not fee.tax_rule.pk:
|
if fee.tax_rule and not fee.tax_rule.pk:
|
||||||
fee.tax_rule = None # TODO: deprecate
|
fee.tax_rule = None # TODO: deprecate
|
||||||
fee.save()
|
fee.save()
|
||||||
|
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ Your {event} team"""))
|
|||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||||
|
|
||||||
we did not yet receive a payment for your order for {event}.
|
we did not yet receive a payment for your order for {event}.
|
||||||
Please keep in mind that if we only guarantee your order if we receive
|
Please keep in mind that we only guarantee your order if we receive
|
||||||
your payment before {expire_date}.
|
your payment before {expire_date}.
|
||||||
|
|
||||||
You can view the payment information and the status of your order at
|
You can view the payment information and the status of your order at
|
||||||
@@ -486,7 +486,15 @@ Your {event} team"""))
|
|||||||
'update_check_id': {
|
'update_check_id': {
|
||||||
'default': None,
|
'default': None,
|
||||||
'type': str
|
'type': str
|
||||||
}
|
},
|
||||||
|
'banner_message': {
|
||||||
|
'default': '',
|
||||||
|
'type': LazyI18nString
|
||||||
|
},
|
||||||
|
'banner_message_detail': {
|
||||||
|
'default': '',
|
||||||
|
'type': LazyI18nString
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
settings_hierarkey = Hierarkey(attribute_name='settings')
|
settings_hierarkey = Hierarkey(attribute_name='settings')
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ class BaseQuestionsViewMixin:
|
|||||||
|
|
||||||
class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||||
invoice_form_class = BaseInvoiceAddressForm
|
invoice_form_class = BaseInvoiceAddressForm
|
||||||
|
only_user_visible = True
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _positions_for_questions(self):
|
def _positions_for_questions(self):
|
||||||
@@ -151,6 +152,9 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def positions(self):
|
def positions(self):
|
||||||
|
qqs = Question.objects.all()
|
||||||
|
if self.only_user_visible:
|
||||||
|
qqs = qqs.filter(ask_during_checkin=False)
|
||||||
return list(self.order.positions.select_related(
|
return list(self.order.positions.select_related(
|
||||||
'item', 'variation'
|
'item', 'variation'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
@@ -158,7 +162,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
|||||||
QuestionAnswer.objects.prefetch_related('options'),
|
QuestionAnswer.objects.prefetch_related('options'),
|
||||||
to_attr='answerlist'),
|
to_attr='answerlist'),
|
||||||
Prefetch('item__questions',
|
Prefetch('item__questions',
|
||||||
Question.objects.filter(ask_during_checkin=False).prefetch_related(
|
qqs.prefetch_related(
|
||||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ from importlib import import_module
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
|
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
|
from pretix.base.models.auth import StaffSession
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
|
|
||||||
from ..helpers.i18n import get_javascript_format, get_moment_locale
|
from ..helpers.i18n import (
|
||||||
|
get_javascript_format, get_javascript_output_format, get_moment_locale,
|
||||||
|
)
|
||||||
from .signals import html_head, nav_event, nav_global, nav_topbar
|
from .signals import html_head, nav_event, nav_global, nav_topbar
|
||||||
|
|
||||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
@@ -80,6 +84,7 @@ def contextprocessor(request):
|
|||||||
|
|
||||||
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
|
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
|
||||||
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
|
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
|
||||||
|
ctx['js_long_date_format'] = get_javascript_output_format('DATE_FORMAT')
|
||||||
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
|
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
|
||||||
ctx['js_locale'] = get_moment_locale()
|
ctx['js_locale'] = get_moment_locale()
|
||||||
ctx['select2locale'] = get_language()[:2]
|
ctx['select2locale'] = get_language()[:2]
|
||||||
@@ -91,11 +96,21 @@ def contextprocessor(request):
|
|||||||
|
|
||||||
ctx['warning_update_available'] = False
|
ctx['warning_update_available'] = False
|
||||||
ctx['warning_update_check_active'] = False
|
ctx['warning_update_check_active'] = False
|
||||||
if request.user.is_superuser:
|
gs = GlobalSettingsObject()
|
||||||
gs = GlobalSettingsObject()
|
ctx['global_settings'] = gs.settings
|
||||||
|
if request.user.is_staff:
|
||||||
if gs.settings.update_check_result_warning:
|
if gs.settings.update_check_result_warning:
|
||||||
ctx['warning_update_available'] = True
|
ctx['warning_update_available'] = True
|
||||||
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
||||||
ctx['warning_update_check_active'] = True
|
ctx['warning_update_check_active'] = True
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
ctx['staff_session'] = request.user.has_active_staff_session(request.session.session_key)
|
||||||
|
ctx['staff_need_to_explain'] = (
|
||||||
|
StaffSession.objects.filter(user=request.user, date_end__isnull=False).filter(
|
||||||
|
Q(comment__isnull=True) | Q(comment="")
|
||||||
|
)
|
||||||
|
if request.user.is_staff and settings.PRETIX_ADMIN_AUDIT_COMMENTS else StaffSession.objects.none()
|
||||||
|
)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
@@ -102,3 +103,68 @@ class SlugWidget(forms.TextInput):
|
|||||||
ctx = super().get_context(name, value, attrs)
|
ctx = super().get_context(name, value, attrs)
|
||||||
ctx['pre'] = self.prefix
|
ctx['pre'] = self.prefix
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleLanguagesWidget(forms.CheckboxSelectMultiple):
|
||||||
|
option_template_name = 'pretixcontrol/multi_languages_widget.html'
|
||||||
|
|
||||||
|
def sort(self):
|
||||||
|
self.choices = sorted(self.choices, key=lambda l: (
|
||||||
|
(
|
||||||
|
0 if l[0] in settings.LANGUAGES_OFFICIAL
|
||||||
|
else (
|
||||||
|
1 if l[0] not in settings.LANGUAGES_INCUBATING
|
||||||
|
else 2
|
||||||
|
)
|
||||||
|
), str(l[1])
|
||||||
|
))
|
||||||
|
|
||||||
|
def options(self, name, value, attrs=None):
|
||||||
|
self.sort()
|
||||||
|
return super().options(name, value, attrs)
|
||||||
|
|
||||||
|
def optgroups(self, name, value, attrs=None):
|
||||||
|
self.sort()
|
||||||
|
return super().optgroups(name, value, attrs)
|
||||||
|
|
||||||
|
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||||
|
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||||
|
opt['official'] = value in settings.LANGUAGES_OFFICIAL
|
||||||
|
opt['incubating'] = value in settings.LANGUAGES_INCUBATING
|
||||||
|
return opt
|
||||||
|
|
||||||
|
|
||||||
|
class SingleLanguageWidget(forms.Select):
|
||||||
|
|
||||||
|
def modify(self):
|
||||||
|
if hasattr(self, '_modified'):
|
||||||
|
return self.choices
|
||||||
|
self.choices = sorted(self.choices, key=lambda l: (
|
||||||
|
(
|
||||||
|
0 if l[0] in settings.LANGUAGES_OFFICIAL
|
||||||
|
else (
|
||||||
|
1 if l[0] not in settings.LANGUAGES_INCUBATING
|
||||||
|
else 2
|
||||||
|
)
|
||||||
|
), str(l[1])
|
||||||
|
))
|
||||||
|
new_choices = []
|
||||||
|
for k, v in self.choices:
|
||||||
|
new_choices.append((
|
||||||
|
k,
|
||||||
|
v if k in settings.LANGUAGES_OFFICIAL
|
||||||
|
else (
|
||||||
|
'{} (inofficial translation)'.format(v) if k not in settings.LANGUAGES_INCUBATING
|
||||||
|
else '{} (translation in progress)'.format(v)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
self._modified = True
|
||||||
|
self.choices = new_choices
|
||||||
|
|
||||||
|
def options(self, name, value, attrs=None):
|
||||||
|
self.modify()
|
||||||
|
return super().options(name, value, attrs)
|
||||||
|
|
||||||
|
def optgroups(self, name, value, attrs=None):
|
||||||
|
self.modify()
|
||||||
|
return super().optgroups(name, value, attrs)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from django.utils.timezone import get_current_timezone_name
|
|||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
from django_countries import Countries
|
from django_countries import Countries
|
||||||
from django_countries.fields import LazyTypedChoiceField
|
from django_countries.fields import LazyTypedChoiceField
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
from i18nfield.forms import (
|
||||||
|
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
|
||||||
|
)
|
||||||
from pytz import common_timezones, timezone
|
from pytz import common_timezones, timezone
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||||
@@ -17,9 +19,11 @@ from pretix.base.models import Event, Organizer, TaxRule
|
|||||||
from pretix.base.models.event import EventMetaValue, SubEvent
|
from pretix.base.models.event import EventMetaValue, SubEvent
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ExtFileField, SlugWidget, SplitDateTimePickerWidget,
|
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
|
||||||
|
SplitDateTimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@ class EventWizardFoundationForm(forms.Form):
|
|||||||
locales = forms.MultipleChoiceField(
|
locales = forms.MultipleChoiceField(
|
||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
label=_("Use languages"),
|
label=_("Use languages"),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=MultipleLanguagesWidget,
|
||||||
help_text=_('Choose all languages that your event should be available in.')
|
help_text=_('Choose all languages that your event should be available in.')
|
||||||
)
|
)
|
||||||
has_subevents = forms.BooleanField(
|
has_subevents = forms.BooleanField(
|
||||||
@@ -144,7 +148,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
|
|
||||||
def clean_slug(self):
|
def clean_slug(self):
|
||||||
slug = self.cleaned_data['slug']
|
slug = self.cleaned_data['slug']
|
||||||
if Event.objects.filter(slug=slug, organizer=self.organizer).exists():
|
if Event.objects.filter(slug__iexact=slug, organizer=self.organizer).exists():
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['duplicate_slug'],
|
self.error_messages['duplicate_slug'],
|
||||||
code='duplicate_slug'
|
code='duplicate_slug'
|
||||||
@@ -279,11 +283,12 @@ class EventSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
locales = forms.MultipleChoiceField(
|
locales = forms.MultipleChoiceField(
|
||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=MultipleLanguagesWidget,
|
||||||
label=_("Available languages"),
|
label=_("Available languages"),
|
||||||
)
|
)
|
||||||
locale = forms.ChoiceField(
|
locale = forms.ChoiceField(
|
||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
|
widget=SingleLanguageWidget,
|
||||||
label=_("Default language"),
|
label=_("Default language"),
|
||||||
)
|
)
|
||||||
show_quota_left = forms.BooleanField(
|
show_quota_left = forms.BooleanField(
|
||||||
@@ -360,6 +365,8 @@ class EventSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
imprint_url = forms.URLField(
|
imprint_url = forms.URLField(
|
||||||
label=_("Imprint URL"),
|
label=_("Imprint URL"),
|
||||||
|
help_text=_("This should point e.g. to a part of your website that has your contact details and legal "
|
||||||
|
"information."),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
confirm_text = I18nFormField(
|
confirm_text = I18nFormField(
|
||||||
@@ -373,7 +380,7 @@ class EventSettingsForm(SettingsForm):
|
|||||||
contact_mail = forms.EmailField(
|
contact_mail = forms.EmailField(
|
||||||
label=_("Contact address"),
|
label=_("Contact address"),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("Public email address for contacting the organizer")
|
help_text=_("We'll show this publicly to allow attendees to contact you.")
|
||||||
)
|
)
|
||||||
cancel_allow_user = forms.BooleanField(
|
cancel_allow_user = forms.BooleanField(
|
||||||
label=_("Allow users to cancel unpaid orders"),
|
label=_("Allow users to cancel unpaid orders"),
|
||||||
@@ -409,7 +416,10 @@ class EventSettingsForm(SettingsForm):
|
|||||||
class PaymentSettingsForm(SettingsForm):
|
class PaymentSettingsForm(SettingsForm):
|
||||||
payment_term_days = forms.IntegerField(
|
payment_term_days = forms.IntegerField(
|
||||||
label=_('Payment term in days'),
|
label=_('Payment term in days'),
|
||||||
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
|
help_text=_("The number of days after placing an order the user has to pay to preserve their reservation. If "
|
||||||
|
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
|
||||||
|
"payment methods, we recommend still setting two or three days to allow people to retry failed "
|
||||||
|
"payments."),
|
||||||
)
|
)
|
||||||
payment_term_last = RelativeDateField(
|
payment_term_last = RelativeDateField(
|
||||||
label=_('Last date of payments'),
|
label=_('Last date of payments'),
|
||||||
@@ -637,6 +647,8 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
|
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
|
||||||
]
|
]
|
||||||
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
|
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
|
||||||
|
locale_names = dict(settings.LANGUAGES)
|
||||||
|
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
|
||||||
|
|
||||||
|
|
||||||
class MailSettingsForm(SettingsForm):
|
class MailSettingsForm(SettingsForm):
|
||||||
@@ -971,6 +983,11 @@ class WidgetCodeForm(forms.Form):
|
|||||||
"bought via the widget, this voucher will be used. This can for example be used to provide "
|
"bought via the widget, this voucher will be used. This can for example be used to provide "
|
||||||
"widgets that give discounts or unlock secret products.")
|
"widgets that give discounts or unlock secret products.")
|
||||||
)
|
)
|
||||||
|
compatibility_mode = forms.BooleanField(
|
||||||
|
label=_("Compatibility mode"),
|
||||||
|
help_text=_("Our regular widget doesn't work in all website builders. If you run into trouble, try using "
|
||||||
|
"this compatibility mode.")
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = kwargs.pop('event')
|
self.event = kwargs.pop('event')
|
||||||
@@ -1032,3 +1049,118 @@ class EventDeleteForm(forms.Form):
|
|||||||
code='slug_wrong',
|
code='slug_wrong',
|
||||||
)
|
)
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
|
|
||||||
|
class QuickSetupForm(I18nForm):
|
||||||
|
show_quota_left = forms.BooleanField(
|
||||||
|
label=_("Show number of tickets left"),
|
||||||
|
help_text=_("Publicly show how many tickets of a certain type are still available."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
waiting_list_enabled = forms.BooleanField(
|
||||||
|
label=_("Waiting list"),
|
||||||
|
help_text=_("Once a ticket is sold out, people can add themselves to a waiting list. As soon as a ticket "
|
||||||
|
"becomes available again, it will be reserved for the first person on the waiting list and this "
|
||||||
|
"person will receive an email notification with a voucher that can be used to buy a ticket."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
ticket_download = forms.BooleanField(
|
||||||
|
label=_("Ticket downloads"),
|
||||||
|
help_text=_("Your customers will be able to download their tickets in PDF format."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
attendee_names_required = forms.BooleanField(
|
||||||
|
label=_("Require all attendees to fill in their names"),
|
||||||
|
help_text=_("By default, we will ask for names but not require them. You can turn this off completely in the "
|
||||||
|
"settings."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
imprint_url = forms.URLField(
|
||||||
|
label=_("Imprint URL"),
|
||||||
|
help_text=_("This should point e.g. to a part of your website that has your contact details and legal "
|
||||||
|
"information."),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
contact_mail = forms.EmailField(
|
||||||
|
label=_("Contact address"),
|
||||||
|
required=False,
|
||||||
|
help_text=_("We'll show this publicly to allow attendees to contact you.")
|
||||||
|
)
|
||||||
|
total_quota = forms.IntegerField(
|
||||||
|
label=_("Total capacity"),
|
||||||
|
min_value=0,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
'placeholder': '∞'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
payment_stripe__enabled = forms.BooleanField(
|
||||||
|
label=_("Payment via Stripe"),
|
||||||
|
help_text=_("Stripe is an online payments processor supporting credit cards and lots of other payment options. "
|
||||||
|
"To accept payments via Stripe, you will need to set up an account with them, which takes less "
|
||||||
|
"than five minutes using their simple interface."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
payment_banktransfer__enabled = forms.BooleanField(
|
||||||
|
label=_("Payment by bank transfer"),
|
||||||
|
help_text=_("Your customers will be instructed to wire the money to your account. You can then import your "
|
||||||
|
"bank statements to process the payments within pretix, or mark them as paid manually."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
payment_banktransfer_bank_details = BankTransfer.form_field(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.obj = kwargs.pop('event', None)
|
||||||
|
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
|
||||||
|
kwargs['locales'] = self.locales
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.obj.settings.payment_stripe_connect_client_id:
|
||||||
|
del self.fields['payment_stripe__enabled']
|
||||||
|
self.fields['payment_banktransfer_bank_details'].required = False
|
||||||
|
|
||||||
|
|
||||||
|
class QuickSetupProductForm(I18nForm):
|
||||||
|
name = I18nFormField(
|
||||||
|
max_length=255,
|
||||||
|
label=_("Product name"),
|
||||||
|
widget=I18nTextInput
|
||||||
|
)
|
||||||
|
default_price = forms.DecimalField(
|
||||||
|
label=_("Price (optional)"),
|
||||||
|
max_digits=7, decimal_places=2, required=False,
|
||||||
|
localize=True,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
'placeholder': _('Free')
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
quota = forms.IntegerField(
|
||||||
|
label=_("Quantity available"),
|
||||||
|
min_value=0,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
'placeholder': '∞'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
initial=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseQuickSetupProductFormSet(I18nFormSetMixin, forms.BaseFormSet):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs.pop('event', None)
|
||||||
|
if event:
|
||||||
|
kwargs['locales'] = event.settings.get('locales')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
QuickSetupProductFormSet = formset_factory(
|
||||||
|
QuickSetupProductForm,
|
||||||
|
formset=BaseQuickSetupProductFormSet,
|
||||||
|
can_order=False, can_delete=True, extra=0
|
||||||
|
)
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ class OrderFilterForm(FilterForm):
|
|||||||
| Q(invoice_address__name__icontains=u)
|
| Q(invoice_address__name__icontains=u)
|
||||||
| Q(invoice_address__company__icontains=u)
|
| Q(invoice_address__company__icontains=u)
|
||||||
| Q(pk__in=matching_invoices)
|
| Q(pk__in=matching_invoices)
|
||||||
|
| Q(comment__icontains=u)
|
||||||
| Q(has_pos=True)
|
| Q(has_pos=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -233,7 +234,7 @@ class OrderSearchFilterForm(OrderFilterForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
request = kwargs.pop('request')
|
request = kwargs.pop('request')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if request.user.is_superuser:
|
if request.user.has_active_staff_session(request.session.session_key):
|
||||||
self.fields['organizer'].queryset = Organizer.objects.all()
|
self.fields['organizer'].queryset = Organizer.objects.all()
|
||||||
else:
|
else:
|
||||||
self.fields['organizer'].queryset = Organizer.objects.filter(
|
self.fields['organizer'].queryset = Organizer.objects.filter(
|
||||||
@@ -392,7 +393,7 @@ class EventFilterForm(FilterForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
request = kwargs.pop('request')
|
request = kwargs.pop('request')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if request.user.is_superuser:
|
if request.user.has_active_staff_session(request.session.session_key):
|
||||||
self.fields['organizer'].queryset = Organizer.objects.all()
|
self.fields['organizer'].queryset = Organizer.objects.all()
|
||||||
else:
|
else:
|
||||||
self.fields['organizer'].queryset = Organizer.objects.filter(
|
self.fields['organizer'].queryset = Organizer.objects.filter(
|
||||||
@@ -582,9 +583,9 @@ class UserFilterForm(FilterForm):
|
|||||||
qs = qs.filter(is_active=False)
|
qs = qs.filter(is_active=False)
|
||||||
|
|
||||||
if fdata.get('superuser') == 'yes':
|
if fdata.get('superuser') == 'yes':
|
||||||
qs = qs.filter(is_superuser=True)
|
qs = qs.filter(is_staff=True)
|
||||||
elif fdata.get('superuser') == 'no':
|
elif fdata.get('superuser') == 'no':
|
||||||
qs = qs.filter(is_superuser=False)
|
qs = qs.filter(is_staff=False)
|
||||||
|
|
||||||
if fdata.get('query'):
|
if fdata.get('query'):
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from i18nfield.forms import I18nFormField, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
|
|
||||||
from pretix.base.forms import SettingsForm
|
from pretix.base.forms import SettingsForm
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
@@ -26,7 +26,17 @@ class GlobalSettingsForm(SettingsForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_("Additional footer link"),
|
label=_("Additional footer link"),
|
||||||
help_text=_("Will be included as the link in the additional footer text.")
|
help_text=_("Will be included as the link in the additional footer text.")
|
||||||
))
|
)),
|
||||||
|
('banner_message', I18nFormField(
|
||||||
|
widget=I18nTextarea,
|
||||||
|
required=False,
|
||||||
|
label=_("Global message banner"),
|
||||||
|
)),
|
||||||
|
('banner_message_detail', I18nFormField(
|
||||||
|
widget=I18nTextarea,
|
||||||
|
required=False,
|
||||||
|
label=_("Global message banner detail text"),
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
responses = register_global_settings.send(self)
|
responses = register_global_settings.send(self)
|
||||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||||
@@ -34,6 +44,9 @@ class GlobalSettingsForm(SettingsForm):
|
|||||||
# We need to be this explicit, since OrderedDict.update does not retain ordering
|
# We need to be this explicit, since OrderedDict.update does not retain ordering
|
||||||
self.fields[key] = value
|
self.fields[key] = value
|
||||||
|
|
||||||
|
self.fields['banner_message'].widget.attrs['rows'] = '2'
|
||||||
|
self.fields['banner_message_detail'].widget.attrs['rows'] = '3'
|
||||||
|
|
||||||
|
|
||||||
class UpdateSettingsForm(SettingsForm):
|
class UpdateSettingsForm(SettingsForm):
|
||||||
update_check_perform = forms.BooleanField(
|
update_check_perform = forms.BooleanField(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class QuestionForm(I18nModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['items'].queryset = self.instance.event.items.all()
|
self.fields['items'].queryset = self.instance.event.items.all()
|
||||||
|
self.fields['identifier'].required = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
@@ -51,6 +52,7 @@ class QuestionForm(I18nModelForm):
|
|||||||
'type',
|
'type',
|
||||||
'required',
|
'required',
|
||||||
'ask_during_checkin',
|
'ask_during_checkin',
|
||||||
|
'identifier',
|
||||||
'items'
|
'items'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@@ -221,6 +223,7 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
|
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
|
||||||
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
|
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
|
||||||
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
|
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
|
||||||
|
self.instance.free_price = self.cleaned_data['copy_from'].free_price
|
||||||
|
|
||||||
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||||
instance = super().save(*args, **kwargs)
|
instance = super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.helpers.money import change_decimal_field
|
from pretix.helpers.money import change_decimal_field
|
||||||
|
|
||||||
|
|
||||||
@@ -164,6 +166,18 @@ class OrderPositionAddForm(forms.Form):
|
|||||||
|
|
||||||
if order.event.has_subevents:
|
if order.event.has_subevents:
|
||||||
self.fields['subevent'].queryset = order.event.subevents.all()
|
self.fields['subevent'].queryset = order.event.subevents.all()
|
||||||
|
self.fields['subevent'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
|
'event': order.event.slug,
|
||||||
|
'organizer': order.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': pgettext_lazy('subevent', 'Date')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||||
|
self.fields['subevent'].required = True
|
||||||
else:
|
else:
|
||||||
del self.fields['subevent']
|
del self.fields['subevent']
|
||||||
change_decimal_field(self.fields['price'], order.event.currency)
|
change_decimal_field(self.fields['price'], order.event.currency)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea
|
|||||||
|
|
||||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||||
from pretix.base.models import Organizer, Team
|
from pretix.base.models import Organizer, Team
|
||||||
from pretix.control.forms import ExtFileField
|
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
|
||||||
from pretix.multidomain.models import KnownDomain
|
from pretix.multidomain.models import KnownDomain
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class OrganizerForm(I18nModelForm):
|
|||||||
|
|
||||||
def clean_slug(self):
|
def clean_slug(self):
|
||||||
slug = self.cleaned_data['slug']
|
slug = self.cleaned_data['slug']
|
||||||
if Organizer.objects.filter(slug=slug).exists():
|
if Organizer.objects.filter(slug__iexact=slug).exists():
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['duplicate_slug'],
|
self.error_messages['duplicate_slug'],
|
||||||
code='duplicate_slug',
|
code='duplicate_slug',
|
||||||
@@ -158,7 +158,7 @@ class OrganizerDisplaySettingsForm(SettingsForm):
|
|||||||
locales = forms.MultipleChoiceField(
|
locales = forms.MultipleChoiceField(
|
||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
label=_("Use languages"),
|
label=_("Use languages"),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=MultipleLanguagesWidget,
|
||||||
help_text=_('Choose all languages that your organizer homepage should be available in.')
|
help_text=_('Choose all languages that your organizer homepage should be available in.')
|
||||||
)
|
)
|
||||||
primary_font = forms.ChoiceField(
|
primary_font = forms.ChoiceField(
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.forms import formset_factory
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
from i18nfield.forms import I18nInlineFormSet
|
from i18nfield.forms import I18nInlineFormSet
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||||
from pretix.base.models.items import SubEventItem
|
from pretix.base.models.items import SubEventItem
|
||||||
|
from pretix.base.reldate import RelativeDateTimeField
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.control.forms import SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimePickerWidget
|
||||||
from pretix.helpers.money import change_decimal_field
|
from pretix.helpers.money import change_decimal_field
|
||||||
@@ -46,6 +52,44 @@ class SubEventForm(I18nModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SubEventBulkForm(SubEventForm):
|
||||||
|
time_from = forms.TimeField(
|
||||||
|
label=_('Event start time'),
|
||||||
|
widget=forms.TimeInput(attrs={'class': 'timepickerfield'})
|
||||||
|
)
|
||||||
|
time_to = forms.TimeField(
|
||||||
|
label=_('Event end time'),
|
||||||
|
widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
time_admission = forms.TimeField(
|
||||||
|
label=_('Admission time'),
|
||||||
|
widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
rel_presale_start = RelativeDateTimeField(
|
||||||
|
label=_('Start of presale'),
|
||||||
|
help_text=_('Optional. No products will be sold before this date.'),
|
||||||
|
required=False,
|
||||||
|
limit_choices=('date_from', 'date_to'),
|
||||||
|
)
|
||||||
|
rel_presale_end = RelativeDateTimeField(
|
||||||
|
label=_('End of presale'),
|
||||||
|
help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
|
||||||
|
'will end after the end date of your event.'),
|
||||||
|
required=False,
|
||||||
|
limit_choices=('date_from', 'date_to'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs['event']
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['location'].widget.attrs['rows'] = '3'
|
||||||
|
del self.fields['date_from']
|
||||||
|
del self.fields['date_to']
|
||||||
|
del self.fields['date_admission']
|
||||||
|
|
||||||
|
|
||||||
class SubEventItemOrVariationFormMixin:
|
class SubEventItemOrVariationFormMixin:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.item = kwargs.pop('item')
|
self.item = kwargs.pop('item')
|
||||||
@@ -97,6 +141,7 @@ class QuotaFormSet(I18nInlineFormSet):
|
|||||||
kwargs['locales'] = self.locales
|
kwargs['locales'] = self.locales
|
||||||
kwargs['event'] = self.event
|
kwargs['event'] = self.event
|
||||||
kwargs['items'] = self.items
|
kwargs['items'] = self.items
|
||||||
|
kwargs['items'] = self.items
|
||||||
return super()._construct_form(i, **kwargs)
|
return super()._construct_form(i, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -155,3 +200,175 @@ class CheckinListFormSet(I18nInlineFormSet):
|
|||||||
)
|
)
|
||||||
self.add_fields(form, None)
|
self.add_fields(form, None)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
class RRuleForm(forms.Form):
|
||||||
|
# TODO: calendar.setfirstweekday
|
||||||
|
exclude = forms.BooleanField(
|
||||||
|
label=_('Exclude these dates instead of adding them.'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
freq = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('yearly', _('year(s)')),
|
||||||
|
('monthly', _('month(s)')),
|
||||||
|
('weekly', _('week(s)')),
|
||||||
|
('daily', _('day(s)')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
interval = forms.IntegerField(
|
||||||
|
label=_('Interval'),
|
||||||
|
initial=1
|
||||||
|
)
|
||||||
|
dtstart = forms.DateField(
|
||||||
|
label=_('Start date'),
|
||||||
|
widget=forms.DateInput(
|
||||||
|
attrs={
|
||||||
|
'class': 'datepickerfield',
|
||||||
|
'required': 'required'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
initial=lambda: now().date()
|
||||||
|
)
|
||||||
|
|
||||||
|
end = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('count', ''),
|
||||||
|
('until', ''),
|
||||||
|
],
|
||||||
|
initial='count',
|
||||||
|
widget=forms.RadioSelect
|
||||||
|
)
|
||||||
|
count = forms.IntegerField(
|
||||||
|
label=_('Number of repititions'),
|
||||||
|
initial=10
|
||||||
|
)
|
||||||
|
until = forms.DateField(
|
||||||
|
widget=forms.DateInput(
|
||||||
|
attrs={
|
||||||
|
'class': 'datepickerfield',
|
||||||
|
'required': 'required'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
label=_('Last date'),
|
||||||
|
required=True,
|
||||||
|
initial=lambda: now() + timedelta(days=365)
|
||||||
|
)
|
||||||
|
|
||||||
|
yearly_bysetpos = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('1', pgettext_lazy('rrule', 'first')),
|
||||||
|
('2', pgettext_lazy('rrule', 'second')),
|
||||||
|
('3', pgettext_lazy('rrule', 'third')),
|
||||||
|
('-1', pgettext_lazy('rrule', 'last')),
|
||||||
|
],
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
yearly_same = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('on', ''),
|
||||||
|
('off', ''),
|
||||||
|
],
|
||||||
|
initial='on',
|
||||||
|
widget=forms.RadioSelect
|
||||||
|
)
|
||||||
|
yearly_byweekday = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('MO', _('Monday')),
|
||||||
|
('TU', _('Tuesday')),
|
||||||
|
('WE', _('Wednesday')),
|
||||||
|
('TH', _('Thursday')),
|
||||||
|
('FR', _('Friday')),
|
||||||
|
('SA', _('Saturday')),
|
||||||
|
('SU', _('Sunday')),
|
||||||
|
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
|
||||||
|
('MO,TU,WE,TH,FR', _('Weekday')),
|
||||||
|
('SA,SU', _('Weekend day')),
|
||||||
|
],
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
yearly_bymonth = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('1', _('January')),
|
||||||
|
('2', _('February')),
|
||||||
|
('3', _('March')),
|
||||||
|
('4', _('April')),
|
||||||
|
('5', _('May')),
|
||||||
|
('6', _('June')),
|
||||||
|
('7', _('July')),
|
||||||
|
('8', _('August')),
|
||||||
|
('9', _('September')),
|
||||||
|
('10', _('October')),
|
||||||
|
('11', _('November')),
|
||||||
|
('12', _('December')),
|
||||||
|
],
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
monthly_same = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('on', ''),
|
||||||
|
('off', ''),
|
||||||
|
],
|
||||||
|
initial='on',
|
||||||
|
widget=forms.RadioSelect
|
||||||
|
)
|
||||||
|
monthly_bysetpos = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('1', pgettext_lazy('rrule', 'first')),
|
||||||
|
('2', pgettext_lazy('rrule', 'second')),
|
||||||
|
('3', pgettext_lazy('rrule', 'third')),
|
||||||
|
('-1', pgettext_lazy('rrule', 'last')),
|
||||||
|
],
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
monthly_byweekday = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
('MO', _('Monday')),
|
||||||
|
('TU', _('Tuesday')),
|
||||||
|
('WE', _('Wednesday')),
|
||||||
|
('TH', _('Thursday')),
|
||||||
|
('FR', _('Friday')),
|
||||||
|
('SA', _('Saturday')),
|
||||||
|
('SU', _('Sunday')),
|
||||||
|
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
|
||||||
|
('MO,TU,WE,TH,FR', _('Weekday')),
|
||||||
|
('SA,SU', _('Weekend day')),
|
||||||
|
],
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
weekly_byweekday = forms.MultipleChoiceField(
|
||||||
|
choices=[
|
||||||
|
('MO', _('Monday')),
|
||||||
|
('TU', _('Tuesday')),
|
||||||
|
('WE', _('Wednesday')),
|
||||||
|
('TH', _('Thursday')),
|
||||||
|
('FR', _('Friday')),
|
||||||
|
('SA', _('Saturday')),
|
||||||
|
('SU', _('Sunday')),
|
||||||
|
],
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxSelectMultiple
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_weekdays(self, value):
|
||||||
|
m = {
|
||||||
|
'MO': 0,
|
||||||
|
'TU': 1,
|
||||||
|
'WE': 2,
|
||||||
|
'TH': 3,
|
||||||
|
'FR': 4,
|
||||||
|
'SA': 5,
|
||||||
|
'SU': 6
|
||||||
|
}
|
||||||
|
if ',' in value:
|
||||||
|
return [m.get(a) for a in value.split(',')]
|
||||||
|
else:
|
||||||
|
return m.get(value)
|
||||||
|
|
||||||
|
|
||||||
|
RRuleFormSet = formset_factory(
|
||||||
|
RRuleForm,
|
||||||
|
can_order=False, can_delete=True, extra=1
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
|
from pretix.base.models.auth import StaffSession
|
||||||
|
|
||||||
|
|
||||||
|
class StaffSessionForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = StaffSession
|
||||||
|
fields = ['comment']
|
||||||
|
|
||||||
|
|
||||||
class UserEditForm(forms.ModelForm):
|
class UserEditForm(forms.ModelForm):
|
||||||
@@ -41,7 +48,7 @@ class UserEditForm(forms.ModelForm):
|
|||||||
'email',
|
'email',
|
||||||
'require_2fa',
|
'require_2fa',
|
||||||
'is_active',
|
'is_active',
|
||||||
'is_superuser'
|
'is_staff'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.db.models.functions import Lower
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
from pretix.base.models import Item, Voucher
|
||||||
from pretix.control.forms import SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimePickerWidget
|
||||||
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
from pretix.control.signals import voucher_form_validation
|
from pretix.control.signals import voucher_form_validation
|
||||||
|
|
||||||
|
|
||||||
|
class FakeChoiceField(forms.ChoiceField):
|
||||||
|
def valid_value(self, value):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class VoucherForm(I18nModelForm):
|
class VoucherForm(I18nModelForm):
|
||||||
itemvar = forms.ChoiceField(
|
itemvar = FakeChoiceField(
|
||||||
label=_("Product"),
|
label=_("Product"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"This product is added to the user's cart if the voucher is redeemed."
|
"This product is added to the user's cart if the voucher is redeemed."
|
||||||
@@ -53,47 +61,64 @@ class VoucherForm(I18nModelForm):
|
|||||||
|
|
||||||
if instance.event.has_subevents:
|
if instance.event.has_subevents:
|
||||||
self.fields['subevent'].queryset = instance.event.subevents.all()
|
self.fields['subevent'].queryset = instance.event.subevents.all()
|
||||||
|
self.fields['subevent'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
|
'event': instance.event.slug,
|
||||||
|
'organizer': instance.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': pgettext_lazy('subevent', 'Date')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||||
|
self.fields['subevent'].required = False
|
||||||
elif 'subevent':
|
elif 'subevent':
|
||||||
del self.fields['subevent']
|
del self.fields['subevent']
|
||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
for i in self.instance.event.items.prefetch_related('variations').all():
|
|
||||||
variations = list(i.variations.all())
|
|
||||||
if variations:
|
|
||||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
|
||||||
for v in variations:
|
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
|
||||||
else:
|
|
||||||
choices.append((str(i.pk), i.name))
|
|
||||||
for q in self.instance.event.quotas.all():
|
|
||||||
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
|
||||||
self.fields['itemvar'].choices = choices
|
self.fields['itemvar'].choices = choices
|
||||||
|
self.fields['itemvar'].widget = Select2ItemVarQuota(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
|
||||||
|
'event': instance.event.slug,
|
||||||
|
'organizer': instance.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
|
||||||
|
self.fields['itemvar'].required = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
if not self._errors:
|
if not self._errors:
|
||||||
itemid = quotaid = None
|
try:
|
||||||
iv = self.data.get('itemvar', '')
|
itemid = quotaid = None
|
||||||
if iv.startswith('q-'):
|
iv = self.data.get('itemvar', '')
|
||||||
quotaid = iv[2:]
|
if iv.startswith('q-'):
|
||||||
elif '-' in iv:
|
quotaid = iv[2:]
|
||||||
itemid, varid = iv.split('-')
|
elif '-' in iv:
|
||||||
else:
|
itemid, varid = iv.split('-')
|
||||||
itemid, varid = iv, None
|
|
||||||
|
|
||||||
if itemid:
|
|
||||||
self.instance.item = Item.objects.get(pk=itemid, event=self.instance.event)
|
|
||||||
if varid:
|
|
||||||
self.instance.variation = ItemVariation.objects.get(pk=varid, item=self.instance.item)
|
|
||||||
else:
|
else:
|
||||||
self.instance.variation = None
|
itemid, varid = iv, None
|
||||||
self.instance.quota = None
|
|
||||||
|
|
||||||
else:
|
if itemid:
|
||||||
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
|
self.instance.item = self.instance.event.items.get(pk=itemid)
|
||||||
self.instance.item = None
|
if varid:
|
||||||
self.instance.variation = None
|
self.instance.variation = self.instance.item.variations.get(pk=varid)
|
||||||
|
else:
|
||||||
|
self.instance.variation = None
|
||||||
|
self.instance.quota = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.instance.quota = self.instance.event.quotas.get(pk=quotaid)
|
||||||
|
self.instance.item = None
|
||||||
|
self.instance.variation = None
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError(_("Invalid product selected."))
|
||||||
|
|
||||||
if 'codes' in data:
|
if 'codes' in data:
|
||||||
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||||
@@ -164,7 +189,10 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
if Voucher.objects.filter(code__in=data['codes'], event=self.instance.event).exists():
|
vouchers = self.instance.event.vouchers.annotate(
|
||||||
|
code_lower=Lower('code')
|
||||||
|
).filter(code_lower__in=[c.lower() for c in data['codes']])
|
||||||
|
if vouchers.exists():
|
||||||
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -36,3 +36,23 @@ class Select2(Select2Mixin, forms.Select):
|
|||||||
|
|
||||||
class Select2Multiple(Select2Mixin, forms.SelectMultiple):
|
class Select2Multiple(Select2Mixin, forms.SelectMultiple):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Select2ItemVarQuotaMixin(Select2Mixin):
|
||||||
|
|
||||||
|
def options(self, name, value, attrs=None):
|
||||||
|
if value and value[0]:
|
||||||
|
yield self.create_option(
|
||||||
|
None,
|
||||||
|
value[0],
|
||||||
|
value[0],
|
||||||
|
True,
|
||||||
|
0,
|
||||||
|
subindex=None,
|
||||||
|
attrs=attrs
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||||
from django.core.urlresolvers import get_script_prefix, resolve, reverse
|
from django.core.urlresolvers import get_script_prefix, resolve, reverse
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import redirect, resolve_url
|
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from hijack.templatetags.hijack_tags import is_hijacked
|
||||||
|
|
||||||
from pretix.base.models import Event, Organizer
|
from pretix.base.models import Event, Organizer
|
||||||
|
from pretix.base.models.auth import SuperuserPermissionSet, User
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||||
)
|
)
|
||||||
@@ -81,16 +83,52 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
slug=url.kwargs['event'],
|
slug=url.kwargs['event'],
|
||||||
organizer__slug=url.kwargs['organizer'],
|
organizer__slug=url.kwargs['organizer'],
|
||||||
).select_related('organizer').first()
|
).select_related('organizer').first()
|
||||||
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event):
|
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event,
|
||||||
|
request=request):
|
||||||
raise Http404(_("The selected event was not found or you "
|
raise Http404(_("The selected event was not found or you "
|
||||||
"have no permission to administrate it."))
|
"have no permission to administrate it."))
|
||||||
request.organizer = request.event.organizer
|
request.organizer = request.event.organizer
|
||||||
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
|
if request.user.has_active_staff_session(request.session.session_key):
|
||||||
|
request.eventpermset = SuperuserPermissionSet()
|
||||||
|
else:
|
||||||
|
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
|
||||||
elif 'organizer' in url.kwargs:
|
elif 'organizer' in url.kwargs:
|
||||||
request.organizer = Organizer.objects.filter(
|
request.organizer = Organizer.objects.filter(
|
||||||
slug=url.kwargs['organizer'],
|
slug=url.kwargs['organizer'],
|
||||||
).first()
|
).first()
|
||||||
if not request.organizer or not request.user.has_organizer_permission(request.organizer):
|
if not request.organizer or not request.user.has_organizer_permission(request.organizer, request=request):
|
||||||
raise Http404(_("The selected organizer was not found or you "
|
raise Http404(_("The selected organizer was not found or you "
|
||||||
"have no permission to administrate it."))
|
"have no permission to administrate it."))
|
||||||
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
|
if request.user.has_active_staff_session(request.session.session_key):
|
||||||
|
request.orgapermset = SuperuserPermissionSet()
|
||||||
|
else:
|
||||||
|
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogMiddleware:
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
|
||||||
|
if is_hijacked(request):
|
||||||
|
hijack_history = request.session.get('hijack_history', False)
|
||||||
|
hijacker = get_object_or_404(User, pk=hijack_history[0])
|
||||||
|
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
|
||||||
|
if ss:
|
||||||
|
ss.logs.create(
|
||||||
|
url=request.path,
|
||||||
|
method=request.method,
|
||||||
|
impersonating=request.user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ss = request.user.get_active_staff_session(request.session.session_key)
|
||||||
|
if ss:
|
||||||
|
ss.logs.create(
|
||||||
|
url=request.path,
|
||||||
|
method=request.method
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.http import urlquote
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
@@ -18,8 +21,7 @@ def event_permission_required(permission):
|
|||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
allowed = (
|
allowed = (
|
||||||
request.user.is_superuser
|
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
|
||||||
or request.user.has_event_permission(request.organizer, request.event, permission)
|
|
||||||
)
|
)
|
||||||
if allowed:
|
if allowed:
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
@@ -57,10 +59,7 @@ def organizer_permission_required(permission):
|
|||||||
# just a double check, should not ever happen
|
# just a double check, should not ever happen
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
allowed = (
|
allowed = request.user.has_organizer_permission(request.organizer, permission, request=request)
|
||||||
request.user.is_superuser
|
|
||||||
or request.user.has_organizer_permission(request.organizer, permission)
|
|
||||||
)
|
|
||||||
if allowed:
|
if allowed:
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
|
|
||||||
@@ -85,14 +84,33 @@ class OrganizerPermissionRequiredMixin:
|
|||||||
def administrator_permission_required():
|
def administrator_permission_required():
|
||||||
"""
|
"""
|
||||||
This view decorator rejects all requests with a 403 response which are not from
|
This view decorator rejects all requests with a 403 response which are not from
|
||||||
users with the is_superuser flag.
|
users with a current staff member session.
|
||||||
"""
|
"""
|
||||||
def decorator(function):
|
def decorator(function):
|
||||||
def wrapper(request, *args, **kw):
|
def wrapper(request, *args, **kw):
|
||||||
if not request.user.is_authenticated: # NOQA
|
if not request.user.is_authenticated: # NOQA
|
||||||
# just a double check, should not ever happen
|
# just a double check, should not ever happen
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if not request.user.is_superuser:
|
if not request.user.has_active_staff_session(request.session.session_key):
|
||||||
|
if request.user.is_staff:
|
||||||
|
return redirect(reverse('control:user.sudo') + '?next=' + urlquote(request.path))
|
||||||
|
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||||
|
return function(request, *args, **kw)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def staff_member_required():
|
||||||
|
"""
|
||||||
|
This view decorator rejects all requests with a 403 response which are not staff
|
||||||
|
members (but do not need to have an active session).
|
||||||
|
"""
|
||||||
|
def decorator(function):
|
||||||
|
def wrapper(request, *args, **kw):
|
||||||
|
if not request.user.is_authenticated: # NOQA
|
||||||
|
# just a double check, should not ever happen
|
||||||
|
raise PermissionDenied()
|
||||||
|
if not request.user.is_staff:
|
||||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
return wrapper
|
return wrapper
|
||||||
@@ -108,3 +126,14 @@ class AdministratorPermissionRequiredMixin:
|
|||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
view = super(AdministratorPermissionRequiredMixin, cls).as_view(**initkwargs)
|
view = super(AdministratorPermissionRequiredMixin, cls).as_view(**initkwargs)
|
||||||
return administrator_permission_required()(view)
|
return administrator_permission_required()(view)
|
||||||
|
|
||||||
|
|
||||||
|
class StaffMemberRequiredMixin:
|
||||||
|
"""
|
||||||
|
This mixin is equivalent to the staff_memer_required view decorator but
|
||||||
|
is in a form suitable for class-based views.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def as_view(cls, **initkwargs):
|
||||||
|
view = super(StaffMemberRequiredMixin, cls).as_view(**initkwargs)
|
||||||
|
return staff_member_required()(view)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
|
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
|
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
|
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
||||||
{% block custom_header %}{% endblock %}
|
{% block custom_header %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}">
|
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}">
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
|
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
@@ -150,6 +152,22 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if request.user.is_staff and not staff_session %}
|
||||||
|
<li>
|
||||||
|
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-link" id="button-sudo">
|
||||||
|
<i class="fa fa-id-card"></i> {% trans "Admin mode" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% elif request.user.is_staff and staff_session %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'control:user.sudo.stop' %}" class="danger">
|
||||||
|
<i class="fa fa-id-card"></i> {% trans "End admin session" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if warning_update_available %}
|
{% if warning_update_available %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:global.update' %}" class="danger">
|
<a href="{% url 'control:global.update' %}" class="danger">
|
||||||
@@ -190,7 +208,7 @@
|
|||||||
{% trans "Dashboard" %}
|
{% trans "Dashboard" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:global.settings' %}"
|
<a href="{% url 'control:global.settings' %}"
|
||||||
{% if "global.settings" in url_name %}class="active"{% endif %}>
|
{% if "global.settings" in url_name %}class="active"{% endif %}>
|
||||||
@@ -218,14 +236,21 @@
|
|||||||
{% trans "Order search" %}
|
{% trans "Order search" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:users' %}"
|
<a href="{% url 'control:users' %}"
|
||||||
{% if "users" in url_name %}class="active"{% endif %}>
|
{% if "users" in url_name %}class="active"{% endif %}>
|
||||||
<i class="fa fa-user fa-fw"></i>
|
<i class="fa fa-user fa-fw"></i>
|
||||||
{% trans "Users" %}
|
{% trans "Users" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'control:user.sudo.list' %}"
|
||||||
|
{% if "sudo" in url_name %}class="active"{% endif %}>
|
||||||
|
<i class="fa fa-id-card fa-fw"></i>
|
||||||
|
{% trans "Admin sessions" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for nav in nav_global %}
|
{% for nav in nav_global %}
|
||||||
<li>
|
<li>
|
||||||
@@ -259,6 +284,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% if staff_need_to_explain %}
|
||||||
|
<div class="impersonate-warning">
|
||||||
|
<span class="fa fa-id-card"></span>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please leave a short comment on what you did in the following admin sessions:
|
||||||
|
{% endblocktrans %}
|
||||||
|
<ul>
|
||||||
|
{% for s in staff_need_to_explain %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url "control:user.sudo.edit" id=s.pk %}">#{{ s.pk }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if request|is_hijacked %}
|
{% if request|is_hijacked %}
|
||||||
<div class="impersonate-warning">
|
<div class="impersonate-warning">
|
||||||
<span class="fa fa-user-secret"></span>
|
<span class="fa fa-user-secret"></span>
|
||||||
@@ -272,6 +312,17 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if global_settings.banner_message %}
|
||||||
|
<div class="impersonate-warning">
|
||||||
|
<span class="fa fa-bell"></span>
|
||||||
|
{{ global_settings.banner_message }}
|
||||||
|
{% if global_settings.banner_message %}
|
||||||
|
<a href="{% url 'control:global.message' %}">
|
||||||
|
{% trans "Read more" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div id="page-wrapper">
|
<div id="page-wrapper">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-sm-6 col-xs-12">
|
<div class="col-lg-2 col-sm-6 col-xs-12">
|
||||||
{% if log.user %}
|
{% if log.user %}
|
||||||
{% if log.user.is_superuser %}
|
{% if log.user.is_staff %}
|
||||||
<span class="fa fa-id-card fa-danger fa-fw"
|
<span class="fa fa-id-card fa-danger fa-fw"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-sm-6 col-xs-12">
|
<div class="col-lg-2 col-sm-6 col-xs-12">
|
||||||
{% if log.user %}
|
{% if log.user %}
|
||||||
{% if log.user.is_superuser %}
|
{% if log.user.is_staff %}
|
||||||
<span class="fa fa-id-card fa-danger fa-fw"
|
<span class="fa fa-id-card fa-danger fa-fw"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||||
|
|||||||
@@ -5,49 +5,53 @@
|
|||||||
<form action="" method="post" class="form-horizontal form-plugins">
|
<form action="" method="post" class="form-horizontal form-plugins">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Payment settings" %}</legend>
|
<legend>{% trans "Payment providers" %}</legend>
|
||||||
{% bootstrap_field sform.payment_term_days layout="control" %}
|
<table class="table table-payment-providers">
|
||||||
{% bootstrap_field sform.payment_term_last layout="control" %}
|
<tbody>
|
||||||
{% bootstrap_field sform.payment_term_weekdays layout="control" %}
|
{% for provider in providers %}
|
||||||
{% bootstrap_field sform.payment_term_expire_automatically layout="control" %}
|
<tr>
|
||||||
{% bootstrap_field sform.payment_term_accept_late layout="control" %}
|
<td>
|
||||||
{% bootstrap_field sform.tax_rate_default layout="control" %}
|
<strong>{{ provider.verbose_name }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if provider.is_enabled %}
|
||||||
|
<span class="text-success">
|
||||||
|
<span class="fa fa-check"></span>
|
||||||
|
{% trans "Enabled" %}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
{% trans "Disabled" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
|
||||||
|
class="btn btn-default">
|
||||||
|
<span class="fa fa-cog"></span>
|
||||||
|
{% trans "Settings" %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">
|
||||||
|
{% trans "There are no payment providers available. Please go to the plugin settings and activate one or more payment plugins." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Payment providers" %}</legend>
|
<legend>{% trans "General payment settings" %}</legend>
|
||||||
<div class="alert alert-warning">
|
{% bootstrap_field form.payment_term_days layout="control" %}
|
||||||
<span class="fa fa-w fa-legal fa-4x pull-left"></span>
|
{% bootstrap_field form.payment_term_last layout="control" %}
|
||||||
<strong>{% trans "Warning:" %}</strong>
|
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
||||||
{% blocktrans trimmed %}
|
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
|
||||||
Please note that EU Directive 2015/2366 bans surcharging payment fees for most common payment
|
{% bootstrap_field form.payment_term_accept_late layout="control" %}
|
||||||
methods within the European Union. Depending on the payment method, this might affect
|
{% bootstrap_field form.tax_rate_default layout="control" %}
|
||||||
selling to consumers only or to business customers as well. Depending on your country, this
|
|
||||||
legislation might already be in effect or become relevant from January 2018 at the latest. This
|
|
||||||
is not legal advice. If in doubt, consult a lawyer or refrain from charging payment fees.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% for provider in providers %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3 class="panel-title">
|
|
||||||
<a class="collapsed" data-toggle="collapse" href="#{{ provider.identifier }}">
|
|
||||||
{{ provider.verbose_name }}
|
|
||||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="{{ provider.identifier }}" class="panel-collapse collapse">
|
|
||||||
<div class="panel-body">
|
|
||||||
{% bootstrap_form provider.form layout='control' %}
|
|
||||||
{% with c=provider.settings_content %}
|
|
||||||
{% if c %}{{ c|safe }}{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
<em>{% trans "There are no payment providers available. Please go to the plugin settings and activate one or more payment plugins." %}</em>
|
|
||||||
{% endfor %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block inside %}
|
||||||
|
<form action="" method="post" class="form-horizontal form-plugins">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
<a href="{% url 'control:event.settings.payment' event=request.event.slug organizer=request.organizer.slug %}"
|
||||||
|
class="btn btn-default btn-sm btn-link">
|
||||||
|
<span class="fa fa-caret-left"></span>
|
||||||
|
{% trans "Back" %}
|
||||||
|
</a>
|
||||||
|
{% trans "Payment provider:" %} {{ provider.verbose_name }}
|
||||||
|
</legend>
|
||||||
|
{% bootstrap_form form layout='control' %}
|
||||||
|
{% if settings_content %}{{ settings_content|safe }}{% endif %}
|
||||||
|
<p> </p>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<span class="fa fa-w fa-legal fa-2x pull-left"></span>
|
||||||
|
<strong>{% trans "Warning:" %}</strong>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please note that EU Directive 2015/2366 bans surcharging payment fees for most common payment
|
||||||
|
methods within the European Union. If in doubt, consult a lawyer or refrain from charging payment
|
||||||
|
fees.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<br>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
In simple terms, this means you need to pay any fees imposed by the payment providers and cannot
|
||||||
|
pass it on to your customers.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
{% if plugin.app.compatibility_errors %}
|
{% if plugin.app.compatibility_errors %}
|
||||||
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
|
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
|
||||||
{% elif plugin.restricted and not request.user.is_superuser %}
|
{% elif plugin.restricted and not request.user.is_staff %}
|
||||||
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
|
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
|
||||||
{% elif plugin.module in plugins_active %}
|
{% elif plugin.module in plugins_active %}
|
||||||
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
|
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>{{ plugin.description }}</p>
|
<p>{{ plugin.description }}</p>
|
||||||
{% if plugin.restricted and not request.user.is_superuser %}
|
{% if plugin.restricted and not request.user.is_staff %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
|
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load formset_tags %}
|
||||||
|
{% block title %}{{ request.event.name }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="quick-setup-step">
|
||||||
|
<div class="quick-icon">
|
||||||
|
<span class="fa fa-fw fa-check-circle text-success"></span>
|
||||||
|
</div>
|
||||||
|
<div class="quick-content">
|
||||||
|
|
||||||
|
<h2>{% trans "Congratulations!" %}</h2>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "You just created an event!" %}</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can scroll down and create your first ticket products quickly, or you can use the navigation
|
||||||
|
on the left to modify the settings of your event in much more detail.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset class="quick-setup-step">
|
||||||
|
<div class="quick-icon">
|
||||||
|
<span class="fa fa-fw fa-ticket text-muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="quick-content">
|
||||||
|
<legend>{% trans "Create ticket types" %}</legend>
|
||||||
|
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% bootstrap_formset_errors formset %}
|
||||||
|
<div class="row hidden-sm hidden-xs" data-formset-form>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>{% trans "Ticket name" %}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>{% trans "Price (optional)" %}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<strong>{% trans "Capacity (optional)" %}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-formset-body id="ticket-type-formset">
|
||||||
|
{% for iform in formset %}
|
||||||
|
<div class="row question-option-row" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ iform.id }}
|
||||||
|
{% bootstrap_field iform.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% bootstrap_form_errors iform %}
|
||||||
|
{% bootstrap_field iform.name layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% bootstrap_field iform.default_price addon_after=request.event.currency layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
{% bootstrap_field iform.quota layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 text-right">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script type="form-template" data-formset-empty-form>
|
||||||
|
{% escapescript %}
|
||||||
|
<div class="row question-option-row" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ formset.empty_form.id }}
|
||||||
|
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
{% bootstrap_field formset.empty_form.quota layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 text-right">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endescapescript %}
|
||||||
|
</script>
|
||||||
|
<div class="row question-option-row helper-width-100">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button type="button" class="btn btn-default" data-formset-add>
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add a new ticket type" %}</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 form-inline form-quicksetup-total-capacity">
|
||||||
|
<strong>{% trans "Total capacity:" %}</strong>
|
||||||
|
<span id="total-capacity"></span>
|
||||||
|
{% bootstrap_field form.total_quota layout="inline" field_class="sr-only" %}
|
||||||
|
<a href="#" data-toggle="tooltip" title="{% trans 'You can set a limit on the total number of tickets sold for your event, regardless of the ticket type.' %}" id="total-capacity-edit">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
<p class="bigger">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
If you want to use more advanced features like non-admission products, product variations, custom
|
||||||
|
quotas, add-on products or want to modify your ticket types in more detail, you can later do so
|
||||||
|
in the "Products" section in the navigation. Don't worry, you can change everything you input here.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p> </p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="quick-setup-step">
|
||||||
|
<div class="quick-icon">
|
||||||
|
<span class="fa fa-fw fa-wrench text-muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="quick-content">
|
||||||
|
<legend>{% trans "Features" %}</legend>
|
||||||
|
<p class="bigger">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
We recommend that you take some time to go through the "Settings" part of your event, but if
|
||||||
|
you're in a hurry and want to get started quickly, here's a short version:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% bootstrap_field form.ticket_download layout="control" label_class="sr-only" field_class="col-md-12" %}
|
||||||
|
{% bootstrap_field form.waiting_list_enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
|
||||||
|
{% bootstrap_field form.show_quota_left layout="control" label_class="sr-only" field_class="col-md-12" %}
|
||||||
|
{% bootstrap_field form.attendee_names_required layout="control" label_class="sr-only" field_class="col-md-12" %}
|
||||||
|
<p> </p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="quick-setup-step" id="quick-setup-step-payment">
|
||||||
|
<div class="quick-icon">
|
||||||
|
<span class="fa fa-fw fa-money text-muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="quick-content">
|
||||||
|
<legend>{% trans "Payment" %}</legend>
|
||||||
|
<p class="bigger">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
pretix supports a
|
||||||
|
<a href="https://pretix.eu/about/en/features/payment" target="_blank">wide range of payment
|
||||||
|
providers</a> allowing you to choose the payment methods that fit your workflow best.
|
||||||
|
Here are just two of them as examples, you can add more in the "Settings" part of your event.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% bootstrap_field form.payment_banktransfer__enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
|
||||||
|
<div data-display-dependency="#id_payment_banktransfer__enabled">
|
||||||
|
{% bootstrap_field form.payment_banktransfer_bank_details layout="control" %}
|
||||||
|
</div>
|
||||||
|
{% if form.payment_stripe__enabled %}
|
||||||
|
{% bootstrap_field form.payment_stripe__enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
|
||||||
|
<div data-display-dependency="#id_payment_stripe__enabled">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
After you saved this page, we will redirect you to Stripe to create or connect an account
|
||||||
|
there. Once you completed this, you will be taken back to pretix.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p> </p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="quick-setup-step">
|
||||||
|
<div class="quick-icon">
|
||||||
|
<span class="fa fa-fw fa-envelope text-muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="quick-content">
|
||||||
|
<legend>{% trans "Getting in touch with you" %}</legend>
|
||||||
|
<p class="bigger">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
In case something goes wrong or is unclear, we strongly suggest that you provide ways for your
|
||||||
|
attendees to contact you:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% bootstrap_field form.contact_mail layout="control" %}
|
||||||
|
{% bootstrap_field form.imprint_url layout="control" %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -20,85 +20,101 @@
|
|||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
{% bootstrap_field form.name layout="control" %}
|
{% bootstrap_field form.name layout="control" %}
|
||||||
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
||||||
<legend>{% trans "Advanced settings" %}</legend>
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<span class="fa fa-fw fa-legal fa-4x pull-left"></span>
|
|
||||||
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
|
|
||||||
These settings are intended for advanced users. See the <a href="{{ docs }}">documentation</a>
|
|
||||||
for more information. Note that we are not responsible for the correct handling
|
|
||||||
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
|
|
||||||
{% endblocktrans %}
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
{% bootstrap_field form.price_includes_tax layout="control" %}
|
|
||||||
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
|
||||||
{% bootstrap_field form.home_country layout="control" %}
|
|
||||||
<legend>{% trans "Custom taxation rules" %}</legend>
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<span class="fa fa-fw fa-exclamation-circle fa-4x pull-left"></span>
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
These settings are intended for professional users with very specific taxation situations.
|
|
||||||
If you create any rule here, the reverse charge settings above will be ignored. The rules will be
|
|
||||||
checked in order and once the first rule matches the order, it will be used and all further rules will
|
|
||||||
be ignored. If no rule matches, tax will be charged.
|
|
||||||
{% endblocktrans %}
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
<div class="panel panel-default">
|
||||||
{{ formset.management_form }}
|
<div class="panel-heading">
|
||||||
{% bootstrap_formset_errors formset %}
|
<h4 class="panel-title">
|
||||||
<div data-formset-body>
|
<a data-toggle="collapse" href="#advanced">
|
||||||
{% for form in formset %}
|
<strong>{% trans "Advanced settings" %}</strong>
|
||||||
{% bootstrap_form_errors form %}
|
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||||
<div class="row" data-formset-form>
|
</a>
|
||||||
<div class="sr-only">
|
</h4>
|
||||||
{{ form.id }}
|
|
||||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
{% bootstrap_field form.country layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-3">
|
|
||||||
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-3">
|
|
||||||
{% bootstrap_field form.action layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2 text-right">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<script type="form-template" data-formset-empty-form>
|
<div id="advanced" class="panel-collapse collapsed {% if rule.eu_reverse_charge or rule.has_custom_rules or form.errors %}in{% endif %}">
|
||||||
{% escapescript %}
|
<div class="panel-body">
|
||||||
<div class="row" data-formset-form>
|
<legend>{% trans "Advanced settings" %}</legend>
|
||||||
<div class="sr-only">
|
<div class="alert alert-warning">
|
||||||
{{ form.id }}
|
<span class="fa fa-fw fa-legal fa-4x pull-left"></span>
|
||||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
|
||||||
</div>
|
These settings are intended for advanced users. See the
|
||||||
<div class="col-sm-4">
|
<a href="{{ docs }}">documentation</a>
|
||||||
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
|
for more information. Note that we are not responsible for the correct handling
|
||||||
</div>
|
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
|
||||||
<div class="col-sm-3">
|
{% endblocktrans %}
|
||||||
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
|
<div class="clearfix"></div>
|
||||||
</div>
|
|
||||||
<div class="col-sm-3">
|
|
||||||
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2 text-right">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endescapescript %}
|
{% bootstrap_field form.price_includes_tax layout="control" %}
|
||||||
</script>
|
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
||||||
<p>
|
{% bootstrap_field form.home_country layout="control" %}
|
||||||
<button type="button" class="btn btn-default" data-formset-add>
|
<legend>{% trans "Custom taxation rules" %}</legend>
|
||||||
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
|
<div class="alert alert-warning">
|
||||||
</p>
|
<span class="fa fa-fw fa-exclamation-circle fa-4x pull-left"></span>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
These settings are intended for professional users with very specific taxation situations.
|
||||||
|
If you create any rule here, the reverse charge settings above will be ignored. The rules will be
|
||||||
|
checked in order and once the first rule matches the order, it will be used and all further rules will
|
||||||
|
be ignored. If no rule matches, tax will be charged.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% bootstrap_formset_errors formset %}
|
||||||
|
<div data-formset-body>
|
||||||
|
{% for form in formset %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
<div class="row" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ form.id }}
|
||||||
|
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{% bootstrap_field form.country layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
{% bootstrap_field form.action layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2 text-right">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script type="form-template" data-formset-empty-form>
|
||||||
|
{% escapescript %}
|
||||||
|
<div class="row" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ form.id }}
|
||||||
|
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2 text-right">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endescapescript %}
|
||||||
|
</script>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-default" data-formset-add>
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,20 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% abseventurl request.event "presale:event.index" as indexurl %}
|
{% abseventurl request.event "presale:event.index" as indexurl %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if form.cleaned_data.compatibility_mode %}
|
||||||
|
<pre><div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></div>
|
||||||
|
<noscript>
|
||||||
|
<div class="pretix-widget">
|
||||||
|
<div class="pretix-widget-info-message">
|
||||||
|
{% blocktrans trimmed with a_attr='target="_blank" rel="noopener" href="'|add:indexurl|add:'"'|safe %}
|
||||||
|
JavaScript is disabled in your browser. To access our ticket shop without JavaScript,
|
||||||
|
please <a {{ a_attr }}>click here</a>.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</pre>
|
||||||
|
{% else %}
|
||||||
<pre><pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></pretix-widget>
|
<pre><pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></pretix-widget>
|
||||||
<noscript>
|
<noscript>
|
||||||
<div class="pretix-widget">
|
<div class="pretix-widget">
|
||||||
@@ -43,6 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
</pre>
|
</pre>
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<a href="https://docs.pretix.eu/en/latest/user/events/widget.html" target="_blank" rel="noopener">
|
<a href="https://docs.pretix.eu/en/latest/user/events/widget.html" target="_blank" rel="noopener">
|
||||||
<span class="fa fa-question-circle"></span>
|
<span class="fa fa-question-circle"></span>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
|
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
|
||||||
</div>
|
</div>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
|
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
|
||||||
{% trans "Create a new organizer" %}
|
{% trans "Create a new organizer" %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user