forked from CGM_Public/pretix_original
Compare commits
53 Commits
v2.8.1
...
hidden-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bbe605439 | ||
|
|
8267da66e4 | ||
|
|
481db3cb88 | ||
|
|
435d90474a | ||
|
|
fcc4170a4a | ||
|
|
c7f345e98e | ||
|
|
d30fbf4e6a | ||
|
|
5326aa7486 | ||
|
|
53147c0f0c | ||
|
|
fe31318413 | ||
|
|
bb4821eeb5 | ||
|
|
003d958cc5 | ||
|
|
93089d87e3 | ||
|
|
f79d17cb6a | ||
|
|
0e8db3181c | ||
|
|
23031642bd | ||
|
|
93cca34eab | ||
|
|
e29c8a1708 | ||
|
|
acfec59abc | ||
|
|
7adf203863 | ||
|
|
3c2de09216 | ||
|
|
26a96f107f | ||
|
|
819dd7eee6 | ||
|
|
ccc662228c | ||
|
|
99a2fde373 | ||
|
|
dda48d92c6 | ||
|
|
0a1429ed60 | ||
|
|
8487a5446d | ||
|
|
64833c0bab | ||
|
|
3f40525af5 | ||
|
|
7b6b3b1348 | ||
|
|
0b9f4cd739 | ||
|
|
885eefbcb0 | ||
|
|
573757e2bf | ||
|
|
c5a2bd35b7 | ||
|
|
4b65b94bd5 | ||
|
|
d716f7e014 | ||
|
|
d85ddb5bda | ||
|
|
b1db5dbb3e | ||
|
|
fed389b990 | ||
|
|
1ce613ff89 | ||
|
|
44bef85b66 | ||
|
|
49c4acefd0 | ||
|
|
61e111742d | ||
|
|
b2274039b3 | ||
|
|
4913190730 | ||
|
|
276a087fdb | ||
|
|
3639f2cea1 | ||
|
|
692e9c38f1 | ||
|
|
dd4075b2cc | ||
|
|
b549cb451a | ||
|
|
576132b2d0 | ||
|
|
e0c432d014 |
@@ -25,8 +25,6 @@ matrix:
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
env: JOB=plugins
|
||||
- python: 3.7
|
||||
env: JOB=doc-spelling
|
||||
- python: 3.7
|
||||
|
||||
@@ -78,6 +78,10 @@ Example::
|
||||
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
|
||||
Defaults to ``off``.
|
||||
|
||||
``obligatory_2fa``
|
||||
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
|
||||
Defaults to ``False``
|
||||
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -36,12 +36,20 @@ answers list of objects Answers to user
|
||||
├ 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
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.17
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This ``seat`` attribute has been added.
|
||||
|
||||
|
||||
Cart position endpoints
|
||||
-----------------------
|
||||
@@ -87,6 +95,7 @@ Cart position endpoints
|
||||
"datetime": "2018-06-11T10:00:00Z",
|
||||
"expires": "2018-06-11T10:00:00Z",
|
||||
"includes_tax": true,
|
||||
"seat": null,
|
||||
"answers": []
|
||||
}
|
||||
]
|
||||
@@ -132,6 +141,7 @@ Cart position endpoints
|
||||
"datetime": "2018-06-11T10:00:00Z",
|
||||
"expires": "2018-06-11T10:00:00Z",
|
||||
"includes_tax": true,
|
||||
"seat": null,
|
||||
"answers": []
|
||||
}
|
||||
|
||||
@@ -178,6 +188,7 @@ Cart position endpoints
|
||||
* ``item``
|
||||
* ``variation`` (optional)
|
||||
* ``price``
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``attendee_email`` (optional)
|
||||
* ``subevent`` (optional)
|
||||
|
||||
@@ -396,6 +396,7 @@ Order position endpoints
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
@@ -505,6 +506,7 @@ Order position endpoints
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
|
||||
@@ -27,9 +27,13 @@ presale_end datetime The date at whi
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
has_subevents boolean ``true`` if the event series feature is active for this
|
||||
event. Cannot change after event is created.
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
event.
|
||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -54,6 +58,10 @@ plugins list A list of packa
|
||||
|
||||
When cloning events, the ``testmode`` attribute will now be cloned, too.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -99,6 +107,8 @@ Endpoints
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -160,6 +170,8 @@ Endpoints
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
@@ -205,6 +217,8 @@ Endpoints
|
||||
"is_public": false,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
@@ -235,6 +249,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"plugins": [
|
||||
@@ -284,6 +300,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"plugins": [
|
||||
@@ -314,6 +332,8 @@ Endpoints
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
@@ -375,6 +395,8 @@ Endpoints
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
|
||||
@@ -23,4 +23,5 @@ Resources and endpoints
|
||||
waitinglist
|
||||
carts
|
||||
webhooks
|
||||
seatingplans
|
||||
billing_invoices
|
||||
|
||||
@@ -176,6 +176,10 @@ answers list of objects Answers to user
|
||||
├ 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
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
pdf_data object Data object required for ticket PDF generation. By default,
|
||||
this field is missing. It will be added only if you add the
|
||||
``pdf_data=true`` query parameter to your request.
|
||||
@@ -197,6 +201,10 @@ pdf_data object Data object req
|
||||
|
||||
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -328,6 +336,7 @@ List of all orders
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
@@ -470,6 +479,7 @@ Fetching individual orders
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
@@ -737,7 +747,7 @@ Creating orders
|
||||
then call the ``mark_paid`` API method.
|
||||
* ``testmode`` (optional) – Defaults to ``false``
|
||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||
order creation is successful. Any quotas that become free by this operation will be credited to your order
|
||||
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
||||
creation.
|
||||
* ``email``
|
||||
* ``locale``
|
||||
@@ -771,6 +781,7 @@ Creating orders
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``price``
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||
* ``attendee_email``
|
||||
* ``secret`` (optional)
|
||||
@@ -1287,6 +1298,7 @@ List of all order positions
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
@@ -1389,6 +1401,7 @@ Fetching individual positions
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
|
||||
209
doc/api/resources/seatingplans.rst
Normal file
209
doc/api/resources/seatingplans.rst
Normal file
@@ -0,0 +1,209 @@
|
||||
.. _`rest-seatingplans`:
|
||||
|
||||
Seating plans
|
||||
=============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The seating plan resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the plan
|
||||
name string Human-readable name of the plan
|
||||
layout object JSON representation of the seating plan. These
|
||||
representations follow a JSON schema that currently
|
||||
still evolves. The version in use can be found `here`_.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This endpoint has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/
|
||||
|
||||
Returns a list of all seating plans within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/seatingplans/ 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": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
||||
|
||||
Returns information on one plan, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/seatingplans/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": 2,
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the seating plan to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/seatingplans/
|
||||
|
||||
Creates a new seating plan
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/seatingplans/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a seating plan for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The seating plan could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
||||
|
||||
Update a plan. 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. **You can not change a plan while it is in use for
|
||||
any events.**
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/seatingplans/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"name": "Old plan"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Old plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the plan to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The plan could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource **or** the plan is currently in use.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
||||
|
||||
Delete a plan. You can not delete plans which are currently in use by any events.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/seatingplans/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 id: The ``id`` field of the plan to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the plan is currently in use.
|
||||
|
||||
|
||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/seating/seating-plan.schema.json
|
||||
@@ -36,7 +36,11 @@ variation_price_overrides list of objects List of variati
|
||||
the default price
|
||||
├ variation integer The internal variation ID
|
||||
└ price money (string) The price or ``null`` for the default price
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
@@ -54,6 +58,10 @@ meta_data dict Values set for
|
||||
|
||||
The attribute ``is_public`` has been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -93,6 +101,8 @@ Endpoints
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
@@ -142,6 +152,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -172,6 +184,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -223,6 +237,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -287,6 +303,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -371,6 +389,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
|
||||
@@ -41,6 +41,7 @@ quota integer An ID of a quot
|
||||
tag string A string that is used for grouping vouchers
|
||||
comment string An internal comment on the voucher
|
||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -48,6 +49,10 @@ subevent integer ID of the date
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``show_hidden_items`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Core
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||
item_copy_data, register_sales_channels, register_global_settings
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -26,7 +26,11 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, checkout_flow_steps, order_info, order_meta_from_request, position_info
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: order_info, order_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
@@ -45,7 +49,7 @@ Backend
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, nav_item, subevent_forms
|
||||
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
|
||||
@@ -40,6 +40,7 @@ frontend
|
||||
frontpage
|
||||
gettext
|
||||
gunicorn
|
||||
guid
|
||||
hardcoded
|
||||
hostname
|
||||
idempotency
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.8.1"
|
||||
__version__ = "2.9.0.dev0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
@@ -12,7 +13,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
def authenticate_credentials(self, key):
|
||||
model = self.get_model()
|
||||
try:
|
||||
device = model.objects.select_related('organizer').get(api_token=key)
|
||||
with scopes_disabled():
|
||||
device = model.objects.select_related('organizer').get(api_token=key)
|
||||
except model.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.base.models import Device, Event, User
|
||||
from pretix.base.models.auth import SuperuserPermissionSet
|
||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
@@ -50,9 +50,6 @@ class EventPermission(BasePermission):
|
||||
return False
|
||||
|
||||
elif 'organizer' in request.resolver_match.kwargs:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=request.resolver_match.kwargs['organizer'],
|
||||
).first()
|
||||
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
|
||||
return False
|
||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||
|
||||
@@ -4,10 +4,13 @@ from hashlib import sha1
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.urls import resolve
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
from rest_framework import status
|
||||
|
||||
from pretix.api.models import ApiCall
|
||||
from pretix.base.models import Organizer
|
||||
|
||||
|
||||
class IdempotencyMiddleware:
|
||||
@@ -89,3 +92,21 @@ class IdempotencyMiddleware:
|
||||
for k, v in json.loads(call.response_headers).values():
|
||||
r[k] = v
|
||||
return r
|
||||
|
||||
|
||||
class ApiScopeMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
if not request.path.startswith('/api/'):
|
||||
return self.get_response(request)
|
||||
|
||||
url = resolve(request.path_info)
|
||||
if 'organizer' in url.kwargs:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
).first()
|
||||
|
||||
with scope(organizer=getattr(request, 'organizer', None)):
|
||||
return self.get_response(request)
|
||||
|
||||
@@ -8,31 +8,33 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer,
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Quota
|
||||
from pretix.base.models import Quota, Seat
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
seat = InlineSeatSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
'answers',)
|
||||
'answers', 'seat')
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
@@ -71,6 +73,22 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
|
||||
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
|
||||
if validated_data.get('seat'):
|
||||
if not seated:
|
||||
raise ValidationError('The specified product does not allow to choose a seat.')
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError('The specified seat does not exist.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available():
|
||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
|
||||
@@ -11,6 +11,9 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Event, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
|
||||
|
||||
class MetaDataField(Field):
|
||||
@@ -26,6 +29,22 @@ class MetaDataField(Field):
|
||||
}
|
||||
|
||||
|
||||
class SeatCategoryMappingField(Field):
|
||||
|
||||
def to_representation(self, value):
|
||||
qs = value.seat_category_mappings.all()
|
||||
if isinstance(value, Event):
|
||||
qs = qs.filter(subevent=None)
|
||||
return {
|
||||
v.layout_category: v.product_id for v in qs
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {
|
||||
'seat_category_mapping': data or {}
|
||||
}
|
||||
|
||||
|
||||
class PluginsField(Field):
|
||||
|
||||
def to_representation(self, obj):
|
||||
@@ -45,12 +64,14 @@ class PluginsField(Field):
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
|
||||
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -61,6 +82,9 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||
|
||||
if full_data.get('has_subevents') and full_data.get('seating_plan'):
|
||||
raise ValidationError('Event series should not directly be assigned a seating plan.')
|
||||
|
||||
return data
|
||||
|
||||
def validate_has_subevents(self, value):
|
||||
@@ -92,6 +116,27 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
return value
|
||||
|
||||
def validate_seating_plan(self, value):
|
||||
if value and value.organizer != self.context['request'].organizer:
|
||||
raise ValidationError('Invalid seating plan.')
|
||||
if self.instance and self.instance.pk:
|
||||
try:
|
||||
validate_plan_change(self.instance, None, value)
|
||||
except SeatProtected as e:
|
||||
raise ValidationError(str(e))
|
||||
return value
|
||||
|
||||
def validate_seat_category_mapping(self, value):
|
||||
if value and (not self.instance or not self.instance.pk):
|
||||
raise ValidationError('You cannot specify seat category mappings on event creation.')
|
||||
item_cache = {i.pk: i for i in self.instance.items.all()}
|
||||
result = {}
|
||||
for k, item in value['seat_category_mapping'].items():
|
||||
if item not in item_cache:
|
||||
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
|
||||
result[k] = item_cache[item]
|
||||
return {'seat_category_mapping': result}
|
||||
|
||||
def validate_plugins(self, value):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
@@ -109,6 +154,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
validated_data.pop('seat_category_mapping', None)
|
||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||
event = super().create(validated_data)
|
||||
|
||||
@@ -120,6 +166,10 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
value=value
|
||||
)
|
||||
|
||||
# Seats
|
||||
if event.seating_plan:
|
||||
generate_seats(event, None, event.seating_plan, {})
|
||||
|
||||
# Plugins
|
||||
if plugins is not None:
|
||||
event.set_active_plugins(plugins)
|
||||
@@ -131,6 +181,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
event = super().update(instance, validated_data)
|
||||
|
||||
# Meta data
|
||||
@@ -151,6 +202,29 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Seats
|
||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||
current_mappings = {
|
||||
m.layout_category: m
|
||||
for m in event.seat_category_mappings.filter(subevent=None)
|
||||
}
|
||||
if not event.seating_plan:
|
||||
seat_category_mapping = {}
|
||||
for key, value in seat_category_mapping.items():
|
||||
if key in current_mappings:
|
||||
m = current_mappings.pop(key)
|
||||
m.product = value
|
||||
m.save()
|
||||
else:
|
||||
event.seat_category_mappings.create(product=value, layout_category=key)
|
||||
for m in current_mappings.values():
|
||||
m.delete()
|
||||
if 'seating_plan' in validated_data or seat_category_mapping is not None:
|
||||
generate_seats(event, None, event.seating_plan, {
|
||||
m.layout_category: m.product
|
||||
for m in event.seat_category_mappings.select_related('product').filter(subevent=None)
|
||||
})
|
||||
|
||||
# Plugins
|
||||
if plugins is not None:
|
||||
event.set_active_plugins(plugins)
|
||||
@@ -196,14 +270,15 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
meta_data = MetaDataField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'event', 'is_public',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -225,6 +300,25 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
def validate_variation_price_overrides(self, data):
|
||||
return list(filter(lambda i: 'variation' in i, data))
|
||||
|
||||
def validate_seating_plan(self, value):
|
||||
if value and value.organizer != self.context['request'].organizer:
|
||||
raise ValidationError('Invalid seating plan.')
|
||||
if self.instance and self.instance.pk:
|
||||
try:
|
||||
validate_plan_change(self.context['request'].event, self.instance, value)
|
||||
except SeatProtected as e:
|
||||
raise ValidationError(str(e))
|
||||
return value
|
||||
|
||||
def validate_seat_category_mapping(self, value):
|
||||
item_cache = {i.pk: i for i in self.context['request'].event.items.all()}
|
||||
result = {}
|
||||
for k, item in value['seat_category_mapping'].items():
|
||||
if item not in item_cache:
|
||||
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
|
||||
result[k] = item_cache[item]
|
||||
return {'seat_category_mapping': result}
|
||||
|
||||
@cached_property
|
||||
def meta_properties(self):
|
||||
return {
|
||||
@@ -242,6 +336,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
subevent = super().create(validated_data)
|
||||
|
||||
for item_price_override_data in item_price_overrides_data:
|
||||
@@ -257,6 +352,18 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
value=value
|
||||
)
|
||||
|
||||
# Seats
|
||||
if subevent.seating_plan:
|
||||
if seat_category_mapping is not None:
|
||||
for key, value in seat_category_mapping.items():
|
||||
self.context['request'].event.seat_category_mappings.create(
|
||||
product=value, layout_category=key, subevent=subevent
|
||||
)
|
||||
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
|
||||
m.layout_category: m.product
|
||||
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
|
||||
})
|
||||
|
||||
return subevent
|
||||
|
||||
@transaction.atomic
|
||||
@@ -264,6 +371,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
subevent = super().update(instance, validated_data)
|
||||
|
||||
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||
@@ -300,6 +408,31 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Seats
|
||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||
current_mappings = {
|
||||
m.layout_category: m
|
||||
for m in self.context['request'].event.seat_category_mappings.filter(subevent=subevent)
|
||||
}
|
||||
if not subevent.seating_plan:
|
||||
seat_category_mapping = {}
|
||||
for key, value in seat_category_mapping.items():
|
||||
if key in current_mappings:
|
||||
m = current_mappings.pop(key)
|
||||
m.product = value
|
||||
m.save()
|
||||
else:
|
||||
self.context['request'].event.seat_category_mappings.create(
|
||||
product=value, layout_category=key, subevent=subevent
|
||||
)
|
||||
for m in current_mappings.values():
|
||||
m.delete()
|
||||
if 'seating_plan' in validated_data or seat_category_mapping is not None:
|
||||
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
|
||||
m.layout_category: m.product
|
||||
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
|
||||
})
|
||||
|
||||
return subevent
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
|
||||
from django.utils.timezone import now
|
||||
@@ -15,7 +14,7 @@ from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||
OrderPosition, Question, QuestionAnswer, SubEvent,
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -71,6 +70,13 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
||||
return [o.identifier for o in instance.options.all()]
|
||||
|
||||
|
||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Seat
|
||||
fields = ('id', 'name', 'seat_guid')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||
@@ -166,12 +172,13 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
downloads = PositionDownloadsField(source='*')
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -430,11 +437,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'secret', 'addon_to', 'subevent', 'answers')
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat')
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||
@@ -615,8 +623,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
ia = None
|
||||
|
||||
with self.context['event'].lock() as now_dt:
|
||||
quotadiff = Counter()
|
||||
|
||||
free_seats = set()
|
||||
seats_seen = set()
|
||||
consume_carts = validated_data.pop('consume_carts', [])
|
||||
delete_cps = []
|
||||
quota_avail_cache = {}
|
||||
@@ -630,7 +638,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if quota_avail_cache[quota][1] is not None:
|
||||
quota_avail_cache[quota][1] += 1
|
||||
if cp.expires > now_dt:
|
||||
quotadiff.subtract(quotas)
|
||||
if cp.seat:
|
||||
free_seats.add(cp.seat)
|
||||
delete_cps.append(cp)
|
||||
|
||||
errs = [{} for p in positions_data]
|
||||
@@ -658,7 +667,22 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
]
|
||||
|
||||
quotadiff.update(new_quotas)
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||
if pos_data.get('seat'):
|
||||
if not seated:
|
||||
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
pos_data['seat'] = seat
|
||||
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
|
||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
|
||||
if any(errs):
|
||||
raise ValidationError({'positions': errs})
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.base.models import Organizer, SeatingPlan
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
|
||||
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Organizer
|
||||
fields = ('name', 'slug')
|
||||
|
||||
|
||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||
layout = CompatibleJSONField(
|
||||
validators=[SeatingPlanLayoutValidator()]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SeatingPlan
|
||||
fields = ('id', 'name', 'layout')
|
||||
|
||||
@@ -27,7 +27,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
model = Voucher
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment', 'subevent')
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items')
|
||||
read_only_fields = ('id', 'redeemed')
|
||||
list_serializer_class = VoucherListSerializer
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import timedelta
|
||||
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import periodic_task
|
||||
@@ -17,10 +18,12 @@ instances.
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
def cleanup_webhook_logs(sender, **kwargs):
|
||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
def cleanup_api_logs(sender, **kwargs):
|
||||
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
||||
|
||||
@@ -18,6 +18,7 @@ orga_router = routers.DefaultRouter()
|
||||
orga_router.register(r'events', event.EventViewSet)
|
||||
orga_router.register(r'subevents', event.SubEventViewSet)
|
||||
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
|
||||
event_router = routers.DefaultRouter()
|
||||
event_router.register(r'subevents', event.SubEventViewSet)
|
||||
|
||||
@@ -24,7 +24,7 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
return CartPosition.objects.filter(
|
||||
event=self.request.event,
|
||||
cart_id__endswith="@api"
|
||||
)
|
||||
).select_related('seat').prefetch_related('answers')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import DateTimeField
|
||||
@@ -24,11 +25,11 @@ from pretix.base.services.checkin import (
|
||||
)
|
||||
from pretix.helpers.database import FixedOrderBy
|
||||
|
||||
|
||||
class CheckinListFilter(FilterSet):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ['subevent']
|
||||
with scopes_disabled():
|
||||
class CheckinListFilter(FilterSet):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
@@ -146,15 +147,16 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
return Response(response)
|
||||
|
||||
|
||||
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||
with scopes_disabled():
|
||||
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(last_checked_in__isnull=not value)
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(last_checked_in__isnull=not value)
|
||||
|
||||
|
||||
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = CheckinListOrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name_cached', 'positionid')
|
||||
ordering_fields = (
|
||||
@@ -229,7 +231,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
))
|
||||
).select_related(
|
||||
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address'
|
||||
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
@@ -239,7 +241,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order')
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||
|
||||
if not self.checkinlist.all_products:
|
||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db import transaction
|
||||
from django.db.models import ProtectedError, Q
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import filters, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
@@ -18,51 +19,51 @@ from pretix.base.models import (
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
with scopes_disabled():
|
||||
class EventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
|
||||
class EventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ['is_public', 'live', 'has_subevents']
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ['is_public', 'live', 'has_subevents']
|
||||
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
)
|
||||
)
|
||||
)
|
||||
return queryset.filter(expr)
|
||||
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
)
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
)
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
|
||||
class EventViewSet(viewsets.ModelViewSet):
|
||||
@@ -85,7 +86,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'meta_values', 'meta_values__property'
|
||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -182,41 +183,42 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class SubEventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
with scopes_disabled():
|
||||
class SubEventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ['active', 'event__live']
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ['active', 'event__live']
|
||||
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
)
|
||||
return queryset.filter(expr)
|
||||
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
if value:
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
)
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -240,12 +242,18 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
event__in=self.request.user.get_events_with_any_permission()
|
||||
)
|
||||
return qs.prefetch_related(
|
||||
'subeventitem_set', 'subeventitemvariation_set'
|
||||
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
super().perform_update(serializer)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
serializer.instance.log_action(
|
||||
'pretix.subevent.changed',
|
||||
user=self.request.user,
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -21,19 +22,19 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
with scopes_disabled():
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
|
||||
else:
|
||||
return queryset.filter(tax_rule__rate=value)
|
||||
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
|
||||
else:
|
||||
return queryset.filter(tax_rule__rate=value)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
|
||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -65,7 +66,14 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
serializer.save(event=self.request.event)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item.changed',
|
||||
user=self.request.user,
|
||||
@@ -312,10 +320,11 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuestionFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||
with scopes_disabled():
|
||||
class QuestionFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||
|
||||
|
||||
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -411,10 +420,11 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuotaFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ['subevent']
|
||||
with scopes_disabled():
|
||||
class QuotaFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -452,9 +462,17 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
current_subevent = serializer.instance.subevent
|
||||
serializer.save(event=self.request.event)
|
||||
request_subevent = serializer.instance.subevent
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.changed',
|
||||
user=self.request.user,
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
@@ -50,17 +51,17 @@ from pretix.base.signals import (
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
with scopes_disabled():
|
||||
class OrderFilter(FilterSet):
|
||||
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
|
||||
class OrderFilter(FilterSet):
|
||||
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
@@ -92,8 +93,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item__category', 'addon_to',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
'item__category', 'addon_to', 'seat',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat'))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -102,7 +103,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -531,48 +532,49 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
|
||||
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
with scopes_disabled():
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||
| Q(attendee_email__icontains=value)
|
||||
| Q(addon_to__attendee_email__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
)
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||
| Q(attendee_email__icontains=value)
|
||||
| Q(addon_to__attendee_email__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
)
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = {
|
||||
'item': ['exact', 'in'],
|
||||
'variation': ['exact', 'in'],
|
||||
'secret': ['exact'],
|
||||
'order__status': ['exact', 'in'],
|
||||
'addon_to': ['exact', 'in'],
|
||||
'subevent': ['exact', 'in'],
|
||||
'pseudonymization_id': ['exact'],
|
||||
'voucher__code': ['exact'],
|
||||
'voucher': ['exact'],
|
||||
}
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = {
|
||||
'item': ['exact', 'in'],
|
||||
'variation': ['exact', 'in'],
|
||||
'secret': ['exact'],
|
||||
'order__status': ['exact', 'in'],
|
||||
'addon_to': ['exact', 'in'],
|
||||
'subevent': ['exact', 'in'],
|
||||
'pseudonymization_id': ['exact'],
|
||||
'voucher__code': ['exact'],
|
||||
'voucher': ['exact'],
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('order__datetime', 'positionid')
|
||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||
@@ -609,13 +611,13 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
)
|
||||
))
|
||||
).select_related(
|
||||
'item', 'variation', 'item__category', 'addon_to'
|
||||
'item', 'variation', 'item__category', 'addon_to', 'seat'
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||
).select_related(
|
||||
'item', 'order', 'order__event', 'order__event__organizer'
|
||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -960,22 +962,23 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer.save()
|
||||
|
||||
|
||||
class InvoiceFilter(FilterSet):
|
||||
refers = django_filters.CharFilter(method='refers_qs')
|
||||
number = django_filters.CharFilter(method='nr_qs')
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
with scopes_disabled():
|
||||
class InvoiceFilter(FilterSet):
|
||||
refers = django_filters.CharFilter(method='refers_qs')
|
||||
number = django_filters.CharFilter(method='nr_qs')
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
|
||||
def refers_qs(self, queryset, name, value):
|
||||
return queryset.annotate(
|
||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||
).filter(refers_nr__iexact=value)
|
||||
def refers_qs(self, queryset, name, value):
|
||||
return queryset.annotate(
|
||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||
).filter(refers_nr__iexact=value)
|
||||
|
||||
def nr_qs(self, queryset, name, value):
|
||||
return queryset.filter(nr__iexact=value)
|
||||
def nr_qs(self, queryset, name, value):
|
||||
return queryset.filter(nr__iexact=value)
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||
|
||||
|
||||
class RetryException(APIException):
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from rest_framework import filters, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.api.serializers.organizer import (
|
||||
OrganizerSerializer, SeatingPlanSerializer,
|
||||
)
|
||||
from pretix.base.models import Organizer, SeatingPlan
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -30,3 +34,50 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
|
||||
else:
|
||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||
|
||||
|
||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SeatingPlanSerializer
|
||||
queryset = SeatingPlan.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.seating_plans.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.seatingplan.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
|
||||
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.seatingplan.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if instance.events.exists() or instance.subevents.exists():
|
||||
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
|
||||
instance.log_action(
|
||||
'pretix.seatingplan.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.utils.timezone import now
|
||||
from django_filters.rest_framework import (
|
||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -15,22 +16,22 @@ from rest_framework.response import Response
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
from pretix.base.models import Voucher
|
||||
|
||||
with scopes_disabled():
|
||||
class VoucherFilter(FilterSet):
|
||||
active = BooleanFilter(method='filter_active')
|
||||
|
||||
class VoucherFilter(FilterSet):
|
||||
active = BooleanFilter(method='filter_active')
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
|
||||
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
|
||||
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
|
||||
|
||||
def filter_active(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
|
||||
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
|
||||
else:
|
||||
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||
def filter_active(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
|
||||
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
|
||||
else:
|
||||
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||
|
||||
|
||||
class VoucherViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import django_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
@@ -10,16 +11,16 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
|
||||
from pretix.base.models import WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
|
||||
with scopes_disabled():
|
||||
class WaitingListFilter(FilterSet):
|
||||
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
||||
|
||||
class WaitingListFilter(FilterSet):
|
||||
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
||||
def has_voucher_qs(self, queryset, name, value):
|
||||
return queryset.filter(voucher__isnull=not value)
|
||||
|
||||
def has_voucher_qs(self, queryset, name, value):
|
||||
return queryset.filter(voucher__isnull=not value)
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||
|
||||
|
||||
class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@@ -8,6 +8,7 @@ from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from requests import RequestException
|
||||
|
||||
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||
@@ -203,51 +204,52 @@ def notify_webhooks(logentry_id: int):
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
with scopes_disabled():
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
with scope(organizer=webhook.organizer):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
types = get_all_webhook_events()
|
||||
event_type = types.get(action_type)
|
||||
if not event_type or not webhook.enabled:
|
||||
return # Ignore, e.g. plugin not installed
|
||||
|
||||
types = get_all_webhook_events()
|
||||
event_type = types.get(action_type)
|
||||
if not event_type or not webhook.enabled:
|
||||
return # Ignore, e.g. plugin not installed
|
||||
payload = event_type.build_payload(logentry)
|
||||
t = time.time()
|
||||
|
||||
payload = event_type.build_payload(logentry)
|
||||
t = time.time()
|
||||
|
||||
try:
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False
|
||||
)
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=resp.status_code,
|
||||
payload=json.dumps(payload),
|
||||
response_body=resp.text[:1024 * 1024],
|
||||
success=200 <= resp.status_code <= 299
|
||||
)
|
||||
if resp.status_code == 410:
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False
|
||||
)
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=resp.status_code,
|
||||
payload=json.dumps(payload),
|
||||
response_body=resp.text[:1024 * 1024],
|
||||
success=200 <= resp.status_code <= 299
|
||||
)
|
||||
if resp.status_code == 410:
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=0,
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=0,
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
|
||||
39
src/pretix/base/management/commands/shell_scoped.py
Normal file
39
src/pretix/base/management/commands/shell_scoped.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def create_parser(self, *args, **kwargs):
|
||||
parser = super().create_parser(*args, **kwargs)
|
||||
parser.parse_args = lambda x: parser.parse_known_args(x)[0]
|
||||
return parser
|
||||
|
||||
def handle(self, *args, **options):
|
||||
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
||||
flags = parser.parse_known_args(sys.argv[2:])[1]
|
||||
if "--override" in flags:
|
||||
with scopes_disabled():
|
||||
return call_command("shell", *args, **options)
|
||||
|
||||
lookups = {}
|
||||
for flag in flags:
|
||||
lookup, value = flag.lstrip("-").split("=")
|
||||
lookup = lookup.split("__", maxsplit=1)
|
||||
lookups[lookup[0]] = {
|
||||
lookup[1] if len(lookup) > 1 else "pk": value
|
||||
}
|
||||
models = {
|
||||
model_name.split(".")[-1]: model_class
|
||||
for app_name, app_content in apps.all_models.items()
|
||||
for (model_name, model_class) in app_content.items()
|
||||
}
|
||||
scope_options = {
|
||||
app_name: models[app_name].objects.get(**app_value)
|
||||
for app_name, app_value in lookups.items()
|
||||
}
|
||||
with scope(**scope_options):
|
||||
return call_command("shell", *args, **options)
|
||||
70
src/pretix/base/migrations/0123_auto_20190530_1035.py
Normal file
70
src/pretix/base/migrations/0123_auto_20190530_1035.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 2.2.1 on 2019-05-30 10:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0122_orderposition_web_secret'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SeatingPlan',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=190)),
|
||||
('layout', models.TextField()),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seating_plans', to='pretixbase.Organizer')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SeatCategoryMapping',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('layout_category', models.CharField(max_length=190)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seat_category_mappings', to='pretixbase.Event')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seat_category_mappings', to='pretixbase.Item')),
|
||||
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seat_category_mappings', to='pretixbase.SubEvent')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Seat',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=190)),
|
||||
('blocked', models.BooleanField(default=False)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seats', to='pretixbase.Event')),
|
||||
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seats', to='pretixbase.Item')),
|
||||
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seats', to='pretixbase.SubEvent')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='seat',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Seat'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='seating_plan',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='events', to='pretixbase.SeatingPlan'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='seat',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Seat'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='seating_plan',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='subevents', to='pretixbase.SeatingPlan'),
|
||||
),
|
||||
]
|
||||
19
src/pretix/base/migrations/0124_seat_seat_guid.py
Normal file
19
src/pretix/base/migrations/0124_seat_seat_guid.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.1 on 2019-05-30 11:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0123_auto_20190530_1035'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='seat',
|
||||
name='seat_guid',
|
||||
field=models.CharField(db_index=True, default=None, max_length=190),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
26
src/pretix/base/migrations/0125_voucher_show_hidden_items.py
Normal file
26
src/pretix/base/migrations/0125_voucher_show_hidden_items.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 2.2.1 on 2019-07-07 10:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_show_hidden_items(apps, schema_editor):
|
||||
Voucher = apps.get_model('pretixbase', 'Voucher')
|
||||
Voucher.objects.filter(quota__isnull=False).update(show_hidden_items=False)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0124_seat_seat_guid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='show_hidden_items',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
set_show_hidden_items,
|
||||
migrations.RunPython.noop,
|
||||
)
|
||||
]
|
||||
@@ -24,6 +24,7 @@ from .orders import (
|
||||
from .organizer import (
|
||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||
)
|
||||
from .seating import Seat, SeatCategoryMapping, SeatingPlan
|
||||
from .tax import TaxRule
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
@@ -283,6 +284,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return True
|
||||
return False
|
||||
|
||||
@scopes_disabled()
|
||||
def get_events_with_any_permission(self, request=None):
|
||||
"""
|
||||
Returns a queryset of events the user has any permissions to.
|
||||
@@ -300,6 +302,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
def get_events_with_permission(self, permission, request=None):
|
||||
"""
|
||||
Returns a queryset of events the user has a specific permissions to.
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
@@ -20,6 +21,8 @@ class CheckinList(LoggedModel):
|
||||
'order have not been paid. This only works with pretixdesk '
|
||||
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
ordering = ('subevent__date_from', 'name')
|
||||
|
||||
@@ -167,6 +170,8 @@ class Checkin(models.Model):
|
||||
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||
|
||||
class Meta:
|
||||
unique_together = (('list', 'position'),)
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def generate_serial():
|
||||
serial = get_random_string(allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', length=16)
|
||||
while Device.objects.filter(unique_serial=serial).exists():
|
||||
@@ -15,6 +17,7 @@ def generate_serial():
|
||||
return serial
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def generate_initialization_token():
|
||||
token = get_random_string(length=16, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
while Device.objects.filter(initialization_token=token).exists():
|
||||
@@ -22,6 +25,7 @@ def generate_initialization_token():
|
||||
return token
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def generate_api_token():
|
||||
token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
while Device.objects.filter(api_token=token).exists():
|
||||
@@ -71,6 +75,8 @@ class Device(LoggedModel):
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
|
||||
class Meta:
|
||||
unique_together = (('organizer', 'device_id'),)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
@@ -335,6 +336,10 @@ class Event(EventMixin, LoggedModel):
|
||||
verbose_name=_('Event series'),
|
||||
default=False
|
||||
)
|
||||
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
|
||||
related_name='events')
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Event")
|
||||
@@ -345,6 +350,26 @@ class Event(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
@property
|
||||
def free_seats(self):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
return self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
order__event=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
|
||||
)
|
||||
),
|
||||
has_cart=Exists(
|
||||
CartPosition.objects.filter(
|
||||
event=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
expires__gte=now()
|
||||
)
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False)
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
if self.has_subevents:
|
||||
@@ -528,6 +553,24 @@ class Event(EventMixin, LoggedModel):
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
|
||||
if other.seating_plan:
|
||||
if other.seating_plan.organizer_id == self.organizer_id:
|
||||
self.seating_plan = other.seating_plan
|
||||
else:
|
||||
self.organizer.seating_plans.create(name=other.seating_plan.name, layout=other.seating_plan.layout)
|
||||
self.save()
|
||||
|
||||
for m in other.seat_category_mappings.filter(subevent__isnull=True):
|
||||
m.pk = None
|
||||
m.event = self
|
||||
m.product = item_map[m.product_id]
|
||||
m.save()
|
||||
|
||||
for s in other.seats.filter(subevent__isnull=True):
|
||||
s.pk = None
|
||||
s.event = self
|
||||
s.save()
|
||||
|
||||
for s in other.settings._objects.all():
|
||||
s.object = self
|
||||
s.pk = None
|
||||
@@ -871,10 +914,14 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Frontpage text")
|
||||
)
|
||||
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
|
||||
related_name='subevents')
|
||||
|
||||
items = models.ManyToManyField('Item', through='SubEventItem')
|
||||
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Date in event series")
|
||||
verbose_name_plural = _("Dates in event series")
|
||||
@@ -883,6 +930,28 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
||||
|
||||
@property
|
||||
def free_seats(self):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
return self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
order__event_id=self.event_id,
|
||||
subevent=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
|
||||
)
|
||||
),
|
||||
has_cart=Exists(
|
||||
CartPosition.objects.filter(
|
||||
event_id=self.event_id,
|
||||
subevent=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
expires__gte=now()
|
||||
)
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False)
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
return self.event.settings
|
||||
@@ -941,6 +1010,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
raise ValidationError(_('One or more variations do not belong to this event.'))
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import pgettext
|
||||
from django_countries.fields import CountryField
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
|
||||
def invoice_filename(instance, filename: str) -> str:
|
||||
@@ -107,6 +108,8 @@ class Invoice(models.Model):
|
||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||
internal_reference = models.TextField(blank=True)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
@staticmethod
|
||||
def _to_numeric_invoice_number(number):
|
||||
return '{:05d}'.format(int(number))
|
||||
|
||||
@@ -17,11 +17,13 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import is_naive, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import ScopedManager
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.signals import quota_availability
|
||||
|
||||
from .event import Event, SubEvent
|
||||
|
||||
@@ -155,28 +157,40 @@ class SubEventItemVariation(models.Model):
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
|
||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
q = (
|
||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||
# in models/event.py: EventMixin.annotated()
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
)
|
||||
if not allow_addons:
|
||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||
qs = qs.filter(q)
|
||||
|
||||
vouchq = Q(hide_without_voucher=False)
|
||||
if voucher and voucher.show_hidden_items:
|
||||
if voucher.item_id:
|
||||
vouchq = Q(pk=voucher.item_id)
|
||||
elif voucher.quota_id:
|
||||
vouchq = Q(quotas__in=[voucher.quota_id])
|
||||
return qs.filter(vouchq)
|
||||
|
||||
|
||||
class ItemQuerySet(models.QuerySet):
|
||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||
q = (
|
||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||
# in models/event.py: EventMixin.annotated()
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
)
|
||||
if not allow_addons:
|
||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||
qs = self.filter(q)
|
||||
return filter_available(self, channel, voucher, allow_addons)
|
||||
|
||||
vouchq = Q(hide_without_voucher=False)
|
||||
if voucher:
|
||||
if voucher.item_id:
|
||||
vouchq |= Q(pk=voucher.item_id)
|
||||
qs = qs.filter(pk=voucher.item_id)
|
||||
elif voucher.quota_id:
|
||||
qs = qs.filter(quotas__in=[voucher.quota_id])
|
||||
return qs.filter(vouchq)
|
||||
|
||||
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._queryset_class = ItemQuerySet
|
||||
|
||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
|
||||
|
||||
|
||||
class Item(LoggedModel):
|
||||
@@ -226,7 +240,7 @@ class Item(LoggedModel):
|
||||
:type sales_channels: bool
|
||||
"""
|
||||
|
||||
objects = ItemQuerySet.as_manager()
|
||||
objects = ItemQuerySetManager()
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -328,7 +342,7 @@ class Item(LoggedModel):
|
||||
verbose_name=_('This product will only be shown if a voucher matching the product is redeemed.'),
|
||||
default=False,
|
||||
help_text=_('This product will be hidden from the event page until the user enters a voucher '
|
||||
'code that is specifically tied to this product (and not via a quota).')
|
||||
'that unlocks this product.')
|
||||
)
|
||||
require_bundling = models.BooleanField(
|
||||
verbose_name=_('Only sell this product as part of a bundle'),
|
||||
@@ -591,6 +605,8 @@ class ItemVariation(models.Model):
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='item__event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Product variation")
|
||||
verbose_name_plural = _("Product variations")
|
||||
@@ -985,6 +1001,8 @@ class Question(LoggedModel):
|
||||
)
|
||||
dependency_value = models.TextField(null=True, blank=True)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question")
|
||||
verbose_name_plural = _("Questions")
|
||||
@@ -1234,6 +1252,8 @@ class Quota(LoggedModel):
|
||||
cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True)
|
||||
cached_availability_time = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Quota")
|
||||
verbose_name_plural = _("Quotas")
|
||||
@@ -1292,6 +1312,9 @@ class Quota(LoggedModel):
|
||||
return _cache[self.pk]
|
||||
now_dt = now_dt or now()
|
||||
res = self._availability(now_dt, count_waitinglist)
|
||||
for recv, resp in quota_availability.send(sender=self.event, quota=self, result=res,
|
||||
count_waitinglist=count_waitinglist):
|
||||
res = resp
|
||||
|
||||
self.event.cache.delete('item_quota_cache')
|
||||
rewrite_cache = count_waitinglist and (
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
@@ -26,6 +26,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_countries.fields import Country, CountryField
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from jsonfallback.fields import FallbackJSONField
|
||||
|
||||
@@ -186,6 +187,8 @@ class Order(LockModel, LoggedModel):
|
||||
verbose_name=_('E-mail address verified')
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
verbose_name_plural = _("Orders")
|
||||
@@ -195,6 +198,8 @@ class Order(LockModel, LoggedModel):
|
||||
return self.full_code
|
||||
|
||||
def gracefully_delete(self, user=None, auth=None):
|
||||
from . import Voucher
|
||||
|
||||
if not self.testmode:
|
||||
raise TypeError("Only test mode orders can be deleted.")
|
||||
self.event.log_action(
|
||||
@@ -203,6 +208,12 @@ class Order(LockModel, LoggedModel):
|
||||
'code': self.code,
|
||||
}
|
||||
)
|
||||
|
||||
if self.status != Order.STATUS_CANCELED:
|
||||
for position in self.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
|
||||
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
|
||||
OrderPosition.all.filter(order=self).delete()
|
||||
OrderFee.all.filter(order=self).delete()
|
||||
@@ -223,6 +234,7 @@ class Order(LockModel, LoggedModel):
|
||||
return self.all_fees(manager='objects')
|
||||
|
||||
@cached_property
|
||||
@scopes_disabled()
|
||||
def count_positions(self):
|
||||
if hasattr(self, 'pcnt'):
|
||||
return self.pcnt or 0
|
||||
@@ -246,6 +258,7 @@ class Order(LockModel, LoggedModel):
|
||||
return None
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
def payment_refund_sum(self):
|
||||
payment_sum = self.payments.filter(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED)
|
||||
@@ -257,6 +270,7 @@ class Order(LockModel, LoggedModel):
|
||||
return payment_sum - refund_sum
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
def pending_sum(self):
|
||||
total = self.total
|
||||
if self.status == Order.STATUS_CANCELED:
|
||||
@@ -431,6 +445,7 @@ class Order(LockModel, LoggedModel):
|
||||
return round_decimal(fee, self.event.currency)
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
def user_cancel_allowed(self) -> bool:
|
||||
"""
|
||||
Returns whether or not this order can be canceled by the user.
|
||||
@@ -615,7 +630,7 @@ class Order(LockModel, LoggedModel):
|
||||
), tz)
|
||||
return term_last
|
||||
|
||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False) -> Union[bool, str]:
|
||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||
"payment settings is over."),
|
||||
@@ -623,29 +638,37 @@ class Order(LockModel, LoggedModel):
|
||||
"payments should be accepted in the payment settings."),
|
||||
'require_approval': _('This order is not yet approved by the event organizer.')
|
||||
}
|
||||
if self.require_approval:
|
||||
return error_messages['require_approval']
|
||||
term_last = self.payment_term_last
|
||||
if term_last and not ignore_date:
|
||||
if now() > term_last:
|
||||
return error_messages['late_lastdate']
|
||||
if not force:
|
||||
if self.require_approval:
|
||||
return error_messages['require_approval']
|
||||
term_last = self.payment_term_last
|
||||
if term_last and not ignore_date:
|
||||
if now() > term_last:
|
||||
return error_messages['late_lastdate']
|
||||
|
||||
if self.status == self.STATUS_PENDING:
|
||||
return True
|
||||
if not self.event.settings.get('payment_term_accept_late') and not ignore_date:
|
||||
if not self.event.settings.get('payment_term_accept_late') and not ignore_date and not force:
|
||||
return error_messages['late']
|
||||
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist)
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True) -> Union[bool, str]:
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
||||
}
|
||||
now_dt = now_dt or now()
|
||||
positions = self.positions.all().select_related('item', 'variation')
|
||||
positions = self.positions.all().select_related('item', 'variation', 'seat')
|
||||
quota_cache = {}
|
||||
try:
|
||||
for i, op in enumerate(positions):
|
||||
if op.seat:
|
||||
if not op.seat.is_available(ignore_orderpos=op):
|
||||
raise Quota.QuotaExceededException(error_messages['seat_unavailable'].format(seat=op.seat))
|
||||
if force:
|
||||
continue
|
||||
|
||||
quotas = list(op.quotas)
|
||||
if len(quotas) == 0:
|
||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||
@@ -814,6 +837,8 @@ class QuestionAnswer(models.Model):
|
||||
max_length=255
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='question__event__organizer')
|
||||
|
||||
@property
|
||||
def backend_file_url(self):
|
||||
if self.file:
|
||||
@@ -921,6 +946,8 @@ class AbstractPosition(models.Model):
|
||||
:type voucher: Voucher
|
||||
:param meta_info: Additional meta information on the position, JSON-encoded.
|
||||
:type meta_info: str
|
||||
:param seat: Seat, if reserved seating is used.
|
||||
:type seat: Seat
|
||||
"""
|
||||
subevent = models.ForeignKey(
|
||||
SubEvent,
|
||||
@@ -967,6 +994,9 @@ class AbstractPosition(models.Model):
|
||||
verbose_name=_("Meta information"),
|
||||
null=True, blank=True
|
||||
)
|
||||
seat = models.ForeignKey(
|
||||
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1137,6 +1167,8 @@ class OrderPayment(models.Model):
|
||||
)
|
||||
migrated = models.BooleanField(default=False)
|
||||
|
||||
objects = ScopedManager(organizer='order__event__organizer')
|
||||
|
||||
class Meta:
|
||||
ordering = ('local_id',)
|
||||
|
||||
@@ -1164,8 +1196,8 @@ class OrderPayment(models.Model):
|
||||
|
||||
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
|
||||
from pretix.base.signals import order_paid
|
||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date)
|
||||
if not force and can_be_paid is not True:
|
||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
|
||||
if can_be_paid is not True:
|
||||
self.order.log_action('pretix.event.order.quotaexceeded', {
|
||||
'message': can_be_paid
|
||||
}, user=user, auth=auth)
|
||||
@@ -1493,6 +1525,8 @@ class OrderRefund(models.Model):
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='order__event__organizer')
|
||||
|
||||
class Meta:
|
||||
ordering = ('local_id',)
|
||||
|
||||
@@ -1554,7 +1588,7 @@ class OrderRefund(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ActivePositionManager(models.Manager):
|
||||
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(canceled=False)
|
||||
|
||||
@@ -1631,7 +1665,7 @@ class OrderFee(models.Model):
|
||||
)
|
||||
canceled = models.BooleanField(default=False)
|
||||
|
||||
all = models.Manager()
|
||||
all = ScopedManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
@property
|
||||
@@ -1736,7 +1770,7 @@ class OrderPosition(AbstractPosition):
|
||||
)
|
||||
canceled = models.BooleanField(default=False)
|
||||
|
||||
all = models.Manager()
|
||||
all = ScopedManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
class Meta:
|
||||
@@ -1836,6 +1870,7 @@ class OrderPosition(AbstractPosition):
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@scopes_disabled()
|
||||
def assign_pseudonymization_id(self):
|
||||
# This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
|
||||
# and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
|
||||
@@ -1943,6 +1978,8 @@ class CartPosition(AbstractPosition):
|
||||
)
|
||||
is_bundled = models.BooleanField(default=False)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Cart position")
|
||||
verbose_name_plural = _("Cart positions")
|
||||
@@ -1992,6 +2029,8 @@ class InvoiceAddress(models.Model):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='order__event__organizer')
|
||||
|
||||
def save(self, **kwargs):
|
||||
if self.order:
|
||||
self.order.touch()
|
||||
|
||||
111
src/pretix/base/models/seating.py
Normal file
111
src/pretix/base/models/seating.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
from collections import namedtuple
|
||||
|
||||
import jsonschema
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
||||
|
||||
|
||||
@deconstructible
|
||||
class SeatingPlanLayoutValidator:
|
||||
def __call__(self, value):
|
||||
if not isinstance(value, dict):
|
||||
try:
|
||||
val = json.loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_('Your layout file is not a valid JSON file.'))
|
||||
else:
|
||||
val = value
|
||||
with open(finders.find('seating/seating-plan.schema.json'), 'r') as f:
|
||||
schema = json.loads(f.read())
|
||||
try:
|
||||
jsonschema.validate(val, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(str(e)))
|
||||
|
||||
|
||||
class SeatingPlan(LoggedModel):
|
||||
"""
|
||||
Represents an abstract seating plan, without relation to any event.
|
||||
"""
|
||||
name = models.CharField(max_length=190, verbose_name=_('Name'))
|
||||
organizer = models.ForeignKey(Organizer, related_name='seating_plans', on_delete=models.CASCADE)
|
||||
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
|
||||
|
||||
Category = namedtuple('Categrory', 'name')
|
||||
RawSeat = namedtuple('Seat', 'name guid number row category')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def layout_data(self):
|
||||
return json.loads(self.layout)
|
||||
|
||||
@layout_data.setter
|
||||
def layout_data(self, v):
|
||||
self.layout = json.dumps(v)
|
||||
|
||||
def get_categories(self):
|
||||
return [
|
||||
self.Category(name=c['name'])
|
||||
for c in self.layout_data['categories']
|
||||
]
|
||||
|
||||
def iter_all_seats(self):
|
||||
for z in self.layout_data['zones']:
|
||||
for r in z['rows']:
|
||||
for s in r['seats']:
|
||||
yield self.RawSeat(
|
||||
number=s['seat_number'],
|
||||
guid=s['seat_guid'],
|
||||
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
|
||||
row=r['row_number'],
|
||||
category=s['category']
|
||||
)
|
||||
|
||||
|
||||
class SeatCategoryMapping(models.Model):
|
||||
"""
|
||||
Input seating plans have abstract "categories", such as "Balcony seat", etc. This model maps them to actual
|
||||
pretix product on a per-(sub)event level.
|
||||
"""
|
||||
event = models.ForeignKey(Event, related_name='seat_category_mappings', on_delete=models.CASCADE)
|
||||
subevent = models.ForeignKey(SubEvent, null=True, blank=True, related_name='seat_category_mappings', on_delete=models.CASCADE)
|
||||
layout_category = models.CharField(max_length=190)
|
||||
product = models.ForeignKey(Item, related_name='seat_category_mappings', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Seat(models.Model):
|
||||
"""
|
||||
This model is used to represent every single specific seat within an (sub)event that can be selected. It's mainly
|
||||
used for internal bookkeeping and not to be modified by users directly.
|
||||
"""
|
||||
event = models.ForeignKey(Event, related_name='seats', on_delete=models.CASCADE)
|
||||
subevent = models.ForeignKey(SubEvent, null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=190)
|
||||
seat_guid = models.CharField(max_length=190, db_index=True)
|
||||
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
||||
blocked = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None):
|
||||
from .orders import Order
|
||||
|
||||
if self.blocked:
|
||||
return False
|
||||
opqs = self.orderposition_set.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||
cpqs = self.cartposition_set.filter(expires__gte=now())
|
||||
if ignore_cart:
|
||||
cpqs = cpqs.exclude(pk=ignore_cart.pk)
|
||||
if ignore_orderpos:
|
||||
opqs = opqs.exclude(pk=ignore_orderpos.pk)
|
||||
return not opqs.exists() and not cpqs.exists()
|
||||
@@ -8,6 +8,9 @@ from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.models import SeatCategoryMapping
|
||||
|
||||
from ..decimal import round_decimal
|
||||
from .base import LoggedModel
|
||||
@@ -23,6 +26,7 @@ def _generate_random_code(prefix=None):
|
||||
return get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def generate_code(prefix=None):
|
||||
while True:
|
||||
code = _generate_random_code(prefix=prefix)
|
||||
@@ -172,6 +176,12 @@ class Voucher(LoggedModel):
|
||||
help_text=_("The text entered in this field will not be visible to the user and is available for your "
|
||||
"convenience.")
|
||||
)
|
||||
show_hidden_items = models.BooleanField(
|
||||
verbose_name=_("Shows hidden products that match this voucher"),
|
||||
default=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Voucher")
|
||||
@@ -391,3 +401,11 @@ class Voucher(LoggedModel):
|
||||
"""
|
||||
|
||||
return Order.objects.filter(all_positions__voucher__in=[self]).distinct()
|
||||
|
||||
def seating_available(self):
|
||||
kwargs = {}
|
||||
if self.subevent:
|
||||
kwargs['subevent'] = self.subevent
|
||||
if self.quota_id:
|
||||
return SeatCategoryMapping.objects.filter(product__quotas__pk=self.quota_id, **kwargs).exists()
|
||||
return self.item.seat_category_mappings.filter(**kwargs).exists()
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Voucher
|
||||
@@ -67,6 +68,8 @@ class WaitingListEntry(LoggedModel):
|
||||
)
|
||||
priority = models.IntegerField(default=0)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Waiting list entry")
|
||||
verbose_name_plural = _("Waiting list entries")
|
||||
|
||||
@@ -238,6 +238,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"TIME_FORMAT"
|
||||
) if ev.date_admission else ""
|
||||
}),
|
||||
("seat", {
|
||||
"label": _("Seat name"),
|
||||
"editor_sample": _("3, 4-5"),
|
||||
"evaluate": lambda op, order, ev: str(op.seat if op.seat else _('General admission'))
|
||||
}),
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -6,15 +6,16 @@ from typing import List, Optional
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DatabaseError, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models import Count, Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation,
|
||||
Voucher,
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation, Seat,
|
||||
SeatCategoryMapping, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
@@ -23,7 +24,7 @@ from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.checkin import _save_answers
|
||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.celery_app import app
|
||||
@@ -90,15 +91,20 @@ error_messages = {
|
||||
'product %(base)s.'),
|
||||
'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'),
|
||||
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'),
|
||||
'seat_required': _('You need to select a specific seat.'),
|
||||
'seat_invalid': _('Please select a valid seat.'),
|
||||
'seat_forbidden': _('You can not select a seat for this position.'),
|
||||
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'),
|
||||
'seat_multiple': _('You can not select the same seat multiple times.'),
|
||||
}
|
||||
|
||||
|
||||
class CartManager:
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent', 'includes_tax', 'bundled'))
|
||||
'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat'))
|
||||
RemoveOperation = namedtuple('RemoveOperation', ('position',))
|
||||
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
|
||||
'quotas', 'subevent'))
|
||||
'quotas', 'subevent', 'seat'))
|
||||
order = {
|
||||
RemoveOperation: 10,
|
||||
ExtendOperation: 20,
|
||||
@@ -116,6 +122,7 @@ class CartManager:
|
||||
self._items_cache = {}
|
||||
self._subevents_cache = {}
|
||||
self._variations_cache = {}
|
||||
self._seated_cache = {}
|
||||
self._expiry = None
|
||||
self.invoice_address = invoice_address
|
||||
self._widget_data = widget_data or {}
|
||||
@@ -127,6 +134,11 @@ class CartManager:
|
||||
Q(cart_id=self.cart_id) & Q(event=self.event)
|
||||
).select_related('item', 'subevent')
|
||||
|
||||
def _is_seated(self, item, subevent):
|
||||
if (item, subevent) not in self._seated_cache:
|
||||
self._seated_cache[item, subevent] = item.seat_category_mappings.filter(subevent=subevent).exists()
|
||||
return self._seated_cache[item, subevent]
|
||||
|
||||
def _calculate_expiry(self):
|
||||
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
||||
|
||||
@@ -187,6 +199,8 @@ class CartManager:
|
||||
i.pk: i
|
||||
for i in self.event.items.select_related('category').prefetch_related(
|
||||
'addons', 'bundles', 'addons__addon_category', 'quotas'
|
||||
).annotate(
|
||||
has_variations=Count('variations'),
|
||||
).filter(
|
||||
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
||||
)
|
||||
@@ -211,10 +225,7 @@ class CartManager:
|
||||
|
||||
def _check_item_constraints(self, op):
|
||||
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
|
||||
if op.item.require_voucher and op.voucher is None:
|
||||
raise CartError(error_messages['voucher_required'])
|
||||
|
||||
if op.item.hide_without_voucher and (op.voucher is None or op.voucher.item is None or op.voucher.item.pk != op.item.pk):
|
||||
if (op.item.require_voucher or op.item.hide_without_voucher) and (op.voucher is None or not op.voucher.show_hidden_items):
|
||||
raise CartError(error_messages['voucher_required'])
|
||||
|
||||
if not op.item.is_available() or (op.variation and not op.variation.active):
|
||||
@@ -223,6 +234,12 @@ class CartManager:
|
||||
if self._sales_channel not in op.item.sales_channels:
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.item.has_variations and not op.variation:
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.variation and op.variation.item_id != op.item.pk:
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
|
||||
raise CartError(error_messages['voucher_invalid_item'])
|
||||
|
||||
@@ -238,6 +255,16 @@ class CartManager:
|
||||
if op.subevent and op.subevent.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
seated = self._is_seated(op.item, op.subevent)
|
||||
if seated and (not op.seat or op.seat.blocked):
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
elif op.seat and not seated:
|
||||
raise CartError(error_messages['seat_forbidden'])
|
||||
elif op.seat and op.seat.product != op.item:
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
elif op.seat and op.count > 1:
|
||||
raise CartError('Invalid request: A seat can only be bought once.')
|
||||
|
||||
if op.subevent:
|
||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if tlv:
|
||||
@@ -300,6 +327,13 @@ class CartManager:
|
||||
def extend_expired_positions(self):
|
||||
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
|
||||
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
|
||||
).annotate(
|
||||
requires_seat=Exists(
|
||||
SeatCategoryMapping.objects.filter(
|
||||
Q(product=OuterRef('item'))
|
||||
& (Q(subevent=OuterRef('subevent')) if self.event.has_subevents else Q(subevent__isnull=True))
|
||||
)
|
||||
)
|
||||
).prefetch_related(
|
||||
'item__quotas',
|
||||
'variation__quotas',
|
||||
@@ -312,6 +346,8 @@ class CartManager:
|
||||
if cp.pk in removed_positions or (cp.addon_to_id and cp.addon_to_id in removed_positions):
|
||||
continue
|
||||
|
||||
cp.item.requires_seat = cp.requires_seat
|
||||
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
@@ -358,7 +394,7 @@ class CartManager:
|
||||
|
||||
op = self.ExtendOperation(
|
||||
position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1,
|
||||
price=price, quotas=quotas, subevent=cp.subevent
|
||||
price=price, quotas=quotas, subevent=cp.subevent, seat=cp.seat
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
|
||||
@@ -377,12 +413,6 @@ class CartManager:
|
||||
operations = []
|
||||
|
||||
for i in items:
|
||||
# Check whether the specified items are part of what we just fetched from the database
|
||||
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||
# a different event
|
||||
if i['item'] not in self._items_cache or (i['variation'] and i['variation'] not in self._variations_cache):
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if self.event.has_subevents:
|
||||
if not i.get('subevent'):
|
||||
raise CartError(error_messages['subevent_required'])
|
||||
@@ -390,6 +420,24 @@ class CartManager:
|
||||
else:
|
||||
subevent = None
|
||||
|
||||
# When a seat is given, we ignore the item that was given, since we can infer it from the
|
||||
# seat. The variation is still relevant, though!
|
||||
seat = None
|
||||
if i.get('seat'):
|
||||
try:
|
||||
seat = (subevent or self.event).seats.get(seat_guid=i.get('seat'))
|
||||
except Seat.DoesNotExist:
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
i['item'] = seat.product_id
|
||||
if i['item'] not in self._items_cache:
|
||||
self._update_items_cache([i['item']], [i['variation']])
|
||||
|
||||
# Check whether the specified items are part of what we just fetched from the database
|
||||
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||
# a different event
|
||||
if i['item'] not in self._items_cache or (i['variation'] and i['variation'] not in self._variations_cache):
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
item = self._items_cache[i['item']]
|
||||
variation = self._variations_cache[i['variation']] if i['variation'] is not None else None
|
||||
voucher = None
|
||||
@@ -445,7 +493,7 @@ class CartManager:
|
||||
bop = self.AddOperation(
|
||||
count=bundle.count, item=bitem, variation=bvar, price=bprice,
|
||||
voucher=None, quotas=bundle_quotas, addon_to='FAKE', subevent=subevent,
|
||||
includes_tax=bool(bprice.rate), bundled=[]
|
||||
includes_tax=bool(bprice.rate), bundled=[], seat=None
|
||||
)
|
||||
self._check_item_constraints(bop)
|
||||
bundled.append(bop)
|
||||
@@ -454,7 +502,7 @@ class CartManager:
|
||||
|
||||
op = self.AddOperation(
|
||||
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled, seat=seat
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
@@ -560,7 +608,7 @@ class CartManager:
|
||||
|
||||
op = self.AddOperation(
|
||||
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[]
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=cp.seat
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
@@ -686,6 +734,7 @@ class CartManager:
|
||||
err = err or self._check_min_per_product()
|
||||
|
||||
self._operations.sort(key=lambda a: self.order[type(a)])
|
||||
seats_seen = set()
|
||||
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.RemoveOperation):
|
||||
@@ -699,6 +748,11 @@ class CartManager:
|
||||
# Create a CartPosition for as much items as we can
|
||||
requested_count = quota_available_count = voucher_available_count = op.count
|
||||
|
||||
if op.seat:
|
||||
if op.seat in seats_seen:
|
||||
err = err or error_messages['seat_multiple']
|
||||
seats_seen.add(op.seat)
|
||||
|
||||
if op.quotas:
|
||||
quota_available_count = min(requested_count, min(quotas_ok[q] for q in op.quotas))
|
||||
|
||||
@@ -744,12 +798,16 @@ class CartManager:
|
||||
available_count = 0
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
if op.seat and not op.seat.is_available():
|
||||
available_count = 0
|
||||
err = err or error_messages['seat_unavailable']
|
||||
|
||||
for k in range(available_count):
|
||||
cp = CartPosition(
|
||||
event=self.event, item=op.item, variation=op.variation,
|
||||
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
|
||||
subevent=op.subevent, includes_tax=op.includes_tax
|
||||
subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat
|
||||
)
|
||||
if self.event.settings.attendee_names_asked:
|
||||
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
|
||||
@@ -788,7 +846,11 @@ class CartManager:
|
||||
|
||||
new_cart_positions.append(cp)
|
||||
elif isinstance(op, self.ExtendOperation):
|
||||
if available_count == 1:
|
||||
if op.seat and not op.seat.is_available(ignore_cart=op.position):
|
||||
err = err or error_messages['seat_unavailable']
|
||||
op.position.addons.all().delete()
|
||||
op.position.delete()
|
||||
elif available_count == 1:
|
||||
op.position.expires = self._expiry
|
||||
op.position.price = op.price.gross
|
||||
try:
|
||||
@@ -819,6 +881,9 @@ class CartManager:
|
||||
# If any quotas are affected that are not unlimited, we lock
|
||||
return True
|
||||
|
||||
if any(getattr(o, 'seat', False) for o in self._operations):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def commit(self):
|
||||
@@ -902,23 +967,22 @@ def get_fees(event, request, total, invoice_address, provider):
|
||||
return fees
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
|
||||
"""
|
||||
Adds a list of items to a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param items: A list of dicts with the keys item, variation, count, custom_price, voucher
|
||||
:param items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
|
||||
:param cart_id: Session ID of a guest
|
||||
:raises CartError: On any error that occured
|
||||
"""
|
||||
with language(locale):
|
||||
event = Event.objects.get(id=event)
|
||||
|
||||
ia = False
|
||||
if invoice_address:
|
||||
try:
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
with scopes_disabled():
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
@@ -934,8 +998,8 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def remove_cart_position(self, event: int, position: int, cart_id: str=None, locale='en') -> None:
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -943,7 +1007,6 @@ def remove_cart_position(self, event: int, position: int, cart_id: str=None, loc
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale):
|
||||
event = Event.objects.get(id=event)
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id)
|
||||
@@ -955,15 +1018,14 @@ def remove_cart_position(self, event: int, position: int, cart_id: str=None, loc
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale):
|
||||
event = Event.objects.get(id=event)
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id)
|
||||
@@ -975,8 +1037,8 @@ def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en',
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
@@ -985,12 +1047,11 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale):
|
||||
event = Event.objects.get(id=event)
|
||||
|
||||
ia = False
|
||||
if invoice_address:
|
||||
try:
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
with scopes_disabled():
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
try:
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import timedelta
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
||||
|
||||
@@ -10,6 +11,7 @@ from ..signals import periodic_task
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def clean_cart_positions(sender, **kwargs):
|
||||
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=False):
|
||||
cp.delete()
|
||||
@@ -20,12 +22,14 @@ def clean_cart_positions(sender, **kwargs):
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def clean_cached_files(sender, **kwargs):
|
||||
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
|
||||
cf.delete()
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def clean_cached_tickets(sender, **kwargs):
|
||||
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
|
||||
cf.delete()
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils.translation import ugettext
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.signals import register_data_exporters
|
||||
from pretix.celery_app import app
|
||||
|
||||
@@ -15,9 +15,8 @@ class ExportError(LazyLocaleException):
|
||||
pass
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, throws=(ExportError,))
|
||||
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
||||
event = Event.objects.get(id=event)
|
||||
@app.task(base=ProfiledEventTask, throws=(ExportError,))
|
||||
def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
||||
file = CachedFile.objects.get(id=fileid)
|
||||
with language(event.settings.locale), override(event.settings.timezone):
|
||||
responses = register_data_exporters.send(event)
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.utils import timezone
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.i18n import language
|
||||
@@ -244,16 +245,18 @@ def generate_invoice(order: Order, trigger_pdf=True):
|
||||
|
||||
@app.task(base=TransactionAwareTask)
|
||||
def invoice_pdf_task(invoice: int):
|
||||
i = Invoice.objects.get(pk=invoice)
|
||||
if i.shredded:
|
||||
return None
|
||||
if i.file:
|
||||
i.file.delete()
|
||||
with language(i.locale):
|
||||
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
|
||||
i.file.save(fname, ContentFile(fcontent))
|
||||
i.save()
|
||||
return i.file.name
|
||||
with scopes_disabled():
|
||||
i = Invoice.objects.get(pk=invoice)
|
||||
with scope(organizer=i.order.event.organizer):
|
||||
if i.shredded:
|
||||
return None
|
||||
if i.file:
|
||||
i.file.delete()
|
||||
with language(i.locale):
|
||||
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
|
||||
i.file.save(fname, ContentFile(fcontent))
|
||||
i.save()
|
||||
return i.file.name
|
||||
|
||||
|
||||
def invoice_qualified(order: Order):
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import ClassicMailRenderer
|
||||
@@ -219,98 +220,102 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
|
||||
if html is not None:
|
||||
email.attach_alternative(html, "text/html")
|
||||
if invoices:
|
||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||
for inv in invoices:
|
||||
if inv.file:
|
||||
try:
|
||||
email.attach(
|
||||
'{}.pdf'.format(inv.number),
|
||||
inv.file.file.read(),
|
||||
'application/pdf'
|
||||
)
|
||||
except:
|
||||
logger.exception('Could not attach invoice to email')
|
||||
pass
|
||||
|
||||
if event:
|
||||
event = Event.objects.get(id=event)
|
||||
with scopes_disabled():
|
||||
event = Event.objects.get(id=event)
|
||||
backend = event.get_mail_backend()
|
||||
cm = lambda: scope(organizer=event.organizer) # noqa
|
||||
else:
|
||||
backend = get_connection(fail_silently=False)
|
||||
cm = lambda: scopes_disabled() # noqa
|
||||
|
||||
if event:
|
||||
if order:
|
||||
try:
|
||||
order = event.orders.get(pk=order)
|
||||
except Order.DoesNotExist:
|
||||
order = None
|
||||
else:
|
||||
if position:
|
||||
with cm():
|
||||
if invoices:
|
||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||
for inv in invoices:
|
||||
if inv.file:
|
||||
try:
|
||||
position = order.positions.get(pk=position)
|
||||
except OrderPosition.DoesNotExist:
|
||||
attach_tickets = False
|
||||
if attach_tickets:
|
||||
args = []
|
||||
attach_size = 0
|
||||
for name, ct in get_tickets_for_order(order, base_position=position):
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
attach_size += len(content)
|
||||
|
||||
if attach_size < 4 * 1024 * 1024:
|
||||
# Do not attach more than 4MB, it will bounce way to often.
|
||||
for a in args:
|
||||
try:
|
||||
email.attach(*a)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.attachments.skipped',
|
||||
data={
|
||||
'subject': 'Attachments skipped',
|
||||
'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
email.attach(
|
||||
'{}.pdf'.format(inv.number),
|
||||
inv.file.file.read(),
|
||||
'application/pdf'
|
||||
)
|
||||
except:
|
||||
logger.exception('Could not attach invoice to email')
|
||||
pass
|
||||
if event:
|
||||
if order:
|
||||
try:
|
||||
order = event.orders.get(pk=order)
|
||||
except Order.DoesNotExist:
|
||||
order = None
|
||||
else:
|
||||
if position:
|
||||
try:
|
||||
position = order.positions.get(pk=position)
|
||||
except OrderPosition.DoesNotExist:
|
||||
attach_tickets = False
|
||||
if attach_tickets:
|
||||
args = []
|
||||
attach_size = 0
|
||||
for name, ct in get_tickets_for_order(order, base_position=position):
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
attach_size += len(content)
|
||||
|
||||
email = email_filter.send_chained(event, 'message', message=email, order=order)
|
||||
if attach_size < 4 * 1024 * 1024:
|
||||
# Do not attach more than 4MB, it will bounce way to often.
|
||||
for a in args:
|
||||
try:
|
||||
email.attach(*a)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.attachments.skipped',
|
||||
data={
|
||||
'subject': 'Attachments skipped',
|
||||
'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
backend.send_messages([email])
|
||||
except smtplib.SMTPResponseException as e:
|
||||
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
logger.exception('Error sending email')
|
||||
email = email_filter.send_chained(event, 'message', message=email, order=order)
|
||||
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
data={
|
||||
'subject': 'SMTP code {}'.format(e.smtp_code),
|
||||
'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
)
|
||||
try:
|
||||
backend.send_messages([email])
|
||||
except smtplib.SMTPResponseException as e:
|
||||
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
logger.exception('Error sending email')
|
||||
|
||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||
except Exception as e:
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
data={
|
||||
'subject': 'Internal error',
|
||||
'message': str(e),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
)
|
||||
logger.exception('Error sending email')
|
||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
data={
|
||||
'subject': 'SMTP code {}'.format(e.smtp_code),
|
||||
'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
)
|
||||
|
||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||
except Exception as e:
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
data={
|
||||
'subject': 'Internal error',
|
||||
'message': str(e),
|
||||
'recipient': '',
|
||||
'invoices': [],
|
||||
}
|
||||
)
|
||||
logger.exception('Error sending email')
|
||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||
|
||||
|
||||
def mail_send(*args, **kwargs):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.i18n import language
|
||||
@@ -12,6 +13,7 @@ from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
|
||||
@app.task(base=TransactionAwareTask)
|
||||
@scopes_disabled()
|
||||
def notify(logentry_id: int):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
if not logentry.event:
|
||||
@@ -66,17 +68,22 @@ def notify(logentry_id: int):
|
||||
@app.task(base=ProfiledTask)
|
||||
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
user = User.objects.get(id=user_id)
|
||||
types = get_all_notification_types(logentry.event)
|
||||
notification_type = types.get(action_type)
|
||||
if not notification_type:
|
||||
return # Ignore, e.g. plugin not active for this event
|
||||
if logentry.event:
|
||||
sm = lambda: scope(organizer=logentry.event.organizer) # noqa
|
||||
else:
|
||||
sm = lambda: scopes_disabled() # noqa
|
||||
with sm():
|
||||
user = User.objects.get(id=user_id)
|
||||
types = get_all_notification_types(logentry.event)
|
||||
notification_type = types.get(action_type)
|
||||
if not notification_type:
|
||||
return # Ignore, e.g. plugin not active for this event
|
||||
|
||||
with language(user.locale):
|
||||
notification = notification_type.build_notification(logentry)
|
||||
with language(user.locale):
|
||||
notification = notification_type.build_notification(logentry)
|
||||
|
||||
if method == "mail":
|
||||
send_notification_mail(notification, user)
|
||||
if method == "mail":
|
||||
send_notification_mail(notification, user)
|
||||
|
||||
|
||||
def send_notification_mail(notification: Notification, user: User):
|
||||
|
||||
@@ -16,6 +16,7 @@ from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import OAuthApplication
|
||||
from pretix.base.i18n import (
|
||||
@@ -23,7 +24,7 @@ from pretix.base.i18n import (
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, Item, ItemVariation, Order, OrderPayment,
|
||||
OrderPosition, Quota, User, Voucher,
|
||||
OrderPosition, Quota, Seat, SeatCategoryMapping, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemBundle
|
||||
@@ -42,7 +43,7 @@ from pretix.base.services.invoices import (
|
||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import (
|
||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
||||
@@ -81,6 +82,8 @@ error_messages = {
|
||||
'affected positions have been removed from your cart.'),
|
||||
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
|
||||
'positions have been removed from your cart.'),
|
||||
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
|
||||
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -427,6 +430,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
products_seen = Counter()
|
||||
changed_prices = {}
|
||||
deleted_positions = set()
|
||||
seats_seen = set()
|
||||
|
||||
def delete(cp):
|
||||
# Delete a cart position, including parents and children, if applicable
|
||||
@@ -489,17 +493,32 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if (cp.requires_seat and not cp.seat) or (cp.seat and not cp.requires_seat) or (cp.seat and cp.seat.product != cp.item) or cp.seat in seats_seen:
|
||||
err = err or error_messages['seat_invalid']
|
||||
delete(cp)
|
||||
break
|
||||
if cp.seat:
|
||||
seats_seen.add(cp.seat)
|
||||
|
||||
if cp.item.require_voucher and cp.voucher is None:
|
||||
delete(cp)
|
||||
err = err or error_messages['voucher_required']
|
||||
break
|
||||
|
||||
if cp.item.hide_without_voucher and (cp.voucher is None or cp.voucher.item is None
|
||||
or cp.voucher.item.pk != cp.item.pk):
|
||||
if cp.item.hide_without_voucher and (cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item.pk, cp.variation.pk)):
|
||||
delete(cp)
|
||||
cp.delete()
|
||||
err = error_messages['voucher_required']
|
||||
break
|
||||
|
||||
if cp.seat:
|
||||
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every time, since we absolutely
|
||||
# can not overbook a seat.
|
||||
if not cp.seat.is_available(ignore_cart=cp) or cp.seat.blocked:
|
||||
err = err or error_messages['seat_unavailable']
|
||||
cp.delete()
|
||||
continue
|
||||
|
||||
if cp.expires >= now_dt and not cp.voucher:
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
@@ -715,10 +734,8 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
|
||||
logger.exception('Order received email could not be sent to attendee')
|
||||
|
||||
|
||||
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
|
||||
|
||||
event = Event.objects.get(id=event)
|
||||
if payment_provider:
|
||||
pprov = event.get_payment_providers().get(payment_provider)
|
||||
if not pprov:
|
||||
@@ -732,25 +749,35 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
addr = None
|
||||
if address is not None:
|
||||
try:
|
||||
addr = InvoiceAddress.objects.get(pk=address)
|
||||
with scopes_disabled():
|
||||
addr = InvoiceAddress.objects.get(pk=address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
positions = CartPosition.objects.filter(id__in=position_ids, event=event)
|
||||
positions = CartPosition.objects.annotate(
|
||||
requires_seat=Exists(
|
||||
SeatCategoryMapping.objects.filter(
|
||||
Q(product=OuterRef('item'))
|
||||
& (Q(subevent=OuterRef('subevent')) if event.has_subevents else Q(subevent__isnull=True))
|
||||
)
|
||||
)
|
||||
).filter(
|
||||
id__in=position_ids, event=event
|
||||
)
|
||||
|
||||
validate_order.send(event, payment_provider=pprov, email=email, positions=positions,
|
||||
locale=locale, invoice_address=addr, meta_info=meta_info)
|
||||
|
||||
lockfn = NoLockManager
|
||||
locked = False
|
||||
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
|
||||
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2)) | Q(seat__isnull=False)).exists():
|
||||
# Performance optimization: If no voucher is used and no cart position is dangerously close to its expiry date,
|
||||
# creating this order shouldn't be prone to any race conditions and we don't need to lock the event.
|
||||
locked = True
|
||||
lockfn = event.lock
|
||||
|
||||
with lockfn() as now_dt:
|
||||
positions = list(positions.select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
|
||||
positions = list(positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons'))
|
||||
if len(positions) == 0:
|
||||
raise OrderError(error_messages['empty'])
|
||||
if len(position_ids) != len(positions):
|
||||
@@ -804,6 +831,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def expire_orders(sender, **kwargs):
|
||||
eventcache = {}
|
||||
|
||||
@@ -818,6 +846,7 @@ def expire_orders(sender, **kwargs):
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def send_expiry_warnings(sender, **kwargs):
|
||||
eventcache = {}
|
||||
today = now().replace(hour=0, minute=0, second=0)
|
||||
@@ -875,6 +904,7 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def send_download_reminders(sender, **kwargs):
|
||||
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@@ -958,12 +988,17 @@ class OrderChangeManager:
|
||||
'addon_to_required': _('This is an add-on product, please select the base position it should be added to.'),
|
||||
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
|
||||
'subevent_required': _('You need to choose a subevent for the new position.'),
|
||||
'seat_unavailable': _('The selected seat "{seat}" is not available.'),
|
||||
'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'),
|
||||
'seat_required': _('The selected product requires you to select a seat.'),
|
||||
'seat_forbidden': _('The selected product does not allow to select a seat.'),
|
||||
}
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||
SeatOperation = namedtuple('SubeventOperation', ('position', 'seat'))
|
||||
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position',))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat'))
|
||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
|
||||
|
||||
@@ -976,6 +1011,7 @@ class OrderChangeManager:
|
||||
self._committed = False
|
||||
self._totaldiff = 0
|
||||
self._quotadiff = Counter()
|
||||
self._seatdiff = Counter()
|
||||
self._operations = []
|
||||
self.notify = notify
|
||||
self._invoice_dirty = False
|
||||
@@ -993,6 +1029,13 @@ class OrderChangeManager:
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.ItemOperation(position, item, variation))
|
||||
|
||||
def change_seat(self, position: OrderPosition, seat: Seat):
|
||||
if position.seat:
|
||||
self._seatdiff.subtract([position.seat])
|
||||
if seat:
|
||||
self._seatdiff.update([seat])
|
||||
self._operations.append(self.SeatOperation(position, seat))
|
||||
|
||||
def change_subevent(self, position: OrderPosition, subevent: SubEvent):
|
||||
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
@@ -1048,12 +1091,14 @@ class OrderChangeManager:
|
||||
self._totaldiff += -position.price
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.CancelOperation(position))
|
||||
if position.seat:
|
||||
self._seatdiff.subtract([position.seat])
|
||||
|
||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None,
|
||||
subevent: SubEvent = None):
|
||||
subevent: SubEvent = None, seat: Seat = None):
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
@@ -1072,6 +1117,14 @@ class OrderChangeManager:
|
||||
if self.order.event.has_subevents and not subevent:
|
||||
raise OrderError(self.error_messages['subevent_required'])
|
||||
|
||||
seated = item.seat_category_mappings.filter(subevent=subevent).exists()
|
||||
if seated and not seat:
|
||||
raise OrderError(self.error_messages['seat_required'])
|
||||
elif not seated and seat:
|
||||
raise OrderError(self.error_messages['seat_forbidden'])
|
||||
if seat and subevent and seat.subevent_id != subevent:
|
||||
raise OrderError(self.error_messages['seat_subevent_mismatch'].format(seat=seat.name))
|
||||
|
||||
new_quotas = (variation.quotas.filter(subevent=subevent)
|
||||
if variation else item.quotas.filter(subevent=subevent))
|
||||
if not new_quotas:
|
||||
@@ -1082,7 +1135,9 @@ class OrderChangeManager:
|
||||
|
||||
self._totaldiff += price.gross
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent))
|
||||
if seat:
|
||||
self._seatdiff.update([seat])
|
||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat))
|
||||
|
||||
def split(self, position: OrderPosition):
|
||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||
@@ -1090,6 +1145,26 @@ class OrderChangeManager:
|
||||
|
||||
self._operations.append(self.SplitOperation(position))
|
||||
|
||||
def _check_seats(self):
|
||||
for seat, diff in self._seatdiff.items():
|
||||
if diff <= 0:
|
||||
continue
|
||||
if not seat.is_available() or diff > 1:
|
||||
raise OrderError(self.error_messages['seat_unavailable'].format(seat=seat.name))
|
||||
|
||||
if self.event.has_subevents:
|
||||
state = {}
|
||||
for p in self.order.positions.all():
|
||||
state[p] = {'seat': p.seat, 'subevent': p.subevent}
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.SeatOperation):
|
||||
state[op.position]['seat'] = op.seat
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
state[op.position]['subevent'] = op.subevent
|
||||
for v in state.values():
|
||||
if v['seat'] and v['seat'].subevent_id != v['subevent'].pk:
|
||||
raise OrderError(self.error_messages['seat_subevent_mismatch'].format(seat=v['seat'].name))
|
||||
|
||||
def _check_quotas(self):
|
||||
for quota, diff in self._quotadiff.items():
|
||||
if diff <= 0:
|
||||
@@ -1156,7 +1231,7 @@ class OrderChangeManager:
|
||||
raise OrderError(self.error_messages['paid_to_free_exceeded'])
|
||||
|
||||
def _perform_operations(self):
|
||||
nextposid = self.order.positions.aggregate(m=Max('positionid'))['m'] + 1
|
||||
nextposid = self.order.all_positions.aggregate(m=Max('positionid'))['m'] + 1
|
||||
split_positions = []
|
||||
|
||||
for op in self._operations:
|
||||
@@ -1176,6 +1251,17 @@ class OrderChangeManager:
|
||||
op.position.variation = op.variation
|
||||
op.position._calculate_tax()
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SeatOperation):
|
||||
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_seat': op.position.seat.name if op.position.seat else "-",
|
||||
'new_seat': op.seat.name if op.seat else "-",
|
||||
'old_seat_id': op.position.seat.pk if op.position.seat else None,
|
||||
'new_seat_id': op.seat.pk if op.seat else None,
|
||||
})
|
||||
op.position.seat = op.seat
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
|
||||
'position': op.position.pk,
|
||||
@@ -1229,7 +1315,7 @@ class OrderChangeManager:
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate,
|
||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||
positionid=nextposid, subevent=op.subevent
|
||||
positionid=nextposid, subevent=op.subevent, seat=op.seat
|
||||
)
|
||||
nextposid += 1
|
||||
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
|
||||
@@ -1240,6 +1326,7 @@ class OrderChangeManager:
|
||||
'price': op.price.gross,
|
||||
'positionid': pos.positionid,
|
||||
'subevent': op.subevent.pk if op.subevent else None,
|
||||
'seat': op.seat.pk if op.seat else None,
|
||||
})
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
split_positions.append(op.position)
|
||||
@@ -1464,6 +1551,7 @@ class OrderChangeManager:
|
||||
raise OrderError(self.error_messages['not_pending_or_paid'])
|
||||
if check_quotas:
|
||||
self._check_quotas()
|
||||
self._check_seats()
|
||||
self._check_complete_cancel()
|
||||
self._perform_operations()
|
||||
self._recalculate_total_and_payment_fee()
|
||||
@@ -1497,8 +1585,8 @@ class OrderChangeManager:
|
||||
return pprov
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def perform_order(self, event: Event, payment_provider: str, positions: List[str],
|
||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||
sales_channel: str='web'):
|
||||
with language(locale):
|
||||
@@ -1513,6 +1601,7 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
@scopes_disabled()
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None, cancellation_fee=None, try_auto_refund=False):
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
from django.db.models import Max, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.celery_app import app
|
||||
@@ -17,6 +18,7 @@ def build_all_quota_caches(sender, **kwargs):
|
||||
|
||||
|
||||
@app.task
|
||||
@scopes_disabled()
|
||||
def refresh_quota_caches():
|
||||
# Active events
|
||||
active = LogEntry.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
|
||||
57
src/pretix/base/services/seating.py
Normal file
57
src/pretix/base/services/seating.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import CartPosition, Seat
|
||||
|
||||
|
||||
class SeatProtected(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def validate_plan_change(event, subevent, plan):
|
||||
current_taken_seats = set(
|
||||
event.seats.select_related('product')
|
||||
.annotate(has_op=Count('orderposition'))
|
||||
.filter(subevent=subevent, has_op=True)
|
||||
.values_list('seat_guid', flat=True)
|
||||
)
|
||||
new_seats = {
|
||||
ss.guid for ss in plan.iter_all_seats()
|
||||
} if plan else set()
|
||||
leftovers = list(current_taken_seats - new_seats)
|
||||
if leftovers:
|
||||
raise SeatProtected(_('You can not change the plan since seat "{}" is not present in the new plan and is '
|
||||
'already sold.').format(leftovers[0]))
|
||||
|
||||
|
||||
def generate_seats(event, subevent, plan, mapping):
|
||||
current_seats = {
|
||||
s.seat_guid: s for s in
|
||||
event.seats.select_related('product').annotate(has_op=Count('orderposition')).filter(subevent=subevent)
|
||||
}
|
||||
create_seats = []
|
||||
if plan:
|
||||
for ss in plan.iter_all_seats():
|
||||
p = mapping.get(ss.category)
|
||||
if ss.guid in current_seats:
|
||||
seat = current_seats.pop(ss.guid)
|
||||
if seat.product != p:
|
||||
seat.product = p
|
||||
seat.save()
|
||||
else:
|
||||
create_seats.append(Seat(
|
||||
event=event,
|
||||
subevent=subevent,
|
||||
seat_guid=ss.guid,
|
||||
name=ss.name,
|
||||
product=p,
|
||||
))
|
||||
|
||||
for s in current_seats.values():
|
||||
if s.has_op:
|
||||
raise SeatProtected(_('You can not change the plan since seat "{}" is not present in the new plan and is '
|
||||
'already sold.').format(s.name))
|
||||
|
||||
Seat.objects.bulk_create(create_seats)
|
||||
CartPosition.objects.filter(seat__in=[s.pk for s in current_seats.values()]).delete()
|
||||
Seat.objects.filter(pk__in=[s.pk for s in current_seats.values()]).delete()
|
||||
@@ -11,14 +11,13 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.shredder import ShredError
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def export(event: str, shredders: List[str]) -> None:
|
||||
event = Event.objects.get(id=event)
|
||||
@app.task(base=ProfiledEventTask)
|
||||
def export(event: Event, shredders: List[str]) -> None:
|
||||
known_shredders = event.get_data_shredders()
|
||||
|
||||
with NamedTemporaryFile() as rawfile:
|
||||
@@ -63,9 +62,8 @@ def export(event: str, shredders: List[str]) -> None:
|
||||
return cf.pk
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, throws=(ShredError,))
|
||||
def shred(event: str, fileid: str, confirm_code: str) -> None:
|
||||
event = Event.objects.get(id=event)
|
||||
@app.task(base=ProfiledEventTask, throws=(ShredError,))
|
||||
def shred(event: Event, fileid: str, confirm_code: str) -> None:
|
||||
known_shredders = event.get_data_shredders()
|
||||
try:
|
||||
cf = CachedFile.objects.get(pk=fileid)
|
||||
|
||||
@@ -14,10 +14,12 @@ import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.metrics import (
|
||||
pretix_task_duration_seconds, pretix_task_runs_total,
|
||||
)
|
||||
from pretix.base.models import Event
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
@@ -61,6 +63,35 @@ class ProfiledTask(app.Task):
|
||||
return super().on_success(retval, task_id, args, kwargs)
|
||||
|
||||
|
||||
class EventTask(app.Task):
|
||||
def __call__(self, *args, **kwargs):
|
||||
if 'event_id' in kwargs:
|
||||
event_id = kwargs.get('event_id')
|
||||
with scopes_disabled():
|
||||
event = Event.objects.select_related('organizer').get(pk=event_id)
|
||||
del kwargs['event_id']
|
||||
kwargs['event'] = event
|
||||
elif 'event' in kwargs:
|
||||
event_id = kwargs.get('event')
|
||||
with scopes_disabled():
|
||||
event = Event.objects.select_related('organizer').get(pk=event_id)
|
||||
kwargs['event'] = event
|
||||
else:
|
||||
args = list(args)
|
||||
event_id = args[0]
|
||||
with scopes_disabled():
|
||||
event = Event.objects.select_related('organizer').get(pk=event_id)
|
||||
args[0] = event
|
||||
|
||||
with scope(organizer=event.organizer):
|
||||
ret = super().__call__(*args, **kwargs)
|
||||
return ret
|
||||
|
||||
|
||||
class ProfiledEventTask(ProfiledTask, EventTask):
|
||||
pass
|
||||
|
||||
|
||||
class TransactionAwareTask(ProfiledTask):
|
||||
"""
|
||||
Task class which is aware of django db transactions and only executes tasks
|
||||
|
||||
@@ -4,13 +4,14 @@ import os
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
|
||||
OrderPosition,
|
||||
)
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.services.tasks import EventTask, ProfiledTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
||||
from pretix.celery_app import app
|
||||
@@ -57,10 +58,11 @@ def generate_order(order: int, provider: str):
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def generate(model: str, pk: int, provider: str):
|
||||
if model == 'order':
|
||||
return generate_order(pk, provider)
|
||||
elif model == 'orderposition':
|
||||
return generate_orderposition(pk, provider)
|
||||
with scopes_disabled():
|
||||
if model == 'order':
|
||||
return generate_order(pk, provider)
|
||||
elif model == 'orderposition':
|
||||
return generate_orderposition(pk, provider)
|
||||
|
||||
|
||||
class DummyRollbackException(Exception):
|
||||
@@ -165,9 +167,8 @@ def get_tickets_for_order(order, base_position=None):
|
||||
return tickets
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def invalidate_cache(event: int, item: int=None, provider: str=None, order: int=None, **kwargs):
|
||||
event = Event.objects.get(id=event)
|
||||
@app.task(base=EventTask)
|
||||
def invalidate_cache(event: Event, item: int=None, provider: str=None, order: int=None, **kwargs):
|
||||
qs = CachedTicket.objects.filter(order_position__order__event=event)
|
||||
qsc = CachedCombinedTicket.objects.filter(order__event=event)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import requests
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext_noop
|
||||
from django_scopes import scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix import __version__
|
||||
@@ -29,6 +30,7 @@ def run_update_check(sender, **kwargs):
|
||||
|
||||
|
||||
@app.task
|
||||
@scopes_disabled()
|
||||
def update_check():
|
||||
gs = GlobalSettingsObject()
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import sys
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event, User, WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.base.services.tasks import EventTask
|
||||
from pretix.base.signals import periodic_task
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None):
|
||||
event = Event.objects.get(id=event_id)
|
||||
@app.task(base=EventTask)
|
||||
def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None):
|
||||
if user_id:
|
||||
user = User.objects.get(id=user_id)
|
||||
else:
|
||||
@@ -69,6 +69,7 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def process_waitinglist(sender, **kwargs):
|
||||
qs = Event.objects.filter(
|
||||
live=True
|
||||
|
||||
@@ -524,3 +524,20 @@ a ``subevent`` argument which might be none and you are expected to return a lis
|
||||
``pretix.base.timeline.TimelineEvent``, which is a ``namedtuple`` with the fields ``event``, ``subevent``,
|
||||
``datetime``, ``description`` and ``edit_url``.
|
||||
"""
|
||||
|
||||
|
||||
quota_availability = EventPluginSignal(
|
||||
providing_args=['quota', 'result', 'count_waitinglist']
|
||||
)
|
||||
"""
|
||||
This signal allows you to modify the availability of a quota. You are passed the ``quota`` and an
|
||||
``availability`` result calculated by pretix code or other plugins. ``availability`` is a tuple
|
||||
with the first entry being one of the ``Quota.AVAILABILITY_*`` constants and the second entry being
|
||||
the number of available tickets (or ``None`` for unlimited). You are expected to return a value
|
||||
of the same time. The parameter ``count_waitinglists`` specifies whether waiting lists should be taken
|
||||
into account.
|
||||
|
||||
**Warning: Use this signal with great caution, it allows you to screw up the performance of the
|
||||
system really bad.** Also, keep in mind that your response is subject to caching and out-of-date
|
||||
quotas might be used for display (not for actual order processing).
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,7 @@ import hmac
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from .. import metrics
|
||||
|
||||
@@ -15,6 +16,7 @@ def unauthed_response():
|
||||
return response
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def serve_metrics(request):
|
||||
if not settings.METRICS_ENABLED:
|
||||
return unauthed_response()
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.urls import Resolver404, get_script_prefix, resolve
|
||||
from django.utils.translation import get_language
|
||||
from django_scopes import scope
|
||||
|
||||
from pretix.base.models.auth import StaffSession
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
@@ -53,10 +54,11 @@ def contextprocessor(request):
|
||||
ctx['has_domain'] = request.event.organizer.domains.exists()
|
||||
|
||||
if not request.event.testmode:
|
||||
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
|
||||
if complain_testmode_orders is None:
|
||||
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
|
||||
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
|
||||
with scope(organizer=request.organizer):
|
||||
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
|
||||
if complain_testmode_orders is None:
|
||||
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
|
||||
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
|
||||
ctx['complain_testmode_orders'] = complain_testmode_orders
|
||||
else:
|
||||
ctx['complain_testmode_orders'] = False
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
)
|
||||
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -44,3 +47,7 @@ class CheckinListForm(forms.ModelForm):
|
||||
'data-inverse-dependency': '<[name$=all_products]'
|
||||
}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_products': SafeModelMultipleChoiceField,
|
||||
'subevent': SafeModelChoiceField,
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class OrderFilterForm(FilterForm):
|
||||
matching_positions = OrderPosition.objects.filter(
|
||||
Q(order=OuterRef('pk')) & Q(
|
||||
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
||||
| Q(secret__istartswith=u)
|
||||
| Q(secret__istartswith=u) | Q(voucher__code__icontains=u)
|
||||
)
|
||||
).values('id')
|
||||
|
||||
@@ -177,11 +177,9 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
orders = {'code': 'code', 'email': 'email', 'total': 'total',
|
||||
'datetime': 'datetime', 'status': 'status'}
|
||||
|
||||
item = forms.ModelChoiceField(
|
||||
item = forms.ChoiceField(
|
||||
label=_('Products'),
|
||||
queryset=Item.objects.none(),
|
||||
required=False,
|
||||
empty_label=_('All products')
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
@@ -241,12 +239,28 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
elif 'subevent':
|
||||
del self.fields['subevent']
|
||||
|
||||
choices = [('', _('All products'))]
|
||||
for i in self.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))
|
||||
self.fields['item'].choices = choices
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
qs = super().filter_qs(qs)
|
||||
|
||||
if fdata.get('item'):
|
||||
qs = qs.filter(all_positions__item=fdata.get('item'), all_positions__canceled=False).distinct()
|
||||
item = fdata.get('item')
|
||||
if item:
|
||||
if '-' in item:
|
||||
var = item.split('-')[1]
|
||||
qs = qs.filter(all_positions__variation_id=var, all_positions__canceled=False).distinct()
|
||||
else:
|
||||
qs = qs.filter(all_positions__item_id=fdata.get('item'), all_positions__canceled=False).distinct()
|
||||
|
||||
if fdata.get('subevent'):
|
||||
qs = qs.filter(all_positions__subevent=fdata.get('subevent'), all_positions__canceled=False).distinct()
|
||||
|
||||
@@ -6,6 +6,9 @@ from django.urls import reverse
|
||||
from django.utils.translation import (
|
||||
pgettext_lazy, ugettext as __, ugettext_lazy as _,
|
||||
)
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
)
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
@@ -94,6 +97,10 @@ class QuestionForm(I18nModelForm):
|
||||
),
|
||||
'dependency_value': forms.Select,
|
||||
}
|
||||
field_classes = {
|
||||
'items': SafeModelMultipleChoiceField,
|
||||
'dependency_question': SafeModelChoiceField,
|
||||
}
|
||||
|
||||
|
||||
class QuestionOptionForm(I18nModelForm):
|
||||
@@ -159,6 +166,9 @@ class QuotaForm(I18nModelForm):
|
||||
'size',
|
||||
'subevent'
|
||||
]
|
||||
field_classes = {
|
||||
'subevent': SafeModelChoiceField,
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
creating = not self.instance.pk
|
||||
|
||||
@@ -10,7 +10,9 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.forms.widgets import DatePickerWidget
|
||||
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, ItemAddOn, Order, OrderPosition, Seat,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -192,10 +194,16 @@ class OrderPositionAddForm(forms.Form):
|
||||
label=_('Product')
|
||||
)
|
||||
addon_to = forms.ModelChoiceField(
|
||||
OrderPosition.objects.none(),
|
||||
OrderPosition.all.none(),
|
||||
required=False,
|
||||
label=_('Add-on to'),
|
||||
)
|
||||
seat = forms.ModelChoiceField(
|
||||
Seat.objects.none(),
|
||||
required=False,
|
||||
label=_('Seat'),
|
||||
empty_label=_('General admission')
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -241,6 +249,19 @@ class OrderPositionAddForm(forms.Form):
|
||||
else:
|
||||
del self.fields['addon_to']
|
||||
|
||||
self.fields['seat'].queryset = order.event.seats.all()
|
||||
self.fields['seat'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'seat',
|
||||
'data-select2-url': reverse('control:event.seats.select2', kwargs={
|
||||
'event': order.event.slug,
|
||||
'organizer': order.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('General admission')
|
||||
}
|
||||
)
|
||||
self.fields['seat'].widget.choices = self.fields['seat'].choices
|
||||
|
||||
if order.event.has_subevents:
|
||||
self.fields['subevent'].queryset = order.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
@@ -269,6 +290,11 @@ class OrderPositionChangeForm(forms.Form):
|
||||
required=False,
|
||||
empty_label=_('(Unchanged)')
|
||||
)
|
||||
seat = forms.ModelChoiceField(
|
||||
Seat.objects.none(),
|
||||
required=False,
|
||||
empty_label=_('(Unchanged)')
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -312,6 +338,22 @@ class OrderPositionChangeForm(forms.Form):
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
if instance.seat:
|
||||
self.fields['seat'].queryset = instance.order.event.seats.all()
|
||||
self.fields['seat'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'seat',
|
||||
'data-select2-url': reverse('control:event.seats.select2', kwargs={
|
||||
'event': instance.order.event.slug,
|
||||
'organizer': instance.order.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('(Unchanged)')
|
||||
}
|
||||
)
|
||||
self.fields['seat'].widget.choices = self.fields['seat'].choices
|
||||
else:
|
||||
del self.fields['seat']
|
||||
|
||||
choices = [
|
||||
('', _('(Unchanged)'))
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
@@ -149,6 +150,9 @@ class TeamForm(forms.ModelForm):
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_events': SafeModelMultipleChoiceField
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
@@ -177,6 +181,9 @@ class DeviceForm(forms.ModelForm):
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_events': SafeModelMultipleChoiceField
|
||||
}
|
||||
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
@@ -307,3 +314,6 @@ class WebHookForm(forms.ModelForm):
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_events': SafeModelMultipleChoiceField
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class SubEventForm(I18nModelForm):
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
'location',
|
||||
'frontpage_text'
|
||||
'frontpage_text',
|
||||
]
|
||||
field_classes = {
|
||||
'date_from': SplitDateTimeField,
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db.models.functions import Lower
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Item, Voucher
|
||||
@@ -31,10 +32,11 @@ class VoucherForm(I18nModelForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||
'comment', 'max_usages', 'price_mode', 'subevent'
|
||||
'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': SplitDateTimeField,
|
||||
'subevent': SafeModelChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'valid_until': SplitDateTimePickerWidget(),
|
||||
@@ -195,10 +197,11 @@ class VoucherBulkForm(VoucherForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||
'max_usages', 'price_mode', 'subevent'
|
||||
'max_usages', 'price_mode', 'subevent', 'show_hidden_items'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': SplitDateTimeField,
|
||||
'subevent': SafeModelChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'valid_until': SplitDateTimePickerWidget(),
|
||||
|
||||
@@ -42,6 +42,12 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
new_price=money_filter(Decimal(data['new_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.seat':
|
||||
return text + ' ' + _('Position #{posid}: Seat "{old_seat}" changed '
|
||||
'to "{new_seat}".').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_seat=data.get('old_seat'), new_seat=data.get('new_seat'),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.subevent':
|
||||
old_se = str(event.subevents.get(pk=data['old_subevent']))
|
||||
new_se = str(event.subevents.get(pk=data['new_subevent']))
|
||||
|
||||
@@ -4,10 +4,11 @@ from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import get_script_prefix, resolve, reverse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_scopes import scope
|
||||
from hijack.templatetags.hijack_tags import is_hijacked
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
@@ -17,7 +18,7 @@ from pretix.helpers.security import (
|
||||
)
|
||||
|
||||
|
||||
class PermissionMiddleware(MiddlewareMixin):
|
||||
class PermissionMiddleware:
|
||||
"""
|
||||
This middleware enforces all requests to the control app to require login.
|
||||
Additionally, it enforces all requests to "control:event." URLs
|
||||
@@ -34,6 +35,23 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
"user.settings.notifications.off",
|
||||
)
|
||||
|
||||
EXCEPTIONS_2FA = (
|
||||
"user.settings.2fa",
|
||||
"user.settings.2fa.add",
|
||||
"user.settings.2fa.enable",
|
||||
"user.settings.2fa.disable",
|
||||
"user.settings.2fa.regenemergency",
|
||||
"user.settings.2fa.confirm.totp",
|
||||
"user.settings.2fa.confirm.u2f",
|
||||
"user.settings.2fa.delete",
|
||||
"auth.logout",
|
||||
"user.reauth"
|
||||
)
|
||||
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
super().__init__()
|
||||
|
||||
def _login_redirect(self, request):
|
||||
# Taken from django/contrib/auth/decorators.py
|
||||
path = request.build_absolute_uri()
|
||||
@@ -52,19 +70,19 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
def process_request(self, request):
|
||||
def __call__(self, request):
|
||||
url = resolve(request.path_info)
|
||||
url_name = url.url_name
|
||||
|
||||
if not request.path.startswith(get_script_prefix() + 'control'):
|
||||
# This middleware should only touch the /control subpath
|
||||
return
|
||||
return self.get_response(request)
|
||||
|
||||
if hasattr(request, 'organizer'):
|
||||
# If the user is on a organizer's subdomain, he should be redirected to pretix
|
||||
return redirect(urljoin(settings.SITE_URL, request.get_full_path()))
|
||||
if url_name in self.EXCEPTIONS:
|
||||
return
|
||||
return self.get_response(request)
|
||||
if not request.user.is_authenticated:
|
||||
return self._login_redirect(request)
|
||||
|
||||
@@ -78,11 +96,16 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
if url_name not in ('user.reauth', 'auth.logout'):
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
|
||||
and url_name not in self.EXCEPTIONS_2FA:
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer').first()
|
||||
with scope(organizer=None):
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer').first()
|
||||
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 "
|
||||
@@ -104,6 +127,12 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
else:
|
||||
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
|
||||
|
||||
with scope(organizer=getattr(request, 'organizer', None)):
|
||||
r = self.get_response(request)
|
||||
if isinstance(r, TemplateResponse):
|
||||
r = r.render()
|
||||
return r
|
||||
|
||||
|
||||
class AuditLogMiddleware:
|
||||
|
||||
|
||||
@@ -237,6 +237,24 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
A second keyword argument ``request`` will contain the request object.
|
||||
"""
|
||||
|
||||
nav_item = EventPluginSignal(
|
||||
providing_args=['request', 'item']
|
||||
)
|
||||
"""
|
||||
This signal is sent out to include tab links on the settings page of an item.
|
||||
Receivers are expected to return a list of dictionaries. The dictionaries
|
||||
should contain at least the keys ``label`` and ``url``. You should also return
|
||||
an ``active`` key with a boolean set to ``True``, when this item should be marked
|
||||
as active.
|
||||
|
||||
If your linked view should stay in the tab-like context of this page, we recommend
|
||||
that you use ``pretix.control.views.item.ItemDetailMixin`` for your view
|
||||
and your template inherits from ``pretixcontrol/item/base.html``.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
A second keyword argument ``request`` will contain the request object.
|
||||
"""
|
||||
|
||||
event_settings_widget = EventPluginSignal(
|
||||
providing_args=['request']
|
||||
)
|
||||
@@ -261,6 +279,22 @@ styles. It is advisable to set a prefix for your form to avoid clashes with othe
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
subevent_forms = EventPluginSignal(
|
||||
providing_args=['request', 'subevent']
|
||||
)
|
||||
"""
|
||||
This signal allows you to return additional forms that should be rendered on the subevent creation
|
||||
or modification page. You are passed ``request`` and ``subevent`` arguments and are expected to return
|
||||
an instance of a form class that you bind yourself when appropriate. Your form will be executed
|
||||
as part of the standard validation and rendering cycle and rendered using default bootstrap
|
||||
styles. It is advisable to set a prefix for your form to avoid clashes with other plugins.
|
||||
|
||||
``subevent`` can be ``None`` during creation. Before ``save()`` is called, a ``subevent`` property of
|
||||
your form instance will automatically being set to the subevent that has just been created.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
oauth_application_registered = Signal(
|
||||
providing_args=["user", "application"]
|
||||
)
|
||||
|
||||
@@ -145,7 +145,11 @@
|
||||
<a href="{{ nav.url }}" title="{{ nav.title }}" {% if nav.active %}class="active"{% endif %}
|
||||
{% if nav.children %}class="dropdown-toggle" data-toggle="dropdown"{% endif %}>
|
||||
{% if nav.icon %}
|
||||
<span class="fa fa-{{ nav.icon }}"></span>
|
||||
{% if "<svg" in nav.icon %}
|
||||
{{ nav.icon|safe }}
|
||||
{% else %}
|
||||
<span class="fa fa-{{ nav.icon }}"></span>
|
||||
{% endif %}
|
||||
<span class="visible-xs-inline">{{ nav.label }}</span>
|
||||
{% else %}
|
||||
{{ nav.label }}
|
||||
@@ -270,7 +274,11 @@
|
||||
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
|
||||
{% if nav.children %}class="has-children"{% endif %}>
|
||||
{% if nav.icon %}
|
||||
<i class="fa fa-{{ nav.icon }} fa-fw"></i>
|
||||
{% if "<svg" in nav.icon %}
|
||||
{{ nav.icon|safe }}
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ nav.icon }}"></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ nav.label }}
|
||||
</a>
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
{% trans "Bundled products" %}
|
||||
</a>
|
||||
</li>
|
||||
{% for n in extra_nav %}
|
||||
<li {% if n.active %}class="active"{% endif %}>
|
||||
<a href="{{ n.url }}">
|
||||
{{ n.label }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h1>{% trans "Create product" %}</h1>
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if has_plugins > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed with num=quota_overbooked %}
|
||||
A plugin is active that might modify the actual result of this quota from what you see here.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if quota_overbooked > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed with num=quota_overbooked %}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}">
|
||||
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}" {% if position.subevent %}data-subevent="{{ position.subevent.id }}{% endif %}">
|
||||
{% bootstrap_form_errors position.form %}
|
||||
{% if position.custom_error %}
|
||||
<div class="alert alert-danger">
|
||||
@@ -87,20 +87,6 @@
|
||||
<strong>{% trans "Change to" %}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Product" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.item }}
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.itemvar layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.event.has_subevents %}
|
||||
<div class="row">
|
||||
@@ -116,6 +102,34 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if position.seat %}
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Seat" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.seat }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.seat layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Product" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.item }}
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.itemvar layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Price" %}</strong>
|
||||
@@ -182,6 +196,7 @@
|
||||
{% if add_form.subevent %}
|
||||
{% bootstrap_field add_form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field add_form.seat layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -258,6 +258,15 @@
|
||||
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}First scanned: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if line.seat %}
|
||||
<br />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 4.7624999 3.7041668" class="svg-icon">
|
||||
<path
|
||||
style="fill:black"
|
||||
d="m 1.9592032,1.8522629e-4 c -0.21468,0 -0.38861,0.17394000371 -0.38861,0.38861000371 0,0.21466 0.17393,0.38861 0.38861,0.38861 0.21468,0 0.3886001,-0.17395 0.3886001,-0.38861 0,-0.21467 -0.1739201,-0.38861000371 -0.3886001,-0.38861000371 z m 0.1049,0.84543000371 c -0.20823,-0.0326 -0.44367,0.12499 -0.39998,0.40462997 l 0.20361,1.01854 c 0.0306,0.15316 0.15301,0.28732 0.3483,0.28732 h 0.8376701 v 0.92708 c 0,0.29313 0.41187,0.29447 0.41187,0.005 v -1.19115 c 0,-0.14168 -0.0995,-0.29507 -0.29094,-0.29507 l -0.65578,-10e-4 -0.1757,-0.87644 C 2.3042533,0.95300523 2.1890432,0.86500523 2.0641032,0.84547523 Z m -0.58549,0.44906997 c -0.0946,-0.0134 -0.20202,0.0625 -0.17829,0.19172 l 0.18759,0.91054 c 0.0763,0.33956 0.36802,0.55914 0.66042,0.55914 h 0.6015201 c 0.21356,0 0.21448,-0.32143 -0.003,-0.32143 H 2.1954632 c -0.19911,0 -0.36364,-0.11898 -0.41341,-0.34107 l -0.17777,-0.87126 c -0.0165,-0.0794 -0.0688,-0.11963 -0.12557,-0.12764 z"/>
|
||||
</svg>
|
||||
{{ line.seat }}
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}
|
||||
<a href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
|
||||
@@ -86,11 +86,29 @@
|
||||
{% for var in item.all_variations %}
|
||||
<tr class="variation {% if tup.0 %}categorized{% endif %}">
|
||||
<td>{{ var }}</td>
|
||||
<td>{{ var.num.canceled|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num.expired|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num.pending|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num.paid|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num.total|togglesum:request.event.currency }}</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=c&provider={{ item.provider }}">
|
||||
{{ var.num.canceled|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=e&provider={{ item.provider }}">
|
||||
{{ var.num.expired|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=n&provider={{ item.provider }}">
|
||||
{{ var.num.pending|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}-{{ var.id }}&status=p&provider={{ item.provider }}">
|
||||
{{ var.num.paid|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ var.num.total|togglesum:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -58,9 +58,9 @@
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.initialized and not d.api_token %}<del>{% endif %}
|
||||
{% if d.revoked %}<del>{% endif %}
|
||||
{{ d.name }}
|
||||
{% if d.initialized and not d.api_token %}</del>{% endif %}
|
||||
{% if d.revoked %}</del>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
|
||||
@@ -74,7 +74,7 @@
|
||||
{% else %}
|
||||
<em>{% trans "Not yet initialized" %}</em>
|
||||
{% endif %}
|
||||
{% if d.initialized and not d.api_token %}
|
||||
{% if d.revoked %}
|
||||
<span class="label label-danger">{% trans "Revoked" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -434,6 +434,12 @@
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Additional settings" %}</legend>
|
||||
{% for f in plugin_forms %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -192,6 +192,12 @@
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Additional settings" %}</legend>
|
||||
{% for f in plugin_forms %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if subevent.pk %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
|
||||
@@ -11,15 +11,35 @@
|
||||
smartphone or a hardware token generator and that changes on a regular basis.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if settings.PRETIX_OBLIGATORY_2FA %}
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Obligatory usage of two-factor authentication" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<strong>{% trans "This system enforces the usage of two-factor authentication!" %}</strong>
|
||||
</p>
|
||||
{% if not devices %}
|
||||
<p>{% trans "Please set up at least one device below." %}</p>
|
||||
{% elif not user.require_2fa %}
|
||||
<p>{% trans "Please activate two-factor authentication using the button below." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if user.require_2fa %}
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Two-factor status" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<a href="{% url "control:user.settings.2fa.disable" %}" class="btn btn-primary pull-right">
|
||||
{% trans "Disable" %}
|
||||
</a>
|
||||
{% if not settings.PRETIX_OBLIGATORY_2FA %}
|
||||
<a href="{% url "control:user.settings.2fa.disable" %}" class="btn btn-primary pull-right">
|
||||
{% trans "Disable" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{% trans "Two-factor authentication is currently enabled." %}</strong>
|
||||
</p>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.max_usages layout="control" %}
|
||||
{% bootstrap_field form.valid_until layout="control" %}
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
@@ -48,7 +49,6 @@
|
||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="controls">
|
||||
@@ -72,6 +72,7 @@
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
{% bootstrap_field form.tag layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||
</fieldset>
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
<div class="form-group submit-group">
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
{% endif %}
|
||||
{% bootstrap_field form.max_usages layout="control" %}
|
||||
{% bootstrap_field form.valid_until layout="control" %}
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
@@ -50,7 +51,6 @@
|
||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="controls">
|
||||
@@ -74,6 +74,7 @@
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
{% bootstrap_field form.tag layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||
</fieldset>
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,7 @@ urlpatterns = [
|
||||
url(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
|
||||
url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
|
||||
url(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
|
||||
url(r'^seats/select2$', typeahead.seat_select2, name='event.seats.select2'),
|
||||
url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
|
||||
url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
|
||||
name='event.subevent.delete'),
|
||||
|
||||
@@ -25,6 +25,7 @@ from pretix.base.models import (
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle
|
||||
from pretix.base.services.tickets import invalidate_cache
|
||||
from pretix.base.signals import quota_availability
|
||||
from pretix.control.forms.item import (
|
||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||
ItemBundleFormSet, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
||||
@@ -33,7 +34,7 @@ from pretix.control.forms.item import (
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required,
|
||||
)
|
||||
from pretix.control.signals import item_forms
|
||||
from pretix.control.signals import item_forms, nav_item
|
||||
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
@@ -640,7 +641,7 @@ class QuotaView(ChartContainingView, DetailView):
|
||||
]
|
||||
|
||||
sum_values = sum([d['value'] for d in data if d['sum']])
|
||||
s = self.object.size - sum_values if self.object.size is not None else 0
|
||||
s = self.object.size - sum_values if self.object.size is not None else ugettext('Infinite')
|
||||
|
||||
data.append({
|
||||
'label': ugettext('Available quota'),
|
||||
@@ -666,6 +667,17 @@ class QuotaView(ChartContainingView, DetailView):
|
||||
ctx['quota_table_rows'] = list(data)
|
||||
ctx['quota_overbooked'] = sum_values - self.object.size if self.object.size is not None else 0
|
||||
|
||||
ctx['has_plugins'] = False
|
||||
res = (
|
||||
Quota.AVAILABILITY_GONE if self.object.size is not None and self.object.size - sum_values <= 0 else
|
||||
Quota.AVAILABILITY_OK,
|
||||
self.object.size - sum_values if self.object.size is not None else None
|
||||
)
|
||||
for recv, resp in quota_availability.send(sender=self.request.event, quota=self.object, result=res,
|
||||
count_waitinglist=True):
|
||||
if resp != res:
|
||||
ctx['has_plugins'] = True
|
||||
|
||||
ctx['has_ignore_vouchers'] = Voucher.objects.filter(
|
||||
Q(allow_ignore_quota=True) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now())) &
|
||||
@@ -782,6 +794,18 @@ class ItemDetailMixin(SingleObjectMixin):
|
||||
model = Item
|
||||
context_object_name = 'item'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
nav = sorted(
|
||||
sum(
|
||||
(list(a[1]) for a in nav_item.send(self.request.event, request=self.request, item=self.get_object())),
|
||||
[]
|
||||
),
|
||||
key=lambda r: str(r['label'])
|
||||
)
|
||||
ctx['extra_nav'] = nav
|
||||
return ctx
|
||||
|
||||
def get_object(self, queryset=None) -> Item:
|
||||
try:
|
||||
if not hasattr(self, 'object') or not self.object:
|
||||
|
||||
@@ -1236,7 +1236,8 @@ class OrderChange(OrderView):
|
||||
ocm.add_position(item, variation,
|
||||
self.add_form.cleaned_data['price'],
|
||||
self.add_form.cleaned_data.get('addon_to'),
|
||||
self.add_form.cleaned_data.get('subevent'))
|
||||
self.add_form.cleaned_data.get('subevent'),
|
||||
self.add_form.cleaned_data.get('seat'))
|
||||
except OrderError as e:
|
||||
self.add_form.custom_error = str(e)
|
||||
return False
|
||||
@@ -1266,6 +1267,9 @@ class OrderChange(OrderView):
|
||||
if item != p.item or variation != p.variation:
|
||||
ocm.change_item(p, item, variation)
|
||||
|
||||
if p.seat and p.form.cleaned_data['seat'] and p.form.cleaned_data['seat'] != p.seat:
|
||||
ocm.change_seat(p, p.form.cleaned_data['seat'])
|
||||
|
||||
if self.request.event.has_subevents and p.form.cleaned_data['subevent'] and p.form.cleaned_data['subevent'] != p.subevent:
|
||||
ocm.change_subevent(p, p.form.cleaned_data['subevent'])
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
|
||||
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
||||
from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
@@ -32,6 +33,7 @@ from pretix.control.forms.subevents import (
|
||||
SubEventMetaValueForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import subevent_forms
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.control.views.event import MetaDataEditorMixin
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -135,6 +137,16 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
meta_form = SubEventMetaValueForm
|
||||
meta_model = SubEventMetaValue
|
||||
|
||||
@cached_property
|
||||
def plugin_forms(self):
|
||||
forms = []
|
||||
for rec, resp in subevent_forms.send(sender=self.request.event, subevent=self.object, request=self.request):
|
||||
if isinstance(resp, (list, tuple)):
|
||||
forms.extend(resp)
|
||||
else:
|
||||
forms.append(resp)
|
||||
return forms
|
||||
|
||||
def _make_meta_form(self, p, val_instances):
|
||||
if not hasattr(self, '_default_meta'):
|
||||
self._default_meta = self.request.event.meta_data
|
||||
@@ -294,6 +306,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
ctx['cl_formset'] = self.cl_formset
|
||||
ctx['itemvar_forms'] = self.itemvar_forms
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
ctx['plugin_forms'] = self.plugin_forms
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
@@ -347,7 +360,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
def is_valid(self, form):
|
||||
return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() and (
|
||||
all([f.is_valid() for f in self.meta_forms])
|
||||
) and self.cl_formset.is_valid()
|
||||
) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms)
|
||||
|
||||
|
||||
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
|
||||
@@ -361,9 +374,9 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if self.is_valid(form):
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
r = self.form_valid(form)
|
||||
return r
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_object(self, queryset=None) -> SubEvent:
|
||||
try:
|
||||
@@ -384,12 +397,23 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
||||
# TODO: LogEntry?
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.has_changed():
|
||||
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
|
||||
data = {
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
for f in self.plugin_forms:
|
||||
data.update({
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
})
|
||||
self.object.log_action(
|
||||
'pretix.subevent.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
'pretix.subevent.changed', user=self.request.user, data=data
|
||||
)
|
||||
for f in self.plugin_forms:
|
||||
f.subevent = self.object
|
||||
f.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
@@ -416,8 +440,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
form = self.get_form()
|
||||
if self.is_valid(form):
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.subevents', kwargs={
|
||||
@@ -442,7 +465,16 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
messages.success(self.request, pgettext_lazy('subevent', 'The new date has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
self.object = form.instance
|
||||
form.instance.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
|
||||
data = dict(form.cleaned_data)
|
||||
for f in self.plugin_forms:
|
||||
data.update({
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
for k in f.cleaned_data
|
||||
})
|
||||
form.instance.log_action('pretix.subevent.added', data=dict(data), user=self.request.user)
|
||||
|
||||
self.save_formset(form.instance)
|
||||
self.save_cl_formset(form.instance)
|
||||
@@ -452,6 +484,9 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
for f in self.meta_forms:
|
||||
f.instance.subevent = form.instance
|
||||
self.save_meta()
|
||||
for f in self.plugin_forms:
|
||||
f.subevent = form.instance
|
||||
f.save()
|
||||
return ret
|
||||
|
||||
@cached_property
|
||||
@@ -657,7 +692,6 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
|
||||
tz = self.request.event.timezone
|
||||
cnt = 0
|
||||
for rdate in self.get_rrule_set():
|
||||
@@ -685,7 +719,15 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
else None
|
||||
)
|
||||
se.save()
|
||||
se.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
data = dict(form.cleaned_data)
|
||||
for f in self.plugin_forms:
|
||||
data.update({
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
for k in f.cleaned_data
|
||||
})
|
||||
se.log_action('pretix.subevent.added', data=data, user=self.request.user)
|
||||
|
||||
for f in self.meta_forms:
|
||||
if f.cleaned_data.get('value'):
|
||||
@@ -731,6 +773,11 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
i.subevent = se
|
||||
i.save()
|
||||
|
||||
for f in self.plugin_forms:
|
||||
f.is_valid()
|
||||
f.subevent = se
|
||||
f.save()
|
||||
|
||||
cnt += 1
|
||||
|
||||
messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(cnt))
|
||||
@@ -742,7 +789,8 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
self.object = SubEvent(event=self.request.event)
|
||||
|
||||
if self.is_valid(form):
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
return self.form_invalid(form)
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.utils.formats import get_format
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
|
||||
from pretix.base.models import Organizer, User
|
||||
from pretix.base.models import Order, Organizer, SubEvent, User, Voucher
|
||||
from pretix.control.forms.event import EventWizardCopyForm
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.helpers.daterange import daterange
|
||||
@@ -67,6 +67,32 @@ def serialize_event(e):
|
||||
}
|
||||
|
||||
|
||||
def serialize_order(o):
|
||||
return {
|
||||
'type': 'order',
|
||||
'event': str(o.event),
|
||||
'title': _('Order {}').format(str(o.code)),
|
||||
'url': reverse('control:event.order', kwargs={
|
||||
'event': o.event.slug,
|
||||
'organizer': o.event.organizer.slug,
|
||||
'code': o.code
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def serialize_voucher(v):
|
||||
return {
|
||||
'type': 'voucher',
|
||||
'event': str(v.event),
|
||||
'title': _('Voucher {}').format(str(v.code)),
|
||||
'url': reverse('control:event.voucher', kwargs={
|
||||
'event': v.event.slug,
|
||||
'organizer': v.event.organizer.slug,
|
||||
'voucher': v.pk
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def event_list(request):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
@@ -132,6 +158,28 @@ def nav_context_list(request):
|
||||
if query:
|
||||
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
|
||||
|
||||
if query:
|
||||
qs_orders = Order.objects.filter(code__icontains=query).select_related('event', 'event__organizer')
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_orders = qs_orders.filter(
|
||||
Q(event__organizer_id__in=request.user.teams.filter(
|
||||
all_events=True, can_view_orders=True).values_list('organizer', flat=True))
|
||||
| Q(event_id__in=request.user.teams.filter(
|
||||
can_view_orders=True).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
qs_vouchers = Voucher.objects.filter(code__icontains=query).select_related('event', 'event__organizer')
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_vouchers = qs_vouchers.filter(
|
||||
Q(event__organizer_id__in=request.user.teams.filter(
|
||||
all_events=True, can_view_vouchers=True).values_list('organizer', flat=True))
|
||||
| Q(event_id__in=request.user.teams.filter(
|
||||
can_view_vouchers=True).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
else:
|
||||
qs_vouchers = Voucher.objects.none()
|
||||
qs_orders = Order.objects.none()
|
||||
|
||||
show_user = not query or (
|
||||
query and request.user.email and query.lower() in request.user.email.lower()
|
||||
) or (
|
||||
@@ -146,15 +194,20 @@ def nav_context_list(request):
|
||||
serialize_orga(e) for e in qs_orga[offset:offset + (pagesize if query else 5)]
|
||||
] + [
|
||||
serialize_event(e) for e in qs_events.select_related('organizer')[offset:offset + (pagesize if query else 5)]
|
||||
] + [
|
||||
serialize_order(e) for e in qs_orders[offset:offset + (pagesize if query else 5)]
|
||||
] + [
|
||||
serialize_voucher(e) for e in qs_vouchers[offset:offset + (pagesize if query else 5)]
|
||||
]
|
||||
|
||||
if show_user and organizer:
|
||||
try:
|
||||
organizer = serialize_orga(Organizer.objects.get(pk=organizer))
|
||||
organizer = Organizer.objects.get(pk=organizer)
|
||||
except Organizer.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if request.user.has_organizer_permission(organizer, request):
|
||||
if request.user.has_organizer_permission(organizer, request=request):
|
||||
organizer = serialize_orga(organizer)
|
||||
if organizer in results:
|
||||
results.remove(organizer)
|
||||
results.insert(1, organizer)
|
||||
@@ -168,6 +221,46 @@ def nav_context_list(request):
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required("can_view_orders")
|
||||
def seat_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
if request.event.has_subevents:
|
||||
try:
|
||||
qs = request.event.subevents.get(active=True, pk=request.GET.get('subevent', 0)).free_seats
|
||||
except SubEvent.DoesNotExist:
|
||||
qs = request.event.seats.none()
|
||||
else:
|
||||
qs = request.event.free_seats
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=query) | Q(seat_guid__icontains=query)
|
||||
).order_by('name').select_related('product', 'subevent')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': e.pk,
|
||||
'text': '{} ({})'.format(e.name, str(e.product)),
|
||||
'product': e.product_id,
|
||||
'event': str(e.subevent) if e.subevent else ''
|
||||
|
||||
}
|
||||
for e in qs[offset:offset + pagesize]
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required(None)
|
||||
def subevent_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
|
||||
6
src/pretix/icons/seat.svg
Normal file
6
src/pretix/icons/seat.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 4.7624999 3.7041668">
|
||||
<path
|
||||
style="fill:black"
|
||||
d="m 1.9592032,1.8522629e-4 c -0.21468,0 -0.38861,0.17394000371 -0.38861,0.38861000371 0,0.21466 0.17393,0.38861 0.38861,0.38861 0.21468,0 0.3886001,-0.17395 0.3886001,-0.38861 0,-0.21467 -0.1739201,-0.38861000371 -0.3886001,-0.38861000371 z m 0.1049,0.84543000371 c -0.20823,-0.0326 -0.44367,0.12499 -0.39998,0.40462997 l 0.20361,1.01854 c 0.0306,0.15316 0.15301,0.28732 0.3483,0.28732 h 0.8376701 v 0.92708 c 0,0.29313 0.41187,0.29447 0.41187,0.005 v -1.19115 c 0,-0.14168 -0.0995,-0.29507 -0.29094,-0.29507 l -0.65578,-10e-4 -0.1757,-0.87644 C 2.3042533,0.95300523 2.1890432,0.86500523 2.0641032,0.84547523 Z m -0.58549,0.44906997 c -0.0946,-0.0134 -0.20202,0.0625 -0.17829,0.19172 l 0.18759,0.91054 c 0.0763,0.33956 0.36802,0.55914 0.66042,0.55914 h 0.6015201 c 0.21356,0 0.21448,-0.32143 -0.003,-0.32143 H 2.1954632 c -0.19911,0 -0.36364,-0.11898 -0.41341,-0.34107 l -0.17777,-0.87126 c -0.0165,-0.0794 -0.0688,-0.11963 -0.12557,-0.12764 z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
|
||||
"POT-Creation-Date: 2019-06-25 09:08+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -250,7 +250,7 @@ msgstr[3] ""
|
||||
msgstr[4] ""
|
||||
msgstr[5] ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:201
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:207
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
@@ -410,78 +410,83 @@ msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "Mo"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Tu"
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "We"
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Th"
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Fr"
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgid "Sa"
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
|
||||
"POT-Creation-Date: 2019-06-25 09:08+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -241,7 +241,7 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:201
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:207
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
@@ -401,78 +401,83 @@ msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "Mo"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Tu"
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "We"
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Th"
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Fr"
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgid "Sa"
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
|
||||
"POT-Creation-Date: 2019-06-25 09:08+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -243,7 +243,7 @@ msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr[2] ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:201
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:207
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
@@ -403,78 +403,83 @@ msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "Mo"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Tu"
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "We"
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Th"
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Fr"
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgid "Sa"
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user