Compare commits

...

68 Commits

Author SHA1 Message Date
Raphael Michel
c751a180a4 Bump version 2017-12-05 18:51:40 +01:00
Raphael Michel
3ee6a0cf6f German translation: Consistent casing of Check-in 2017-12-04 20:58:59 +01:00
Raphael Michel
0c23f36e36 pretixdroid: Search should also return unpaid tickets 2017-12-04 20:35:50 +01:00
Raphael Michel
40b84fd676 Docker: Workaround for bug in reportlab 3.4.0 2017-12-04 20:35:32 +01:00
Raphael Michel
e5e1d3b8e5 Fix German translation file 2017-12-04 18:39:22 +01:00
Jakob Schnell
9bba225157 Add mention of CoC in README.md (#696)
* mention CoC in README.md

According to the discussion in #689, the Code of Conduct gets directly
referenced in the README-file, instead of linking to the
CODE_OF_CONDUCT-file

* undo whitespace-change
2017-12-04 18:24:18 +01:00
Raphael Michel
196c615f53 Update German translation 2017-12-04 18:21:24 +01:00
Raphael Michel
353dce789d Fix #515 -- Add check-in lists (#693)
* Data model and migration

* Some backwards compatibility

* CRUD for checkin lists

* Show and perform checkins

* Correct numbers in table and dashboard widget

* event creation and cloning

* Allow to link specific exports and pass options per query

* Play with the CSV export

* PDF export

* Collapse exports by default

* Improve PDF exporter

* Addon stuff

* Subevent stuff, pretixdroid tests

* pretixdroid tests

* Add CRUD API

* Test compatibility

* Fix test

* DB-independent sorting behavior

* Add CRUD and coyp tests

* Re-enable pretixdroid plugin

* pretixdroid config

* Tests & fixes
2017-12-04 18:12:23 +01:00
Raphael Michel
f1be7ed69d API docs: Add version change for quotas 2017-12-04 16:50:19 +01:00
Raphael Michel
37146c1e10 Dashboards: Link tiles with subevent 2017-12-04 16:24:42 +01:00
Raphael Michel
feba94547a Dashboard and product list: Respect availability dates of products 2017-12-04 16:24:07 +01:00
Raphael Michel
1b82b64a0a PDF ticket output: Fix TypeError on 'content': null 2017-12-04 15:48:21 +01:00
Raphael Michel
0f8cd31e0a Fixed incorrect typo-check suggestion
Thanks @freakboy3742 for reporting
see https://twitter.com/freakboy3742/status/936764146074316801
2017-12-04 15:40:06 +01:00
Raphael Michel
c351a5cf72 Ticket preview: Locale and invoice address 2017-12-04 13:56:02 +01:00
Raphael Michel
98a58779ad Consistent markdown parsing, docs on markdown and display settings 2017-12-03 15:19:15 +01:00
Raphael Michel
1aef721794 User documentation: More information on payments 2017-12-01 11:19:14 +01:00
Raphael Michel
7373d958a5 User documentation: subevents 2017-12-01 10:26:43 +01:00
Raphael Michel
37fdbf25ff User docs: Add FAQ and domain pages 2017-11-30 12:16:17 +01:00
Jakob Schnell
1580709c97 Fix #689 -- Add CODE_OF_CONDUCT.md (#695)
fixes #689
2017-11-30 12:01:24 +01:00
Raphael Michel
c7f1f67ee9 Widget: Fix incorrect duplicate formatting of float, loosing precision 2017-11-30 07:52:17 +01:00
Raphael Michel
8d90c9e03a Add icon columns to product overview 2017-11-26 14:19:02 +01:00
Raphael Michel
40818ae853 GitLab config 2017-11-25 19:18:40 +01:00
Raphael Michel
364ea9ca29 [SECURITY] Respect session timeout in API 2017-11-25 19:18:40 +01:00
Raphael Michel
f6b1bd9fe8 [SECURITY] Fix handling of session timeouts 2017-11-25 19:18:40 +01:00
Raphael Michel
30c7319811 Remvoe obsolete code 2017-11-25 19:18:40 +01:00
Jakob Schnell
41fbf362fa fix typos in doc signal (#687) 2017-11-25 14:31:09 +01:00
Raphael Michel
e8867d0fbc Allow searching organizes by name 2017-11-25 14:27:35 +01:00
Raphael Michel
3bf8aad127 Update German translation 2017-11-24 17:06:16 +01:00
Raphael Michel
fb5354c3cd Fix AttributeError in redemption view 2017-11-24 10:04:04 +01:00
Raphael Michel
a62105fa28 Stripe Elements: Fix incorrect JavaScript and display loading indicator 2017-11-24 09:42:26 +01:00
Raphael Michel
65592dc42d Checkout step Questions: Automatically focus first field 2017-11-24 09:35:47 +01:00
Raphael Michel
3a345c0d7f Stripe webhook: Schedule retry on lock timeout 2017-11-24 09:32:41 +01:00
Raphael Michel
3da11e615f Fix TypeError when accessing a product list without a subevent 2017-11-24 09:29:05 +01:00
Raphael Michel
3eb87a878a Fix invoice API test after addition of attendee names 2017-11-24 09:28:31 +01:00
Raphael Michel
91ed869dba Correctly assign positions of newly created products 2017-11-23 20:55:36 +01:00
Raphael Michel
bd5d0093ef Allow adding question answers to ticket layouts 2017-11-23 20:55:03 +01:00
Raphael Michel
bd7ba09f10 Ticket PDF editor: Correct save button handling in Firefox 2017-11-23 18:52:42 +01:00
Raphael Michel
851b6a837f Add attendee names to invoices 2017-11-23 18:44:41 +01:00
Raphael Michel
d8064d1567 Add email_filter signal 2017-11-23 18:15:41 +01:00
Raphael Michel
046edd5a86 PDF ticket editor: Adjust CSS for very small papers 2017-11-22 14:57:15 +01:00
Raphael Michel
8d8eb5d13b Enforce step order of event creation wizard 2017-11-22 12:09:52 +01:00
Raphael Michel
2a3adb135b Fix position of "Optional" label on small screens 2017-11-22 12:09:52 +01:00
Raphael Michel
b0c4c62668 Expose product mapping in event_copy_data signal 2017-11-22 12:09:52 +01:00
ari-s
a08cb3b8e4 fix "typo" Referrer-Options -> Referrer-Options (#685)
this header was probably meant - at least it's the one that's actually used in the wild.
2017-11-20 08:29:23 +01:00
Raphael Michel
943d61dee9 Update German translation 2017-11-16 22:26:26 +01:00
Raphael Michel
d22427f578 Implement progress indicator during checkout (#677)
* Implement progress indicator during checkout

* Do not extend bars to the edge

* Wording

* Add a bit more margin
2017-11-16 21:24:55 +01:00
Ture Gjørup
e4167380b9 API: Write methods for quotas (#657)
* MKBDIGI-183: Added quotas API write methods

* MKBDIGI-183: Fixed code formatting

* MKBDIGI-183: Added test for permission requirements

* MKBDIGI-183: Documentation corrections

* MKBDIGI-183: Removed redundant create/update locks

* MKBDIGI-183: Added quota validation to check that items and variations corresponds to each other

* MKBDIGI-183: Added quota validation to check that item belong to the same event as the endpoint

* MKBDIGI-183: Added subevent validation to check that subevent belong to the same event as the endpoint

* MKBDIGI-183: Added subevent validation to check that subevent is null for non-series events

* MKBDIGI-183: Changed validation error text

* MKBDIGI-183: Added logging for subevents

* MKBDIGI-183: Fixed code formatting

* MKBDIGI-183: Fixed validation error in API test

* MKBDIGI-183: Fixed documentation errors

* MKBDIGI-183: Fixed typos in validation messages

* MKBDIGI-183: Refactored validation loop vars check

* MKBDIGI-183: Updated error strings in test assersions

* MKBDIGI-183: Fixed logging for API quota update to account changing subevents
2017-11-16 18:39:10 +01:00
Jakob Schnell
445afcc50c fix english typo (#684) 2017-11-16 18:18:58 +01:00
Jakob Schnell
e0e37d9a2d fix german typo (#683) 2017-11-16 18:18:37 +01:00
Raphael Michel
d94faae5af Add subevent to bulk voucher form 2017-11-15 14:53:46 +01:00
Tobias Kunze
e7f38abd77 Use rel="noopener" with target="_blank". (#682)
Required reading: https://mathiasbynens.github.io/rel-noopener/
2017-11-13 18:52:15 +01:00
Raphael Michel
01585877d7 Fix retrying Stripe checkout payments 2017-11-11 22:38:49 +01:00
Raphael Michel
8baa800e30 Fix incorrect encoding of bank account in emails 2017-11-11 22:23:38 +01:00
Raphael Michel
84b2c24f9f Fix language of dates on PDF tickets 2017-11-11 22:13:28 +01:00
Raphael Michel
3fc8ccf8be Add additional voucher test case 2017-11-11 22:09:03 +01:00
Raphael Michel
b294f1a854 Update German translation 2017-11-11 22:07:55 +01:00
Raphael Michel
06725441a1 Fix localization of payment reminder email 2017-11-11 22:00:43 +01:00
Raphael Michel
aa40a27558 Make event slug helptext more accurate 2017-11-11 21:50:30 +01:00
Raphael Michel
f5958a7ff2 Fix TypeError 2017-11-07 18:18:57 +01:00
Raphael Michel
f3221e6e76 Allow attaching invoices to emails 2017-11-07 17:53:28 +01:00
Raphael Michel
7649fa11d3 Fix logging language of expiry mails 2017-11-06 17:18:26 +01:00
Tobias Kunze
98aa70c9ce Remove minor typos in documentation (#673) 2017-11-05 12:54:05 +01:00
Raphael Michel
a3be5c9616 Fix typo in documentation 2017-11-04 20:52:30 +01:00
Raphael Michel
decc8b9141 Fix TypeError on retrying stripe sofort payments 2017-11-04 15:03:43 +01:00
Raphael Michel
1c7df4d9f7 Manually merge changes from #672 (thanks @koebi!) 2017-11-04 11:29:02 +01:00
Jakob Schnell
b94f307379 Fix typos in informal german translation (#671)
Critical points here are the gendering change of "Studenten" into
"Studierende" and "Studentenausweis" into "Studierendenausweis".
2017-11-04 11:23:30 +01:00
Raphael Michel
33d9e35667 Fix typos (thanks @koebi) 2017-11-04 11:18:57 +01:00
Raphael Michel
ad9a3e01de Bump release to dev 2017-11-03 12:33:50 +01:00
162 changed files with 6709 additions and 2626 deletions

View File

@@ -8,6 +8,8 @@ tests:
- XDG_CACHE_HOME=/cache bash .travis.sh tests
tags:
- python3
except:
- pypi
pypi:
stage: release
script:
@@ -22,7 +24,7 @@ pypi:
tags:
- python3
only:
- release
- pypi
artifacts:
paths:
- src/dist/

5
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,5 @@
Code of Conduct
===============
We have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html)
in place that applies to all project contributions, including issues, pull requests, etc.

View File

@@ -40,6 +40,11 @@ Contributing
If you want to contribute to pretix, please read the `developer documentation`_
in our documentation. If you have any further questions, please do not hesitate to ask!
Code of Conduct
---------------
We have a `Code of Conduct`_ in place that applies to all project contributions,
including issues, pull requests, etc.
License
-------
The code in this repository is published under the terms of the Apache License.
@@ -50,5 +55,6 @@ AUTHORS file for a list of all the awesome folks who contributed to this project
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html
.. _Code of Conduct: https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html
.. _pretix.eu: https://pretix.eu
.. _blog: https://pretix.eu/about/en/blog/

View File

@@ -23,6 +23,7 @@ autostart=true
autorestart=true
priority=5
user=pretixuser
environment=HOME=/pretix
[program:pretixtask]
command=/usr/local/bin/pretix taskworker

View File

@@ -239,6 +239,8 @@ Restarting the service can take a few seconds, especially if the update requires
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
version, if you want to.
.. _`docker_plugininstall`:
Install a plugin
----------------

View File

@@ -177,7 +177,7 @@ For background tasks we need a second service ``/etc/systemd/system/pretix-worke
[Install]
WantedBy=multi-user.target
You can now run the following comamnds to enable and start the services::
You can now run the following commands to enable and start the services::
# systemctl daemon-reload
# systemctl enable pretix-web pretix-worker
@@ -213,7 +213,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
ssl_certificate /path/to/cert.chain.pem;
ssl_certificate_key /path/to/key.pem;
add_header Referrer-Options same-origin;
add_header Referrer-Policy same-origin;
add_header X-Content-Type-Options nosniff;
location / {
@@ -276,6 +276,8 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
# systemctl restart pretix-web pretix-worker
.. _`manual_plugininstall`:
Install a plugin
----------------

View File

@@ -0,0 +1,238 @@
Check-in lists
==============
Resource description
--------------------
You can create check-in lists that you can use e.g. at the entrance of your event to track who is coming and if they
actually bought a ticket.
You can create multiple check-in lists to separate multiple parts of your event, for example if you have separate
entries for multiple ticket types. Different check-in lists are completely independent: If a ticket shows up on two
lists, it is valid once on every list. This might be useful if you run a festival with festival passes that allow
access to every or multiple performances as well as tickets only valid for single performances.
The check-in list resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the check-in list
name string The internal name of the check-in list
all_products boolean If ``True``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
limit_products list of integers List of item IDs to include in this list.
subevent integer ID of the date inside an event series this list belongs to (or ``null``).
position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only).
===================================== ========================== =======================================================
.. versionchanged:: 1.10
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
Returns a list of all check-in lists within a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/ 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": 1,
"name": "Default list",
"checkin_count": 123,
"position_count": 456,
"all_products": true,
"limit_products": [],
"subevent": null
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query integer subevent: Only return check-in lists of the sub-event with the given ID
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(id)/
Returns information on one check-in list, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Default list",
"checkin_count": 123,
"position_count": 456,
"all_products": true,
"limit_products": [],
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the check-in list to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
Creates a new check-in list.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"name": "VIP entry",
"all_products": false,
"limit_products": [1, 2],
"subevent": null
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"name": "VIP entry",
"checkin_count": 0,
"position_count": 0,
"all_products": false,
"limit_products": [1, 2],
"subevent": null
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
:param event: The ``slug`` field of the event to create a list for
:statuscode 201: no error
:statuscode 400: The list could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(id)/
Update a check-in list. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be resetted 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 and the ``checkin_count`` and ``position_count``
fields.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"name": "Backstage",
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"name": "Backstage",
"checkin_count": 23,
"position_count": 42,
"all_products": false,
"limit_products": [1, 2],
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the list to modify
:statuscode 200: no error
:statuscode 400: The list could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
Delete a check-in list. Note that this also deletes the information on all checkins performed via this list.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/checkinlist/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the check-in list to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -15,4 +15,5 @@ Resources and endpoints
orders
invoices
vouchers
checkinlists
waitinglist

View File

@@ -125,7 +125,7 @@ Endpoints
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
``is_cancellation`` will be returned.
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
:query string refers: If set, only invoices refering to the given invoice will be returned.
:query string refers: If set, only invoices referring to the given invoice will be returned.
:query string locale: If set, only invoices with the given locale will be returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
``nr`` (equals to ``number``). Default: ``nr``

View File

@@ -24,7 +24,7 @@ email string The customer em
locale string The locale used for communication with this customer
datetime datetime Time of order creation
expires datetime The order will expire, if it is still pending by this time
payment_date date Date of payment receival
payment_date date Date of payment receipt
payment_provider string Payment provider used for this order
payment_fee money (string) Payment fee included in this order's total
payment_fee_tax_rate decimal (string) Tax rate applied to the payment fee
@@ -94,7 +94,7 @@ Order position resource
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the order positon
id integer Internal ID of the order position
code string Order code of the order the position belongs to
positionid integer Number of the position within the order
item integer ID of the purchased item
@@ -373,10 +373,10 @@ Order endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downlodas are not available for this order at this time. The response content will
**or** downloads are not available for this order at this time. The response content will
contain more details.
:statuscode 404: The requested order or output provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting vor a few
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
@@ -731,7 +731,7 @@ Order position endpoints
Download tickets for one order position, identified by its internal ID.
Depending on the chosen output, the response might be a ZIP file, PDF file or something else. The order details
response contains a list of output options for this partictular order position.
response contains a list of output options for this particular order position.
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
configuration downloads might be only unavailable for add-on products or non-admission products.
@@ -763,8 +763,8 @@ Order position endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downlodas are not available for this order position at this time. The response content will
**or** downloads are not available for this order position at this time. The response content will
contain more details.
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting vor a few
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.

View File

@@ -4,7 +4,7 @@ Quotas
Resource description
--------------------
Questions define how many times an item can be sold.
Quotas define how many times an item can be sold.
The quota resource contains the following public fields:
.. rst-class:: rest-resource-table
@@ -20,6 +20,10 @@ variations list of integers List of item va
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
===================================== ========================== =======================================================
.. versionchanged:: 1.10
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
Endpoints
---------
@@ -106,6 +110,131 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/quotas/
Creates a new quota
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/quotas/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
:param event: The ``slug`` field of the event to create a quota for
:statuscode 201: no error
:statuscode 400: The quota could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/quotas/(id)/
Update a quota. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/quotas/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"name": "New Ticket Quota",
"size": 100,
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"name": "New Ticket Quota",
"size": 100,
"items": [
1,
2
],
"variations": [
1,
2
],
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the quota rule to modify
:statuscode 200: no error
:statuscode 400: The quota could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/quota/(id)/
Delete a quota. Note that if you delete a quota the items the quota acts on might no longer be available for sale.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/quotas/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the quotas to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/quotas/(id)/availability/
Returns availability information on one quota, identified by its ID.

View File

@@ -162,7 +162,7 @@ Endpoints
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/taxrules/(id)/
Update a tax rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:

View File

@@ -234,9 +234,8 @@ Endpoints
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
Update a voucher. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
her.
You can change all fields of the resource except the ``id`` and ``redeemed`` fields.
@@ -283,7 +282,7 @@ her.
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the tax rule to modify
:param id: The ``id`` field of the voucher to modify
:statuscode 200: no error
:statuscode 400: The voucher could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
@@ -311,7 +310,7 @@ her.
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the tax rule to delete
:param id: The ``id`` field of the voucher to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -11,7 +11,7 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data
:members: periodic_task, event_live_issues, event_copy_data, email_filter
Order events
""""""""""""

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,3 +1,5 @@
.. _event_create:
Creating an event
=================
@@ -26,7 +28,7 @@ is useful if you have a large number of events that are very similar to each oth
(i.e. users should be able to buy tickets for multiple events at the same time). Those single events can differ in
available products, quotas, prices and some meta information, but most settings need to be the same for all of them.
We recommend to use this feature only if you really know that you need it and if you really run a lot of events, not if
you run e.g. a yearly conference.
you run e.g. a yearly conference. You can read more on this feature :ref:`here <subevents>`.
Once you set these values, you can procede to the next step:

View File

@@ -0,0 +1,42 @@
Display settings
================
The settings at "Settings" → "Display" allow you to customize the appearance of your ticket shop.
.. thumbnail:: ../../screens/event/settings_display.png
:align: center
:class: screenshot
The upper part of the page contains settings that you always need to set specifically for your event. Those are
currently::
Logo image
This logo will be shown as a banner above your shop. If you set it, the event name and date will no longer be
displayed by the shop, so we suggest to include them in the image yourself. The maximal height of the image is
120 pixels and if you want to use the full width, make your image 1140 pixels wide. If the user's screen is
smaller, the logo will be scaled down automatically, so it should still be legigible at smaller sizes.
Frontpage text
This text will be shown on the front page of your ticket shop, above the list of products. You can use it to explain
your product types, give more information on the event or for other general notices.
You can use :ref:`Markdown syntax <markdown-guide>` in this field.
Show variations of a product expanded by default
If this is not checked, a product with variations will be shown as one row in the show by default and will expand
into multiple rows once it is clicked on. With this box checked, the variations will be shown as multiple rows
right from the beginning.
The lower part of the page contains settings that you can **either** set on organizer-level for all your events **or**
override for this single event individually. Those are:
Primary color
This color will be used for links, buttons, and other design elements throughout your shop and emails sent to your
customers. We suggest not choosing something to light, since text in that color should be readable on a white
background and white text should be readable on a background of this color.
Font
Choose one of multiple fonts to use for your web shop.
.. note:: Both the color and font settings can take a few seconds up to a few minutes before they become active on your
shop.

View File

@@ -0,0 +1,20 @@
Configuring plugins
===================
Plugins are optional parts of pretix that can be installed to extend the available functionality and that can be turned
on or off completely for every event. For your event, a number of plugins might be active already, but you can unlock
even more functionality by going to "Settings" → "Plugins" and enable more of them, if you need.
.. thumbnail:: ../../screens/event/settings_plugins.png
:align: center
:class: screenshot
For each plugin, you will find a short description as well as an Enable/Disable button. The pretix website has
`an overview`_ of available plugins and more details of them. If you are on the pretix.eu hosted service, look for
the "pretix Hosted" badge in the plugin list to learn which ones are supported there.
If you are running pretix on your own server, refer to the installation manual of your installation type to learn
how to install additional plugins (:ref:`manual <manual_plugininstall>` or :ref:`Docker <docker_plugininstall>`).
.. _an overview: https://pretix.eu/about/en/plugins

View File

@@ -0,0 +1,11 @@
Configuring an event
====================
.. toctree::
:maxdepth: 2
subevents
../payments/index
plugins
display
taxes

View File

@@ -0,0 +1,111 @@
.. _subevents:
Event series
============
During creation of a new event, you can choose that you want to create this event as an event series.
By event series, we mean a group of events that are similar in their structure and that you want to
sell within a single shop. An event series consists of **dates**. Each date represents one "event"
within the series.
For example, we think good examples to use the event series feature are:
* A theater or theater group that shows the same play on five evenings.
* A band on tour that hosts the same show in different locations.
* A workshop that is given multiple times in different locations or at different times.
We **don't** think that the feature is well-suited for events like the following:
* Event series distributed over a large timescale like annual conferences. We suggest using multiple events in this
case. You can avoid having to configure everything twice since you can copy settings from an existing event during
creation of the new event.
* Multiple parts of a conference or festival (e.g. different days) if a significant number of attendees will visit
more than one of them. We suggest just using different products in this case.
When using an event series, the single dates of the series are using the same settings in most places. They can
**only** differ in the following aspects:
* They can have different date, time, and location parameters.
* They can use different text on the shop front page.
* They can have different prices for the various products.
* They always have distinct quotas, which allows you to assign different amounts of tickets or to enable or disable
some products completely.
* They can have different rules for check-in.
Therefore, if your events are likely to need more different settings, this is probably not the feature for you. The
benefits of using event series, on the other hand, are:
* You only need to set most settings once, as the multiple dates live in the same shop.
* Your customers can build mixed orders, i.e. they can order tickets for multiple dates at once.
Creating and modifying dates in the series
------------------------------------------
Click on "Dates" in the left navigation menu of your event. This page shows you the list of currently existing event
dates and allows you to create, edit, clone and delete them.
If "Dates" is missing from the navigation menu, you have insufficient permission or your event has not been set up as
an event series and you need to create a new event.
.. thumbnail:: ../../screens/event/subevent_list.png
:align: center
:class: screenshot
If you click on one of them or create a new one, you will see the following form:
.. thumbnail:: ../../screens/event/subevent_create.png
:align: center
:class: screenshot
Here, you can make changes to the following fields, most of which are optional:
Name
This is the public name of your date. It should be descriptive enough to tell the user which date to select in
a calendar.
Active
This date will only show up for customers if you check this box. In this sense, it corresponds to the "live" setting
of events.
Event start time
The date and time that this date starts at.
Event end time
The date and time this date ends at.
Location
This is the location of your date in a human-readable format. We will show this on the ticket shop frontpage, but
it might also be used e.g. in Wallet tickets.
Admission time
The admission date and time to show on the ticket shop page or on the tickets.
Frontpage text
A text to show on the front page of the ticket shop for this date.
Start of presale
If you set this, no ticket will be sold before the time you set. If you set this on event series level as well,
both dates must be in the past for the tickets to be available.
End of presale
If you set this, no ticket will be sold after the time you set. If you set this on event series level as well,
both dates must be in the future for the tickets to be available.
Quotas
As for all events, no tickets will be available unless there is a quota created for them that specifies the number
of tickets available. You can create multiple quotas that are assinged to this date directly from this interface.
Item prices
This is a table of all products configured for your shop. If you want, you can enter a new price for each one of them
in the right column to make them cheaper or more expensive for this date. If you leave a field empty, the price will
follow the product's default price.

View File

@@ -1,5 +1,7 @@
Tax rules
=========
.. _taxes:
Configuring taxes
=================
In most countries, you will be required to pay some form of sales tax for your event tickets. If you don't know about
the exact rules, you should consult a professional tax consultant right now.

51
doc/user/faq.rst Normal file
View File

@@ -0,0 +1,51 @@
FAQ and Troubleshooting
=======================
How can I test my shop before taking it live?
---------------------------------------------
There are multiple ways to do this.
First, you could just create some orders in your real shop and cancel/refund them later. If you don't want to process
real payments for the tests, you can either use a "manual" payment method like bank transfer and just mark the orders
as paid with the button in the backend, or if you want to use e.g. Stripe, you can configure pretix to use your keys
for the Stripe test sytem and use their test credit cars. Read our :ref:`Stripe documentation <stripe>` for more
information.
Second, you could create a separate event, just for testing. In the last step of the :ref:`event creation process <event_create>`,
you can specify that you want to copy all settings from your real event, so you don't have to do all of it twice.
We are planning to add a dedicated test mode in a later version of pretix.
If you are using the hosted service at pretix.eu and want to get rid of the test orders completely, contact us at
support@pretix.eu and we can remove them for you. Please note that we only are able to do that *before* you have
received any real orders (i.e. taken the shop public). We won't charge any fees for test orders or test events.
How do I delete an event?
-------------------------
It is currently not possible to delete events, you can just disable the shop by clicking the first square on your event
dashboard. Events can't be deleted as they most likely contain information on financial transactions which legally
needs to be kept on record for multiple years in most countries.
If you are using the hosted service at pretix.eu and want to get rid of an event that you only used for testing, contact
us at support@pretix.eu and we can remove it for you.
Why doesn't my product show up in the ticket shop?
--------------------------------------------------
If you created a product and it doesn't show up, please follow the following steps to find out why:
1. Check if the product's "active" checkbox is enabled.
2. Check if the product is in a category that has the "Products in this category are add-on products" checkbox enabled.
If this is the case, the product won't show up on the shop front page, but only in the first step of checkout when
a product in the cart allows to add add-on products from this category.
3. Check if the product's "Available from" or "Available until" settings restrict it to a date range.
4. Check if the product's checkbox "This product will only be shown if a voucher matching the product is redeemed." is
enabled. If this is the case, the product will only be shown if the customer redeems a voucher that *directly* matches
to this product. It will not be shown if the voucher only is configured to match a quota that contains the product.
5. Check that a quota exists that contains this product. If your product has variations, check that at least one
variation is contained in a quota. If your event is an event series, make sure that the product is contained in a
quota that is assigned to the series date that you access the shop for.
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
your event.

View File

@@ -9,6 +9,7 @@ wanting to use pretix to sell tickets.
organizers/index
events/create
events/taxes
payments/index
events/settings
events/widget
faq
markdown

166
doc/user/markdown.rst Normal file
View File

@@ -0,0 +1,166 @@
.. _markdown-guide:
Markdown Guide
==============
What is markdown?
-----------------
In many places of your shop, like frontpage texts, product descriptions and email texts, you can use
`Markdown`_ to create links, bold text, and other formatted content. Markdown is a good middle-ground
since it is way easier to learn than languages like HTML but allows all basic formatting options required
for text in those places.
Formatting rules
----------------
Simple text formatting
""""""""""""""""""""""
To set a text in italics, you can put it in asterisks or underscores. For example,
.. code-block:: markdown
Please *really* pay your _ticket_.
will become:
Please *really* pay your _ticket_.
If you set double asterisks or underscores, the text will be printed in bold. For example,
.. code-block:: markdown
This is **important**.
will become:
This is **important**.
You can also display, for example:
.. code-block:: markdown
Input this `exactly like this`.
You will get:
Input this ``exactly like this``.
Links
"""""
You can create a link by just pasting it in, e.g.
.. code-block:: markdown
Check this on https://en.wikipedia.org
will become:
Check this on https://en.wikipedia.org
However, if you want to control the text of the link, you can put the text of the link in ``[]`` brackets and the
link target in ``()`` parentheses, like this:
.. code-block:: markdown
Check this on [Wikipedia](https://en.wikipedia.org).
This will yield:
Check this on `Wikipedia`_
All links created with pretix Markdown syntax will open in a new tab.
Lists
"""""
You can create un-numbered lists by prepending the lines with asterisks.
.. code-block:: markdown
* First item
* Second item with a text that is too long to
fit in a line
* Third item
will become:
* First item
* Second item with a text that is too long to
fit in a line
* Third item
You can also use numbers as list items
.. code-block:: markdown
1. Red
2. Green
3. Blue
to get
1. Red
2. Green
3. Blue
Headlines
"""""""""
To create a headline, prepend it with ``#`` for the main headline, ``##`` for a headline of the second level,
and so on. For example:
.. code-block:: markdown
# Headline 1
## Headline 2
### Headline 3
#### Headline 4
##### Headline 5
###### Headline 6
We do not recommend using headlines of the first level, as pretix will already set the name of your event as a level-1
headline of the page and HTML pages should have only one headline on the first level.
You can also use
.. code-block:: markdown
*****
to create a horizontal line, like the following:
.. raw:: html
<hr>
Using HTML
----------
You can also directly embed HTML code, if you want, although we recommend
using Markdown, as it enables e.g. people using text-based email clients
to get a better plain text representation of your text. Note however, that for
security reasons you can only use the following HTML elements::
a, abbr, acronym, b, br, code, div, em, h1, h2,
h3, h4, h5, h6, hr, i, li, ol, p, span, strong,
table, tbody, td, thead, tr, ul
Additionally, only the following attributes are allowed on them::
<a href="…" title="…">
<abbr title="…">
<acronym title="…">
<table width="…">
<td width="…" align="…">
<div class="…">
<p class="…">
<span class="…">
All other elements and attributes will be stripped during parsing.
.. _Markdown: https://en.wikipedia.org/wiki/Markdown
.. _Wikipedia: https://en.wikipedia.org

View File

@@ -0,0 +1,43 @@
Organizer account
=================
The basis of all your operations within pretix is your organizer account. It represents an entity that is running
events, for example a company, yourself or any other institution.
Every event belongs to one organizer account and events within the same organizer account are assumed to belong together
in some sense, whereas events in different organizer accounts are completely isolated.
If you want to use the hosted pretix service, you can create an organizer account on our `Get started`_ page. Otherwise,
ask your pretix administrator for access to an organizer account.
You can find out all organizer accounts you have access to by going to your global dashboard (click on the pretix logo
in the top-left corner) and then select "Organizers" from the navigation bar on the left side. Then, choose one of the
organizer accounts presented, if there are multiple of them:
.. thumbnail:: ../../screens/organizer/list.png
:align: center
:class: screenshot
This overview shows you all event that belong to the organizer and you have access to:
.. thumbnail:: ../../screens/organizer/event_list.png
:align: center
:class: screenshot
With the "Edit" button at the top, next to the organizer account name, you can modify properties of the organizer
account such as its name and display settings for the public profile page of the organizer account:
.. thumbnail:: ../../screens/organizer/edit.png
:align: center
:class: screenshot
.. tip::
The profile page will be shown as ``https://pretix.eu/slug/`` where ``slug`` is to be replaced by the short form of
the organizer name that you entered during account creation and ``pretix.eu`` is to be replaced by your
installation's domain name if you are not using our hosted service.
Instead, you can also use a custom domain for the profile page and your events, for example
``https://tickets.example.com/`` if ``example.com`` is a domain that you own. Head to :ref:`custom_domain` to learn
more.
.. _Get started: https://pretix.eu/about/en/setup

View File

@@ -0,0 +1,54 @@
.. _custom_domain:
Using a custom domain
=====================
By default, event shops built with pretix are accessible at ``https://<domain>/<organizer>/<event>/``, where
``<domain>`` is ``pretix.eu`` if you are using our hosted service and ``<organizer>`` and ``<event>`` are the short
form versions of your organizer account name and event name, respectively.
However, you are also able to use a custom domain for your ticket shops! If you work for "Awesome Party Corporation"
and your website is ``awesomepartycorp.com``, you might want to sell your tickets at ``tickets.awesomepartycorp.com``
and with pretix, you can do this. On this page, you find out the necessary steps to take.
With the pretix.eu hosted service
---------------------------------
Step 1: DNS Configuration
#########################
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
domain provider.
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
The value of the record should be ``www.pretix.eu``.
Step 2: Wait for the DNS entry to propagate
###########################################
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
Step 3: Tell us
###############
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
certificate for you (for free!) and configure the domain.
With a custom pretix installation
---------------------------------
If you installed pretix on a server yourself, you can also use separate domains for separate organizers.
First of all, configure your webserver or reverse proxy to pass requests to the new domain to pretix as well.
Then, go to the organizer account in pretix and click the "Edit" button. Enter the new domain in the "Custom Domain"
field, then you're done!
.. thumbnail:: ../../screens/organizer/edit_sysadmin.png
:align: center
:class: screenshot
Note that this field only shows up if you are logged in as a system administrator of your pretix installation.

View File

@@ -1,112 +1,9 @@
Organizer accounts and teams
============================
Organizer account
-----------------
.. toctree::
:maxdepth: 2
The basis of all your operations within pretix is your organizer account. It represents an entity that is running
events, for example a company, yourself or any other institution.
Every event belongs to one organizer account and events within the same organizer account are assumed to belong together
in some sense, whereas events in different organizer accounts are completely isolated.
If you want to use the hosted pretix service, you can create an organizer account on our `Get started`_ page. Otherwise,
ask your pretix administrator for access to an organizer account.
You can find out all organizer accounts you have access to by going to your global dashboard (click on the pretix logo
in the top-left corner) and then select "Organizers" from the navigation bar on the left side. Then, choose one of the
organizer accounts presented, if there are multiple of them:
.. thumbnail:: ../../screens/organizer/list.png
:align: center
:class: screenshot
This overview shows you all event that belong to the organizer and you have access to:
.. thumbnail:: ../../screens/organizer/event_list.png
:align: center
:class: screenshot
With the "Edit" button at the top, next to the organizer account name, you can modify properties of the organizer
account such as its name and display settings for the public profile page of the organizer account:
.. thumbnail:: ../../screens/organizer/edit.png
:align: center
:class: screenshot
.. tip::
The profile page will be shown as ``https://pretix.eu/slug/`` where ``slug`` is to be replaced by the short form of
the organizer name that you entered during account creation and ``pretix.eu`` is to be replaced by your
installation's domain name if you are not using our hosted service.
Instead, you can also use a custom domain for the profile page and your events, for example
``https://tickets.example.com/`` if ``example.com`` is a domain that you own. In this case, please contact the pretix
hosted support or your system administrator to set up the custom domain.
Teams
-----
We don't expect you to work on your events all by yourself and therefore, pretix comes with ways to invite your fellow
team members to access your pretix organizer account. To manage teams, click on the "Teams" link on your organizer
settings page (see above how to find it). This shows you a list of teams that should contain at least one team already:
.. thumbnail:: ../../screens/organizer/team_list.png
:align: center
:class: screenshot
If you click on a team name, you get to a page that shows you the current members of the team:
.. thumbnail:: ../../screens/organizer/team_detail.png
:align: center
:class: screenshot
You see that there is a list of pretix user accounts (i.e. email addresses), who are part of the team. To add a user to
the team, just enter their email address in the text box next to the "Add" button. If the user already has an account
in the pretix system they will instantly get access to the team. Otherwise, they will be sent an email with an invitation
link that can be used to create an account. This account will then instantly have access to the team. Users can be part
of as many teams as you want.
In the section below, you can also create access tokens for our :ref:`rest-api`. You can read more on this topic in the
section :ref:`rest-auth` of the API documentation.
Next to the team name, you again see a button called "Edit" that allows you to modify the permissions of the team.
Permissions separate into two areas:
* **Organizer permissions** allow actions on the level of an organizer account, in particular:
* Can create events To create a new event under this organizer account, users need to have this permission
* Can change teams and permissions This permission is required to perform the kind of action you are doing right now.
Anyone with this permission can assign arbitrary other permissions to themselves, so this is the most powerful
permission there is to give.
* Can change organizer settings This permission is required to perform changes to the settings of the organizer
account, e.g. its name or display settings.
* **Event permissions** allow actions on the level of an event. You can give the team access to all events of the
organizer (including future ones that are not yet created) or just a selected set of events. The specific permissions to choose from are:
* Can change event settings This permission gives access to most areas of the control panel that are not controlled
by one of the other event permissions, especially those that are related to setting up and configuring the event.
* Can change product settings This permission allows to create and modify products and objects that are closely
related to products, such as product categories, quotas, and questions.
* Can view orders This permission allows viewing the list of orders and allindividual order details, but not
changing anything about it. This also includes the various exports offered.
* Can change orders This permission allows all actions that involve changing an order, such as changing the products
in an order, marking an order as paid or refunden, importing banking data, etc. This only works properly if the
same users also have the "Can view orders" permission.
* Can view vouchers This permission allows viewing the list of vouchers including the voucher codes themselves and
their redemption status.
* Can change vouchers This permission allows to create and modify vouchers in all their details. It only works
properly if the same users also have the "Can view vouchers" permission.
.. thumbnail:: ../../screens/organizer/team_edit.png
:align: center
:class: screenshot
.. _Get started: https://pretix.eu/about/en/setup
account
teams
domain

View File

@@ -0,0 +1,65 @@
Teams
=====
We don't expect you to work on your events all by yourself and therefore, pretix comes with ways to invite your fellow
team members to access your pretix organizer account. To manage teams, click on the "Teams" link on your organizer
settings page (see above how to find it). This shows you a list of teams that should contain at least one team already:
.. thumbnail:: ../../screens/organizer/team_list.png
:align: center
:class: screenshot
If you click on a team name, you get to a page that shows you the current members of the team:
.. thumbnail:: ../../screens/organizer/team_detail.png
:align: center
:class: screenshot
You see that there is a list of pretix user accounts (i.e. email addresses), who are part of the team. To add a user to
the team, just enter their email address in the text box next to the "Add" button. If the user already has an account
in the pretix system they will instantly get access to the team. Otherwise, they will be sent an email with an invitation
link that can be used to create an account. This account will then instantly have access to the team. Users can be part
of as many teams as you want.
In the section below, you can also create access tokens for our :ref:`rest-api`. You can read more on this topic in the
section :ref:`rest-auth` of the API documentation.
Next to the team name, you again see a button called "Edit" that allows you to modify the permissions of the team.
Permissions separate into two areas:
* **Organizer permissions** allow actions on the level of an organizer account, in particular:
* Can create events To create a new event under this organizer account, users need to have this permission
* Can change teams and permissions This permission is required to perform the kind of action you are doing right now.
Anyone with this permission can assign arbitrary other permissions to themselves, so this is the most powerful
permission there is to give.
* Can change organizer settings This permission is required to perform changes to the settings of the organizer
account, e.g. its name or display settings.
* **Event permissions** allow actions on the level of an event. You can give the team access to all events of the
organizer (including future ones that are not yet created) or just a selected set of events. The specific permissions to choose from are:
* Can change event settings This permission gives access to most areas of the control panel that are not controlled
by one of the other event permissions, especially those that are related to setting up and configuring the event.
* Can change product settings This permission allows to create and modify products and objects that are closely
related to products, such as product categories, quotas, and questions.
* Can view orders This permission allows viewing the list of orders and allindividual order details, but not
changing anything about it. This also includes the various exports offered.
* Can change orders This permission allows all actions that involve changing an order, such as changing the products
in an order, marking an order as paid or refunden, importing banking data, etc. This only works properly if the
same users also have the "Can view orders" permission.
* Can view vouchers This permission allows viewing the list of vouchers including the voucher codes themselves and
their redemption status.
* Can change vouchers This permission allows to create and modify vouchers in all their details. It only works
properly if the same users also have the "Can view vouchers" permission.
.. thumbnail:: ../../screens/organizer/team_edit.png
:align: center
:class: screenshot

View File

@@ -30,3 +30,9 @@ many orders could be processed correctly and how many could not. You can then go
transfers from your bank statement that are not yet matched to an order. Using the input field and the buttons on the
left of each transaction, you can manually enter an order code to match it to or just discard it from the list, e.g.
if the transaction is not related to the event at all.
.. tip:: If you aren't afraid of getting a bit more technical and your bank supports the HBCI/FinTS protocol (as most
German banks do), you can use `pretix-banktool`_ to fully automate this process.
.. _pretix-banktool: https://github.com/pretix/pretix-banktool

View File

@@ -1,3 +1,5 @@
.. _payment-fees:
Payment method fees
===================
@@ -18,6 +20,9 @@ might also decide to go for option one to make it easier for customers who don't
legislation might already be in place or become relevant from January 2018 the latest. This is not
legal advice. If in doubt, consult a lawyer or refrain from charging payment fees.
If you go for the first option (as you should in the EU), you can just leave the payment fee fields in pretix' settings
empty.
If you go for the second option, you can configure pretix to charge the payment method fees to your user. You can
define both an absolute fee as well as a percental fee based on the order total. If you do so, there are two
different ways in which pretix can calculate the fee. Normally, it is fine to just go with the default setting, but

View File

@@ -1,9 +1,10 @@
Accepting payments
==================
Payment settings
================
.. toctree::
:maxdepth: 2
settings
overview
fees
paypal

View File

@@ -10,25 +10,8 @@ Payment methods are built as pretix plugins. For this reason, you might first ne
If you host pretix on your own server, you might need to install a plugin first for some of the payment methods listed
on this page as well as for additional ones.
:ref:`stripe`
Stripe is a US-based company that offers you an easy way to accept credit card payments from all over the world.
To accept payments with Stripe, you need to have a Stripe merchant account that is easy to create. Click on the link
above to get more details about the Stripe integration into pretix.
:ref:`paypal`
If you want to accept online payments via PayPal, you can do so using pretix. You will need a PayPal merchant
account and it is a little bit complicated to obtain the required technical details, but we've got you covered.
Click on the link above to learn more.
:ref:`banktransfer`
Classical IBAN wire transfers are a common payment method in central Europe that has the large benefit that it
often does not cause any additional fees. However, it requires you to invest some more effort as you need to
check your bank account for incoming payments regularly. We provide some tools to make this easier for you.
SEPA debit
In some Europen countries, a very popular online payment method is SEPA direct debit. If you want to offer this
option in your pretix ticket shop, we provide a convenient plugin that allows users to enter their SEPA bank
account details and issue a SEPA mandate. You will then need to regularly download a SEPA XML file from pretix
and upload it to your bank's interface to actually perform the debits.
To get an overview of the officially supported payment methods and their pros and cons, head to the `pretix website`_.
On these pages, you get more information on how to configure :ref:`stripe`, :ref:`paypal`, and :ref:`banktransfer`.
.. _pretix website: https://pretix.eu/about/en/payments

View File

@@ -0,0 +1,65 @@
General settings
================
At "Settings" → "Pages", you can configure every aspect related to the payments you want to accept. The upper part
of the page shows a number of general settings that affect all payment methods:
.. thumbnail:: ../../screens/event/settings_payment.png
:align: center
:class: screenshot
In particular, these are:
Payment term in days
If a order has been created, it is supposed to be paid within this number of days. Of course, some payment mehtods
(like credit card) succeed immediately in most cases, but others don't (like bank transfer) and even credit card
payments might fail and you might want to give the customer a chance to try another credit card before losing their
ticket. Therefore, we recommend setting a few days here. If you are accepting bank transfers, we wouldn't recommend
less than 10 days.
Last date of payments
There is probably no use for payments received after your event, so you can set a date that the payment deadline of
a new order will never exceed. This has precendence over the number of days configured above, so if I create an order
two days before the configured last date of payments, my payment term will only be two days, not ten. If you have
payment methods that always require some time (like bank transfer), you will later be able to selectively disable them
once the event comes closer.
Only end payment terms on weekdays
If you check this box, the payment term calculated by the number of days configured above will never end on a Saturday
or a Sunday. If it technically would do so, the term is extended to the next Monday. Note that this currently does not
take into account national or bank holidays in your country.
Automatically expire unpaid orders
If you check this box, orders will automatically go into "expired" state if the payment term is over and no payment
has been received. This means that the tickets will no longer be reserved for the customer and someone else can buy
them from the shop again. If you do not check this box, tickets do not become available again automatically, but you
can mark orders as expired manually.
Accept late payments
If you check this box, incoming payments will accepted even if the order is in "expired" state -- as long as there
still is sufficient quota available and the last date of payments is not yet over. We recommend to check this in most
cases.
Tax rule for payment fees
If you pass on the payment method fees to your customers, you will most likely also need to pay sales tax on those
fees. Here, you can configure the tax rate. Read :ref:`taxes` for more information.
Below, you can configure the details of the various payment methods. You can find information on their different settings
on the next pages of this documentation, but there are a few things most of them have in common:
Enable payment method
Check this box to allow customers to use this method. At least one method needs to be active to process non-free orders.
Additional fee (absolute and percentage), Calculate the fee from the total value including the fee
These fields allow you to pass fees on to your customers instead of paying them yourselves. Read :ref:`payment-fees`
for documentation on how this behaves.
Available until
This allows you to set a date at which this payment method will automatically become disabled. This is useful if you
want people to be able to pay by card on the day before your event, but not by bank transfer, because it would not
arrive in time.
Text on invoices
If you are using pretix' invoicing feature, this is a text that will be printed on every invoice for an order that
uses this payment method. You could use this to tell the accounting department of the invoice receiver that the payment
has already been received online or that it should be performed via bank transfer.

View File

@@ -1 +1 @@
__version__ = "1.9.0"
__version__ = "1.10.0"

View File

@@ -1,3 +1,7 @@
import time
from django.conf import settings
from django.contrib.auth import logout
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import SAFE_METHODS, BasePermission
@@ -10,8 +14,6 @@ class EventPermission(BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken):
if request.method in SAFE_METHODS and request.path.startswith('/api/v1/docs/'):
return True
return False
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
@@ -21,6 +23,18 @@ class EventPermission(BasePermission):
else:
required_permission = None
if request.user.is_authenticated:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return False
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return False
request.session['pretix_auth_last_used'] = int(time.time())
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:

View File

@@ -0,0 +1,37 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import CheckinList
class CheckinListSerializer(I18nAwareModelSerializer):
checkin_count = serializers.IntegerField(read_only=True)
position_count = serializers.IntegerField(read_only=True)
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count')
def validate(self, data):
data = super().validate(data)
event = self.context['event']
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
for item in full_data.get('limit_products'):
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))
if event.has_subevents:
if not full_data.get('subevent'):
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != full_data.get('subevent').event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if full_data.get('subevent'):
raise ValidationError(_('The subevent does not belong to this event.'))
return data

View File

@@ -73,3 +73,16 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
def validate(self, data):
data = super().validate(data)
event = self.context['event']
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
Quota.clean_variations(full_data.get('items'), full_data.get('variations'))
Quota.clean_items(event, full_data.get('items'), full_data.get('variations'))
Quota.clean_subevent(event, full_data.get('subevent'))
return data

View File

@@ -1,19 +0,0 @@
{% extends "rest_framework/base.html" %}
{% load staticfiles %}
{% load compress %}
{% block bootstrap_theme %}
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "rest_framework/scss/main.scss" %}" />
{% endcompress %}
{% endblock %}
{% block branding %}
<a class="navbar-brand" href="/api/v1/">pretix REST API</a>
{% endblock %}
{% block description %}
<div class="alert alert-info alert-docs-link">
<a href="https://docs.pretix.eu/en/latest/api/index.html">
You can find documentation on our REST API on docs.pretix.eu.
</a>
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@ from django.apps import apps
from django.conf.urls import include, url
from rest_framework import routers
from .views import event, item, order, organizer, voucher, waitinglist
from .views import checkin, event, item, order, organizer, voucher, waitinglist
router = routers.DefaultRouter()
router.register(r'organizers', organizer.OrganizerViewSet)
@@ -24,6 +24,7 @@ event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
# Force import of all plugins to give them a chance to register URLs with the router
for app in apps.get_app_configs():

View File

@@ -0,0 +1,59 @@
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.base.models import CheckinList
from pretix.base.models.organizer import TeamAPIToken
class CheckinListFilter(FilterSet):
class Meta:
model = CheckinList
fields = ['subevent']
class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer
queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend,)
filter_class = CheckinListFilter
permission = 'can_view_orders'
write_permission = 'can_change_event_settings'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related(
'limit_products',
)
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
return qs
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.checkinlist.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.checkinlist.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.checkinlist.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)

View File

@@ -11,6 +11,7 @@ from pretix.api.serializers.item import (
QuotaSerializer,
)
from pretix.base.models import Item, ItemCategory, Question, Quota
from pretix.base.models.organizer import TeamAPIToken
class ItemFilter(FilterSet):
@@ -77,7 +78,7 @@ class QuotaFilter(FilterSet):
fields = ['subevent']
class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
class QuotaViewSet(viewsets.ModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
@@ -85,10 +86,80 @@ class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = ('id', 'size')
ordering = ('id',)
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.quotas.all()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.quota.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
if serializer.instance.subevent:
serializer.instance.subevent.log_action(
'pretix.subevent.quota.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def perform_update(self, serializer):
current_subevent = serializer.instance.subevent
serializer.save(event=self.request.event)
request_subevent = serializer.instance.subevent
serializer.instance.log_action(
'pretix.event.quota.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
if current_subevent == request_subevent:
if current_subevent is not None:
current_subevent.log_action(
'pretix.subevent.quota.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
else:
if request_subevent is not None:
request_subevent.log_action(
'pretix.subevent.quota.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
if current_subevent is not None:
current_subevent.log_action(
'pretix.subevent.quota.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
serializer.instance.rebuild_cache()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.quota.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
if instance.subevent:
instance.subevent.log_action(
'pretix.subevent.quota.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
@detail_route(methods=['get'])
def availability(self, request, *args, **kwargs):
quota = self.get_object()

View File

@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-11-24 16:29
from __future__ import unicode_literals
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.utils.translation import ugettext as _
import pretix.base.validators
from pretix.base.i18n import language
def create_checkin_lists(apps, schema_editor):
Event = apps.get_model('pretixbase', 'Event')
Checkin = apps.get_model('pretixbase', 'Checkin')
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
for e in Event.objects.all():
locale = EventSettingsStore.objects.filter(object=e, key='locale').first()
if locale:
locale = locale.value
else:
locale = settings.LANGUAGE_CODE
if e.has_subevents:
for se in e.subevents.all():
with language(locale):
cl = e.checkin_lists.create(name=se.name, subevent=se, all_products=True)
Checkin.objects.filter(position__subevent=se, position__order__event=e).update(list=cl)
else:
with language(locale):
cl = e.checkin_lists.create(name=_('Default list'), all_products=True)
Checkin.objects.filter(position__order__event=e).update(list=cl)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0076_orderfee_squashed_0082_invoiceaddress_internal_reference'),
]
operations = [
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
name='name',
field=models.CharField(db_index=True, help_text='Can not contain spaces or special characters except underscores', max_length=50, validators=[django.core.validators.RegexValidator(message='The property name may only contain letters, numbers and underscores.', regex='^[a-zA-Z0-9_]+$')], verbose_name='Name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190)),
('all_products', models.BooleanField(default=True, verbose_name='All products (including newly created ones)')),
],
),
migrations.AddField(
model_name='checkinlist',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkin_lists', to='pretixbase.Event'),
),
migrations.AddField(
model_name='checkinlist',
name='subevent',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, related_name='checkin_lists', to='pretixbase.SubEvent'),
),
migrations.AddField(
model_name='checkinlist',
name='limit_products',
field=models.ManyToManyField(blank=True, to='pretixbase.Item', verbose_name='Limit to products'),
),
migrations.AddField(
model_name='checkin',
name='list',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.CheckinList'),
),
migrations.AlterField(
model_name='checkin',
name='list',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.CheckinList'),
),
migrations.AlterField(
model_name='checkinlist',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.RunPython(
create_checkin_lists,
migrations.RunPython.noop
),
migrations.AlterField(
model_name='checkin',
name='list',
field=models.ForeignKey(null=False, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.CheckinList'),
),
]

View File

@@ -1,7 +1,7 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .checkin import Checkin, CheckinList
from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,

View File

@@ -1,5 +1,76 @@
from django.db import models
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 pretix.base.models import LoggedModel
class CheckinList(LoggedModel):
event = models.ForeignKey('Event', related_name='checkin_lists')
name = models.CharField(max_length=190)
all_products = models.BooleanField(default=True, verbose_name=_("All products (including newly created ones)"))
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
verbose_name=pgettext_lazy('subevent', 'Date'))
@staticmethod
def annotate_with_numbers(qs, event):
from . import Order, OrderPosition
cqs = Checkin.objects.filter(
position__order__event=event,
position__order__status=Order.STATUS_PAID,
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
pqs_all = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_limited = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
item__in=OuterRef('limit_products')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
return qs.annotate(
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
position_count=Coalesce(Case(
When(all_products=True, then=Subquery(pqs_all, output_field=models.IntegerField())),
default=Subquery(pqs_limited, output_field=models.IntegerField()),
output_field=models.IntegerField()
), 0)
).annotate(
percent=Case(
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
default=0,
output_field=models.IntegerField()
)
)
def __str__(self):
return self.name
class Checkin(models.Model):
@@ -9,3 +80,11 @@ class Checkin(models.Model):
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins')
datetime = models.DateTimeField(default=now)
nonce = models.CharField(max_length=190, null=True, blank=True)
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
def __repr__(self):
return "<Checkin: pos {} on list '{}' at {}>".format(
self.position, self.list, self.datetime
)

View File

@@ -199,8 +199,8 @@ class Event(EventMixin, LoggedModel):
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
"We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
"Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your "
"events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
"remembered, but you can also choose to use a random value. "
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
validators=[
@@ -394,12 +394,15 @@ class Event(EventMixin, LoggedModel):
for v in vars:
q.variations.add(variation_map[v.pk])
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
items = list(q.items.all())
opts = list(q.options.all())
question_map[q.pk] = q
q.pk = None
q.event = self
q.save()
for i in items:
q.items.add(item_map[i.pk])
for o in opts:
@@ -407,6 +410,14 @@ class Event(EventMixin, LoggedModel):
o.question = q
o.save()
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
items = list(cl.limit_products.all())
cl.pk = None
cl.event = self
cl.save()
for i in items:
cl.limit_products.add(item_map[i.pk])
for s in other.settings._objects.all():
s.object = self
s.pk = None
@@ -431,7 +442,11 @@ class Event(EventMixin, LoggedModel):
else:
s.save()
event_copy_data.send(sender=self, other=other)
event_copy_data.send(
sender=self, other=other,
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
question_map=question_map
)
def get_payment_providers(self) -> dict:
"""

View File

@@ -897,3 +897,30 @@ class Quota(LoggedModel):
class QuotaExceededException(Exception):
pass
@staticmethod
def clean_variations(items, variations):
for variation in variations:
if variation.item not in items:
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
break
@staticmethod
def clean_items(event, items, variations):
for item in items:
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))
if item.has_variations:
if not any(var.item == item for var in variations):
raise ValidationError(_('One or more items has variations but none of these are in the variations list.'))
@staticmethod
def clean_subevent(event, subevent):
if event.has_subevents:
if not subevent:
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != subevent.event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if subevent:
raise ValidationError(_('The subevent does not belong to this event.'))

View File

@@ -368,7 +368,7 @@ class Order(LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None):
user: User=None, headers: dict=None, sender: str=None, invoices: list=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -387,26 +387,28 @@ class Order(LoggedModel):
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
recipient = self.email
email_content = render_mail(template, context)[0]
try:
with language(self.locale):
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)[0]
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender
self.event, self.locale, self, headers, sender,
invoices=invoices
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else []
}
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
data={
'subject': subject,
'message': email_content,
'recipient': recipient
}
)
def answerfile_name(instance, filename: str) -> str:

View File

@@ -32,8 +32,8 @@ class Organizer(LoggedModel):
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
"This is being used in addresses and bank transfer references."),
"Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used "
"once. This is being used in URLs to refer to your organizer accounts and your events."),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9.-]+$",

View File

@@ -176,7 +176,7 @@ class BasePaymentProvider:
forms.BooleanField(
label=_('Calculate the fee from the total value including the fee.'),
help_text=_('We recommend to enable this if you want your users to pay the payment fees of your '
'payment provider. <a href="{docs_url}" target="_blank">Click here '
'payment provider. <a href="{docs_url}" target="_blank" rel="noopener">Click here '
'for detailed information on what this does.</a> Don\'t forget to set the correct fees '
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False

View File

@@ -1,6 +1,7 @@
from typing import Any, Dict
from django.core.files.base import ContentFile
from django.utils.timezone import override
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, cachedfile_name
@@ -13,7 +14,7 @@ from pretix.celery_app import app
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
event = Event.objects.get(id=event)
file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale):
with language(event.settings.locale), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for receiver, response in responses:
ex = response(event)

View File

@@ -109,6 +109,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
desc += " - " + str(p.variation.value)
if p.addon_to_id:
desc = " + " + desc
if invoice.event.settings.invoice_attendee_name and p.attendee_name:
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
InvoiceLine.objects.create(
position=i, invoice=invoice, description=desc,
gross_value=p.price, tax_value=p.tax_value,
@@ -158,7 +160,7 @@ def build_cancellation(invoice: Invoice):
return invoice
def generate_cancellation(invoice: Invoice):
def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation = copy.copy(invoice)
cancellation.pk = None
cancellation.invoice_no = None
@@ -170,7 +172,8 @@ def generate_cancellation(invoice: Invoice):
cancellation.save()
cancellation = build_cancellation(cancellation)
invoice_pdf(cancellation.pk)
if trigger_pdf:
invoice_pdf(cancellation.pk)
return cancellation
@@ -183,7 +186,7 @@ def regenerate_invoice(invoice: Invoice):
return invoice
def generate_invoice(order: Order):
def generate_invoice(order: Order, trigger_pdf=True):
locale = order.event.settings.get('invoice_language')
if locale:
if locale == '__user__':
@@ -197,10 +200,11 @@ def generate_invoice(order: Order):
locale=locale
)
invoice = build_invoice(invoice)
invoice_pdf(invoice.pk)
if trigger_pdf:
invoice_pdf(invoice.pk)
if order.status in (Order.STATUS_CANCELED, Order.STATUS_REFUNDED):
generate_cancellation(invoice)
generate_cancellation(invoice, trigger_pdf)
return invoice

View File

@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Union
import bleach
import cssutils
import markdown
from celery import chain
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import get_template
@@ -12,7 +13,10 @@ from i18nfield.strings import LazyI18nString
from inlinestyler.utils import inline_css
from pretix.base.i18n import language
from pretix.base.models import Event, InvoiceAddress, Order
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import email_filter
from pretix.base.templatetags.rich_text import markdown_compile
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -33,7 +37,7 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None, sender: str=None):
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -61,6 +65,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param sender: Set the sender email address. If not set and ``event`` is set, the event's default will be used,
otherwise the system default.
:param invoices: A list of invoices to attach to this email.
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -137,21 +143,58 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
tpl = get_template('pretixbase/email/plainwrapper.html')
body_html = tpl.render(htmlctx)
return mail_send([email], subject, body_plain, body_html, sender, event.id if event else None, headers)
send_task = mail_send_task.si(
to=[email],
subject=subject,
body=body_plain,
html=body_html,
sender=sender,
event=event.id if event else None,
headers=headers,
invoices=[i.pk for i in invoices] if invoices else [],
order=order.pk if order else None
)
if invoices:
task_chain = [invoice_pdf_task.si(i.pk).on_error(send_task) for i in invoices if not i.file]
else:
task_chain = []
task_chain.append(send_task)
chain(*task_chain).apply_async()
@app.task
def mail_send_task(to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None, bcc: List[str]=None) -> bool:
def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
order: int=None) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(inline_css(html), "text/html")
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
if inv.file:
email.attach(
'{}.pdf'.format(inv.number),
inv.file.file.read(),
'application/pdf'
)
if event:
event = Event.objects.get(id=event)
backend = event.get_mail_backend()
else:
backend = get_connection(fail_silently=False)
if event:
if order:
try:
order = event.orders.get(pk=order)
except Order.DoesNotExist:
order = None
email = email_filter.send_chained(event, 'message', message=email, order=order)
try:
backend.send_messages([email])
except Exception:
@@ -168,11 +211,8 @@ def render_mail(template, context):
body = str(template)
if context:
body = body.format_map(TolerantDict(context))
body_md = bleach.linkify(bleach.clean(markdown.markdown(body), tags=bleach.ALLOWED_TAGS + [
'p', 'pre'
]))
else:
tpl = get_template(template)
body = tpl.render(context)
body_md = bleach.linkify(markdown.markdown(body))
body_md = bleach.linkify(markdown_compile(body))
return body, body_md

View File

@@ -127,9 +127,13 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
}, user=user, api_token=api_token)
order_paid.send(order.event, order=order)
invoice = None
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
if not order.invoices.exists():
generate_invoice(order)
invoice = generate_invoice(
order,
trigger_pdf=not send_mail or not order.event.settings.invoice_email_attachment
)
if send_mail:
with language(order.locale):
@@ -155,7 +159,8 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and order.event.settings.invoice_email_attachment else []
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@@ -502,9 +507,11 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info)
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
if not order.invoices.exists():
generate_invoice(order)
if not invoice:
invoice = generate_invoice(order, trigger_pdf=not event.settings.invoice_email_attachment)
# send_mail will trigger PDF generation later
if order.total == Decimal('0.00'):
email_template = event.settings.mail_text_order_free
@@ -536,7 +543,8 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
)
except SendMailException:
logger.exception('Order received email could not be sent')
@@ -573,37 +581,38 @@ def send_expiry_warnings(sender, **kwargs):
days = eventsettings.get('mail_days_order_expire_warning', as_type=int)
tz = pytz.timezone(eventsettings.get('timezone', settings.TIME_ZONE))
if days and (o.expires - today).days <= days:
o.expiry_reminder_sent = True
o.save()
try:
invoice_name = o.invoice_address.name
invoice_company = o.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = eventsettings.mail_text_order_expire_warning
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
if eventsettings.payment_term_expire_automatically:
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
else:
email_subject = _('Your order is pending payment: %(code)s') % {'code': o.code}
with language(o.locale):
o.expiry_reminder_sent = True
o.save()
try:
invoice_name = o.invoice_address.name
invoice_company = o.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = eventsettings.mail_text_order_expire_warning
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
if eventsettings.payment_term_expire_automatically:
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
else:
email_subject = _('Your order is pending payment: %(code)s') % {'code': o.code}
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.expire_warning_sent'
)
except SendMailException:
logger.exception('Reminder email could not be sent')
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.expire_warning_sent'
)
except SendMailException:
logger.exception('Reminder email could not be sent')
@receiver(signal=periodic_task)

View File

@@ -7,7 +7,8 @@ from django.utils.translation import ugettext as _
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, Order, OrderPosition,
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
OrderPosition,
)
from pretix.base.services.async import ProfiledTask
from pretix.base.signals import register_ticket_outputs
@@ -73,13 +74,21 @@ def preview(event: int, provider: str):
event = Event.objects.get(id=event)
with rolledback_transaction(), language(event.settings.locale):
item = event.items.create(name=_("Sample product"), default_price=42.23)
item = event.items.create(name=_("Sample product"), default_price=42.23,
description=_("Sample product description"))
item2 = event.items.create(name=_("Sample workshop"), default_price=23.40)
from pretix.base.models import Order
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
locale=event.settings.locale,
expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
responses = register_ticket_outputs.send(event)
for receiver, response in responses:

View File

@@ -49,6 +49,10 @@ DEFAULTS = {
'default': 'False',
'type': bool,
},
'invoice_attendee_name': {
'default': 'True',
'type': bool,
},
'invoice_address_required': {
'default': 'False',
'type': bool,
@@ -129,6 +133,10 @@ DEFAULTS = {
'default': '__user__',
'type': str
},
'invoice_email_attachment': {
'default': 'False',
'type': bool
},
'show_items_outside_presale_period': {
'default': 'True',
'type': bool

View File

@@ -25,6 +25,24 @@ class EventPluginSignal(django.dispatch.Signal):
Event.
"""
def _is_active(self, sender, receiver):
# Find the Django application this belongs to
searchpath = receiver.__module__
core_module = any([searchpath.startswith(cm) for cm in settings.CORE_MODULES])
app = None
if not core_module:
while True:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
searchpath, _ = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins and core modules
if core_module or (sender and app and app.name in sender.get_plugins()):
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
return True
return False
def send(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers that belong to
@@ -43,24 +61,35 @@ class EventPluginSignal(django.dispatch.Signal):
_populate_app_cache()
for receiver in self._live_receivers(sender):
# Find the Django application this belongs to
searchpath = receiver.__module__
core_module = any([searchpath.startswith(cm) for cm in settings.CORE_MODULES])
app = None
if not core_module:
while True:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
searchpath, _ = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins and core modules
if core_module or (sender and app and app.name in sender.get_plugins()):
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
if self._is_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return sorted(responses, key=lambda r: (receiver.__module__, receiver.__name__))
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
second receiver and so on. The return value of the last receiver is returned by this method.
sender is required to be an instance of ``pretix.base.models.Event``.
"""
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
response = named.get(chain_kwarg_name)
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return response
if not app_cache:
_populate_app_cache()
for receiver in self._live_receivers(sender):
if self._is_active(sender, receiver):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
return response
class DeprecatedSignal(django.dispatch.Signal):
@@ -151,7 +180,7 @@ order_paid = EventPluginSignal(
"""
This signal is sent out every time an order is paid. The order object is given
as the first argument. This signal is *not* sent out if an order is marked as paid
because it an already-paid order has been splitted.
because an already-paid order has been split.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -223,7 +252,10 @@ You don't need to copy data inside the general settings storage which is cloned
but you might need to modify that data.
The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
keyword argument will contain the event to **copy from**.
keyword argument will contain the event to **copy from**. The keyword arguments
``tax_map``, ``category_map``, ``item_map``, ``question_map``, and ``variation_map`` contain
mappings from object IDs in the original event to objects in the new event of the respective
types.
"""
periodic_task = django.dispatch.Signal()
@@ -274,3 +306,16 @@ a download will not be offered.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
email_filter = EventPluginSignal(
providing_args=['message', 'order']
)
"""
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
The ``message`` argument will contian an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
"""

View File

@@ -35,12 +35,14 @@ ALLOWED_TAGS = [
'th',
'div',
'span',
'hr',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
# Update doc/user/markdown.rst if you change this!
]
ALLOWED_ATTRIBUTES = {
@@ -52,6 +54,7 @@ ALLOWED_ATTRIBUTES = {
'div': ['class'],
'p': ['class'],
'span': ['class'],
# Update doc/user/markdown.rst if you change this!
}
@@ -61,24 +64,39 @@ def safelink_callback(attrs, new=False):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
def abslink_callback(attrs, new=False):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, attrs.get((None, 'href'), '/'))
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
def markdown_compile(source):
return bleach.clean(
markdown.markdown(
source,
extensions=[
'markdown.extensions.sane_lists',
# 'markdown.extensions.nl2br', # TODO: Enable, but check backwards-compatibility issues e.g. with mails
]
),
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES
)
@register.filter
def rich_text(text: str, **kwargs):
"""
Processes markdown and cleans HTML in a text input.
"""
text = str(text)
body_md = bleach.linkify(bleach.clean(
markdown.markdown(text),
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
), callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]))
body_md = bleach.linkify(
markdown_compile(text),
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback])
)
return mark_safe(body_md)

View File

@@ -0,0 +1,31 @@
from django import forms
from pretix.base.models.checkin import CheckinList
class CheckinListForm(forms.ModelForm):
def __init__(self, **kwargs):
self.event = kwargs.pop('event')
kwargs.pop('locales', None)
super().__init__(**kwargs)
self.fields['limit_products'].queryset = self.event.items.all()
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].required = True
else:
del self.fields['subevent']
class Meta:
model = CheckinList
localized_fields = '__all__'
fields = [
'name',
'all_products',
'limit_products',
'subevent'
]
widgets = {
'limit_products': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '<[name$=all_products]'
}),
}

View File

@@ -544,7 +544,20 @@ class InvoiceSettingsForm(SettingsForm):
('user', _('Automatically on user request')),
('True', _('Automatically for all created orders')),
('paid', _('Automatically on payment')),
)
),
help_text=_("Invoices will never be automatically generated for free orders.")
)
invoice_attendee_name = forms.BooleanField(
label=_("Show attendee names on invoices"),
required=False
)
invoice_email_attachment = forms.BooleanField(
label=_("Attach invoices to emails"),
help_text=_("If invoices are automatically generated for all orders, they will be attached to the order "
"confirmation mail. If they are automatically generated on payment, they will be attached to the "
"payment confirmation mail. If they are not automatically generated, they will not be attached "
"to emails."),
required=False
)
invoice_renderer = forms.ChoiceField(
label=_("Invoice style"),

View File

@@ -1,14 +1,14 @@
from django import forms
from django.apps import apps
from django.db.models import Exists, OuterRef, Q
from django.db.models.functions import Concat
from django.db.models import Exists, F, OuterRef, Q
from django.db.models.functions import Coalesce, Concat
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.models import Event, Invoice, Item, Order, Organizer, SubEvent
from pretix.base.signals import register_payment_providers
from pretix.control.utils.i18n import i18ncomp
from pretix.helpers.database import rolledback_transaction
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
PAYMENT_PROVIDERS = []
@@ -57,7 +57,10 @@ class FilterForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['ordering'] = forms.ChoiceField(
choices=sum([[(a, b), ('-' + a, '-' + b)] for a, b in self.orders.items()], []),
choices=sum([
[(a, a), ('-' + a, '-' + a)]
for a in self.orders.keys()
], []),
required=False
)
@@ -136,7 +139,7 @@ class OrderFilterForm(FilterForm):
qs = qs.filter(status=s)
if fdata.get('ordering'):
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
qs = qs.order_by(self.orders[fdata.get('ordering')])
if fdata.get('provider'):
qs = qs.filter(payment_provider=fdata.get('provider'))
@@ -272,6 +275,39 @@ class SubEventFilterForm(FilterForm):
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
)
if fdata.get('ordering'):
qs = qs.order_by(self.orders[fdata.get('ordering')])
return qs
class OrganizerFilterForm(FilterForm):
orders = {
'slug': 'slug',
'name': 'name',
}
query = forms.CharField(
label=_('Organizer name'),
widget=forms.TextInput(attrs={
'placeholder': _('Organizer name'),
'autofocus': 'autofocus'
}),
required=False
)
def __init__(self, *args, **kwargs):
kwargs.pop('request')
super().__init__(*args, **kwargs)
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(name__icontains=query) | Q(slug__icontains=query)
)
if fdata.get('ordering'):
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
@@ -354,6 +390,94 @@ class EventFilterForm(FilterForm):
)
if fdata.get('ordering'):
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
qs = qs.order_by(self.orders[fdata.get('ordering')])
return qs
class CheckInFilterForm(FilterForm):
orders = {
'code': ('order__code', 'item__name'),
'-code': ('-order__code', '-item__name'),
'email': ('order__email', 'item__name'),
'-email': ('-order__email', '-item__name'),
'status': (FixedOrderBy(F('last_checked_in'), nulls_first=True, descending=True), 'order__code'),
'-status': (FixedOrderBy(F('last_checked_in'), nulls_last=True), '-order__code'),
'timestamp': (FixedOrderBy(F('last_checked_in'), nulls_first=True), 'order__code'),
'-timestamp': (FixedOrderBy(F('last_checked_in'), nulls_last=True, descending=True), '-order__code'),
'item': ('item__name', 'variation__value', 'order__code'),
'-item': ('-item__name', '-variation__value', '-order__code'),
'name': {'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
'-name': {'_order': F('display_name').desc(nulls_last=True),
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
}
user = forms.CharField(
label=_('Search attendee…'),
widget=forms.TextInput(attrs={
'placeholder': _('Search attendee…'),
'autofocus': 'autofocus'
}),
required=False
)
status = forms.ChoiceField(
label=_('Check-in status'),
choices=(
('', _('All attendees')),
('1', _('Checked in')),
('0', _('Not checked in')),
),
required=False,
)
item = forms.ModelChoiceField(
label=_('Products'),
queryset=Item.objects.none(),
required=False,
empty_label=_('All products')
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.list = kwargs.pop('list')
super().__init__(*args, **kwargs)
if self.list.all_products:
self.fields['item'].queryset = self.event.items.all()
else:
self.fields['item'].queryset = self.list.limit_products.all()
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('user'):
u = fdata.get('user')
qs = qs.filter(
Q(order__email__icontains=u)
| Q(attendee_name__icontains=u)
| Q(attendee_email__icontains=u)
| Q(order__invoice_address__name__icontains=u)
| Q(order__invoice_address__company__icontains=u)
)
if fdata.get('status'):
s = fdata.get('status')
if s == '1':
qs = qs.filter(last_checked_in__isnull=False)
elif s == '0':
qs = qs.filter(last_checked_in__isnull=True)
if fdata.get('ordering'):
ob = self.orders[fdata.get('ordering')]
if isinstance(ob, dict):
ob = dict(ob)
o = ob.pop('_order')
qs = qs.annotate(**ob).order_by(o)
elif isinstance(ob, (list, tuple)):
qs = qs.order_by(*ob)
else:
qs = qs.order_by(ob)
if fdata.get('item'):
qs = qs.filter(item=fdata.get('item'))
return qs

View File

@@ -42,7 +42,7 @@ class UpdateSettingsForm(SettingsForm):
help_text=_("During the update check, pretix will report an anonymous, unique installation ID, "
"the current version of pretix and your installed plugins and the number of active and "
"inactive events in your installation to servers operated by the pretix developers. We "
"will only store anonymous data, never any IP adresses and we will not know who you are "
"will only store anonymous data, never any IP addresses and we will not know who you are "
"or where to find your instance. You can disable this behaviour here at any time.")
)
update_check_email = forms.EmailField(

View File

@@ -202,8 +202,8 @@ class ItemCreateForm(I18nModelForm):
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):

View File

@@ -123,3 +123,30 @@ class SubEventMetaValueForm(forms.ModelForm):
widgets = {
'value': forms.TextInput
}
class CheckinListFormSet(I18nInlineFormSet):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event', None)
self.locales = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
@cached_property
def items(self):
return self.event.items.prefetch_related('variations').all()
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
event=self.event,
)
self.add_fields(form, None)
return form

View File

@@ -9,7 +9,9 @@ from django.utils.formats import date_format
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.strings import LazyI18nString
from pretix.base.models import Event, ItemVariation, LogEntry, OrderPosition
from pretix.base.models import (
CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
)
from pretix.base.signals import logentry_display
OVERVIEW_BLACKLIST = [
@@ -122,7 +124,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
'pretix.event.order.payment.changed': _('The payment method has been changed.'),
'pretix.event.order.email.sent': _('An unindentified type email has been sent.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about '
'to expire.'),
@@ -167,6 +169,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.taxrule.added': _('The tax rule has been added.'),
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.plugins.enabled': _('A plugin has been enabled.'),
@@ -222,15 +227,24 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
dt = dateutil.parser.parse(data.get('datetime'))
tz = pytz.timezone(sender.settings.timezone)
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data:
try:
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if data.get('first'):
return _('Position #{posid} has been checked in manually at {datetime}.').format(
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted
datetime=dt_formatted,
list=checkin_list,
)
return _('Position #{posid} has been checked in again at {datetime}.').format(
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted
datetime=dt_formatted,
list=checkin_list
)
if logentry.action_type == 'pretix.team.member.added':

View File

@@ -64,15 +64,17 @@ class PermissionMiddleware(MiddlewareMixin):
return self._login_redirect(request)
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
# If this logic is updated, make sure to also update the logic in pretix/api/auth/permission.py
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return self._login_redirect(request)
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE and url_name != 'user.reauth':
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
if url_name != 'user.reauth':
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
request.session['pretix_auth_last_used'] = int(time.time())
request.session['pretix_auth_last_used'] = int(time.time())
if 'event' in url.kwargs and 'organizer' in url.kwargs:
request.event = Event.objects.filter(

View File

@@ -32,6 +32,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>

View File

@@ -2,65 +2,70 @@
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% block title %}{% trans "Check-ins" %}{% endblock %}
{% load bootstrap3 %}
{% block title %}
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% endblock %}
{% block content %}
<h1>{% trans "Check-ins" %}</h1>
<p>
<form class="form-inline helper-display-inline" action="" method="get">
<select name="status" class="form-control">
<option value="">{% trans "All status" %}</option>
<option value="1" {% if request.GET.status == "1" %}selected="selected"{% endif %}>{% trans "Checked in" %}</option>
<option value="0" {% if request.GET.status == "0" %}selected="selected"{% endif %}>{% trans "Not checked in" %}</option>
</select>
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<input type="text" name="user" class="form-control" placeholder="{% trans "Search user" %}" value="{{ request.GET.user }}">
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</form>
</p>
<h1>
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% if 'can_change_event_settings' in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit list" %}
</a>
{% endif %}
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>
{% trans "PDF" %}
</a>
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistcsv&checkinlistcsv-list={{ checkinlist.pk }}"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>
{% trans "CSV" %}
</a>
</h1>
<form class="row filter-form" action="" method="get">
<div class="col-md-4 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.user layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
<span class="hidden-md">
{% trans "Filter" %}
</span>
</button>
</div>
</form>
{% if entries|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No check-in record was found.
No attendee record was found.
{% endblocktrans %}
</p>
</div>
{% else %}
{% include "pretixcontrol/pagination.html" %}
<form method="post" action="">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th />
<th></th>
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'item'%}"><i class="fa fa-caret-up"></i></a></th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %} <a href="?{% url_replace request 'ordering' '-subevent'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'subevent'%}"><i class="fa fa-caret-up"></i></a></th>
{% endif %}
<th>{% trans "Email" %} <a href="?{% url_replace request 'ordering' '-email'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'email'%}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Name" %} <a href="?{% url_replace request 'ordering' '-name'%}"><i class="fa fa-caret-down"></i></a>
@@ -73,20 +78,16 @@
</thead>
<tbody>
{% for e in entries %}
{% with e.checkins.first as checkin %}
<tr>
<td>
<input type="checkbox" name="checkin"
id="id_checkin" class=""
value="{{e.pk}}"/>
</td>
<td>
{% if "can_change_orders" in request.eventpermset %}
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
{% endif %}
</td>
<td>
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}">{{ e.order.code }}</a></strong>
</td>
<td>{{ e.item.name }}{% if e.variation %} {{ e.variation }}{% endif %}</td>
{% if request.event.has_subevents %}
<td>{{ e.subevent.name }} {{ e.subevent.get_date_range_display }}</td>
{% endif %}
<td>{{ e.order.email }}</td>
<td>
{% if e.addon_to %}
@@ -96,26 +97,27 @@
{% endif %}
</td>
<td>
{% if not checkin %}
{% if not e.last_checked_in %}
<span class="label label-danger">{% trans "Not checked in" %}</span>
{% else %}
<span class="label label-success">{% trans "Checked in" %}</span>
{% endif %}
</td>
<td>
{% if checkin %}
{{ checkin.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if e.last_checked_in %}
{{ e.last_checked_in_aware|date:"SHORT_DATETIME_FORMAT" }}
{% endif %}
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Check-In selected attendees" %}
</button>
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Check-In selected attendees" %}
</button>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}

View File

@@ -0,0 +1,25 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete check-in list" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete check-in list" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans with name=checkinlist.name %}Are you sure you want to delete the check-in list <strong>{{ name }}</strong>?{% endblocktrans %}</p>
{% if checkinlist.checkins.exists > 0 %}
<p>{% blocktrans trimmed with num=checkinlist.checkins.count %}
This will delete the information of <strong>{{ num }}</strong> check-ins as well.
{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.checkinlists" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}
{% if checkinlist %}
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% else %}
{% trans "Check-in list" %}
{% endif %}
{% endblock %}
{% block inside %}
{% if checkinlist %}
<h1>{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}</h1>
{% else %}
<h1>{% trans "Check-in list" %}</h1>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %}
{% if form.subevent %}
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
<legend>{% trans "Products" %}</legend>
<p>
{% blocktrans trimmed %}
Please select the products that should be part of this check-in list.
{% endblocktrans %}
</p>
{% bootstrap_field form.all_products layout="control" %}
{% bootstrap_field form.limit_products layout="control" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Check-in lists" %}{% endblock %}
{% block inside %}
<h1>{% trans "Check-in lists" %}</h1>
<p>
{% blocktrans trimmed %}
You can create check-in lists that you can use e.g. at the entrance of your event to track who is coming
and if they actually bought a ticket. You can do this process by printing out the list on paper, using this
web interface or by using one of our mobile or desktop apps to automatically scan tickets.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can create multiple check-in lists to separate multiple parts of your event, for example if you have
separate entries for multiple ticket types. Different check-in lists are completely independent: If a ticket
shows up on two lists, it is valid once on every list. This might be useful if you run a festival with
festival passes that allow access to every or multiple performances as well as tickets only valid for single
performances.
{% endblocktrans %}
</p>
{% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get">
<p>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
{% endif %}
{% if checkinlists|length == 0 %}
<div class="empty-collection">
<p>
{% if request.GET.subevent %}
{% trans "Your search did not match any check-in lists." %}
{% else %}
{% blocktrans trimmed %}
You haven't created any check-in lists yet.
{% endblocktrans %}
{% endif %}
</p>
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
{% trans "Create a new check-in list" %}</a>
{% endif %}
</div>
{% else %}
{% if "can_change_event_settings" in request.eventpermset %}
<p>
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}
</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Check-in lists" %}</th>
<th>{% trans "Checked in" %}</th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for cl in checkinlists %}
<tr>
<td>
<strong><a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
</td>
<td>
<div class="quotabox availability">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
</div>
</div>
<div class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} / {{ cl.position_count|default_if_none:"0" }}
</div>
</div>
</td>
{% if request.event.has_subevents %}
<td>{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display }}</td>
{% endif %}
<td>
{% if cl.all_products %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in cl.limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td class="text-right">
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -101,12 +101,6 @@
{% trans "Waiting list" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.checkins' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.checkins" %}class="active"{% endif %}>
{% trans "Check-ins" %}
</a>
</li>
</ul>
</li>
{% endif %}
@@ -119,6 +113,15 @@
</a>
</li>
{% endif %}
{% if 'can_view_orders' in request.eventpermset %}
<li>
<a href="{% url 'control:event.orders.checkinlists' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.orders.checkin" in url_name %}class="active"{% endif %}>
<i class="fa fa-check-square-o fa-fw"></i>
{% trans "Check-in lists" %}
</a>
</li>
{% endif %}
{% for nav in nav_event %}
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}

View File

@@ -11,12 +11,14 @@
{% bootstrap_field form.invoice_address_required layout="control" %}
{% bootstrap_field form.invoice_name_required layout="control" %}
{% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_address_vatid layout="control" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
{% bootstrap_field form.invoice_renderer layout="control" %}
{% bootstrap_field form.invoice_language layout="control" %}
{% bootstrap_field form.invoice_include_free layout="control" %}
{% bootstrap_field form.invoice_attendee_name layout="control" %}
{% bootstrap_field form.invoice_address_from layout="control" %}
{% bootstrap_field form.invoice_introductory_text layout="control" %}
{% bootstrap_field form.invoice_additional_text layout="control" %}

View File

@@ -35,7 +35,7 @@
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
&lt;div class="pretix-widget-info-message"&gt;
{% blocktrans trimmed with a_attr='target="_blank" href="'|add:indexurl|add:'"'|safe %}
{% blocktrans trimmed with a_attr='target="_blank" rel="noopener" href="'|add:indexurl|add:'"'|safe %}
JavaScript is disabled in your browser. To access our ticket shop without JavaScript,
please &lt;a {{ a_attr }}&gt;click here&lt;/a&gt;.
{% endblocktrans %}
@@ -44,7 +44,7 @@
&lt;/noscript&gt;
</pre>
<p>
<a href="https://docs.pretix.eu/en/latest/user/events/widget.html" target="_blank">
<a href="https://docs.pretix.eu/en/latest/user/events/widget.html" target="_blank" rel="noopener">
<span class="fa fa-question-circle"></span>
{% trans "Read our documentation for more information" %}
</a>

View File

@@ -18,7 +18,7 @@
{% if possible %}
<p>{% blocktrans %}Are you sure you want to delete the product <strong>{{ item }}</strong>?{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans %}You cannot delete the product <strong>{{ item }}</strong> because it already has been ordered, but you can deactive it.{% endblocktrans %}</p>
<p>{% blocktrans %}You cannot delete the product <strong>{{ item }}</strong> because it already has been ordered, but you can deactivate it.{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.items" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">

View File

@@ -31,6 +31,10 @@
<thead>
<tr>
<th>{% trans "Product name" %}</th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th>{% trans "Category" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
@@ -46,7 +50,31 @@
<a href="
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i.name }}</a>
{% if not i.active %}</strike>{% endif %}
</strong></td>
</strong>
</td>
<td>
{% if i.available_from or i.available_until %}
<span class="fa fa-clock-o fa-fw text-muted" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}">
</span>
{% endif %}
</td>
<td>
{% if i.admission %}
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket" %}"></span>
{% endif %}
</td>
<td>
{% if i.var_count %}
<span class="fa fa-list-ul fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
{% endif %}
</td>
<td>
{% if i.hide_without_voucher %}
<span class="fa fa-ticket fa-fw text-muted" data-toggle="tooltip" title="{% trans "Only visible with a voucher" %}"></span>
{% elif i.require_voucher %}
<span class="fa fa-ticket fa-fw text-muted" data-toggle="tooltip" title="{% trans "Can only bought using a voucher" %}"></span>
{% endif %}
</td>
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>
<td>
<a href="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a>

View File

@@ -187,7 +187,9 @@
{{ line.variation }}
{% endif %}
{% if line.checkins.all %}
<span class="fa fa-check" data-toggle="tooltip" title="{% blocktrans trimmed with date=line.checkins.all.0.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
{% for c in line.checkins.all %}
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
{% endfor %}
{% endif %}
{% if line.voucher %}
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}

View File

@@ -4,23 +4,36 @@
{% load order_overview %}
{% block title %}{% trans "Data export" %}{% endblock %}
{% block content %}
<h1>{% trans "Data export" %}</h1>
<h1>
{% trans "Data export" %}
{% if "identifier" in request.GET %}
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
{% endif %}
</h1>
{% for e in exporters %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ e.verbose_name }}</h3>
<h3 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#{{ e.identifier }}">
{{ e.verbose_name }}
<i class="fa fa-angle-down collapse-indicator"></i>
</a>
</h3>
</div>
<div class="panel-body">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='horizontal' %}
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>
</form>
<div id="{{ e.identifier }}" class="panel-collapse collapse {% if "identifier" in request.GET %}in
{% endif %}">
<div class="panel-body">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='horizontal' %}
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>
</form>
</div>
</div>
</div>
{% endfor %}

View File

@@ -1,20 +1,46 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% load eventurl %}
{% block title %}{% trans "Organizers" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizers" %}</h1>
<p>{% trans "The list below shows all organizer accounts you have administrative access to." %}</p>
<form class="row filter-form" action="" method="get">
<div class="col-md-10 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
<span class="hidden-md">
{% trans "Filter" %}
</span>
</button>
</div>
</form>
{% if request.user.is_superuser %}
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new organizer" %}
</a>
<p>
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new organizer" %}
</a>
</p>
{% endif %}
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Organizer name" %}</th>
<th>
{% trans "Organizer name" %}
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Short form" %}
<a href="?{% url_replace request 'ordering' '-slug' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'slug' %}"><i class="fa fa-caret-up"></i></a>
</th>
</tr>
</thead>
<tbody>
@@ -23,6 +49,7 @@
<td><strong>
<a href="{% url "control:organizer" organizer=o.slug %}">{{ o.name }}</a>
</strong></td>
<td>{{ o.slug }}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -123,6 +123,72 @@
{% bootstrap_field f.price layout="control" %}
{% endfor %}
</fieldset>
<fieldset>
<legend>{% trans "Check-in lists" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
{{ cl_formset.management_form }}
{% bootstrap_formset_errors cl_formset %}
<div data-formset-body>
{% for form in cl_formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.all_products layout="control" %}
{% bootstrap_field form.limit_products layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ cl_formset.empty_form.id }}
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
</button>
</p>
</fieldset>
</div>
{% if subevent.pk %}
<div class="col-xs-12 col-lg-2">

View File

@@ -64,6 +64,9 @@
</div>
</div>
</div>
{% if form.subevent %}
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
</fieldset>

View File

@@ -164,7 +164,14 @@ urlpatterns = [
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
url(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'),
url(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
url(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(), name='event.orders.waitinglist.delete'),
url(r'^checkins/$', checkin.CheckInView.as_view(), name='event.orders.checkins'),
url(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
name='event.orders.waitinglist.delete'),
url(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
url(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
url(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
url(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
name='event.orders.checkinlists.edit'),
url(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),
name='event.orders.checkinlists.delete'),
])),
]

View File

@@ -1,122 +1,221 @@
import dateutil.parser
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce
from django.shortcuts import redirect
from django.utils.timezone import now
from django.db import transaction
from django.db.models import Max, OuterRef, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
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.views.generic import ListView
from django.views.generic import DeleteView, ListView
from pytz import UTC
from pretix.base.models import Checkin, Item, Order, OrderPosition
from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.control.forms.checkin import CheckinListForm
from pretix.control.forms.filter import CheckInFilterForm
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import CreateView, UpdateView
class CheckInView(EventPermissionRequiredMixin, ListView):
class CheckInListShow(EventPermissionRequiredMixin, ListView):
model = Checkin
context_object_name = 'entries'
paginate_by = 30
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
def get_queryset(self):
def get_queryset(self, filter=True):
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.list.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(order__event=self.request.event, order__status='p')
qs = OrderPosition.objects.filter(
order__event=self.request.event,
order__status=Order.STATUS_PAID,
subevent=self.list.subevent
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'addon_to')
# if this setting is False, we check only items for admission
if not self.request.event.settings.ticket_download_nonadm:
qs = qs.filter(item__admission=True)
if not self.list.all_products:
qs = qs.filter(item__in=self.list.limit_products.values_list('id', flat=True))
if self.request.GET.get("status", "") != "":
p = self.request.GET.get("status", "")
if p == '1':
# records with check-in record
qs = qs.filter(checkins__isnull=False)
elif p == '0':
qs = qs.filter(checkins__isnull=True)
if filter and self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.request.GET.get("user", "") != "":
u = self.request.GET.get("user", "")
qs = qs.filter(
Q(order__email__icontains=u) | Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
)
return qs
if self.request.GET.get("item", "") != "":
u = self.request.GET.get("item", "")
qs = qs.filter(item_id=u)
@cached_property
def filter_form(self):
return CheckInFilterForm(
data=self.request.GET,
event=self.request.event,
list=self.list
)
if self.request.GET.get("subevent", "") != "":
s = self.request.GET.get("subevent", "")
qs = qs.filter(subevent_id=s)
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.filter(position__order__event=self.request.event))
).select_related('order', 'item', 'addon_to')
if self.request.GET.get("ordering", "") != "":
p = self.request.GET.get("ordering", "")
keys_allowed = self.get_ordering_keys_mappings()
if p in keys_allowed:
mapped_field = keys_allowed[p]
if isinstance(mapped_field, dict):
order = mapped_field.pop('_order')
qs = qs.annotate(**mapped_field).order_by(order)
elif isinstance(mapped_field, (list, tuple)):
qs = qs.order_by(*mapped_field)
else:
qs = qs.order_by(mapped_field)
return qs.distinct()
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = Item.objects.filter(event=self.request.event)
ctx['filtered'] = ("status" in self.request.GET or "user" in self.request.GET or "item" in self.request.GET
or "subevent" in self.request.GET)
ctx['checkinlist'] = self.list
ctx['filter_form'] = self.filter_form
for e in ctx['entries']:
if e.last_checked_in:
if isinstance(e.last_checked_in, str):
# Apparently only happens on SQLite
e.last_checked_in_aware = make_aware(dateutil.parser.parse(e.last_checked_in), UTC)
else:
e.last_checked_in_aware = e.last_checked_in
return ctx
def post(self, request, *args, **kwargs):
positions = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
order__event=self.request.event,
if "can_change_orders" not in request.eventpermset:
messages.error(request, _('You do not have permission to perform this action.'))
return redirect(reverse('control:event.orders.checkins', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}) + '?' + request.GET.urlencode())
positions = self.get_queryset(filter=False).filter(
pk__in=request.POST.getlist('checkin')
)
for op in positions:
created = False
if op.order.status == Order.STATUS_PAID:
ci, created = Checkin.objects.get_or_create(position=op, defaults={
ci, created = Checkin.objects.get_or_create(position=op, list=self.list, defaults={
'datetime': now(),
})
op.order.log_action('pretix.control.views.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': created,
'datetime': now()
'datetime': now(),
'list': self.list.pk
}, user=request.user)
messages.success(request, _('The selected tickets have been marked as checked in.'))
return redirect(reverse('control:event.orders.checkins', kwargs={
return redirect(reverse('control:event.orders.checkinlists.show', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
'organizer': self.request.event.organizer.slug,
'list': self.list.pk
}) + '?' + request.GET.urlencode())
@staticmethod
def get_ordering_keys_mappings():
return {
'code': 'order__code',
'-code': '-order__code',
'email': 'order__email',
'-email': '-order__email',
# Set nulls_first to be consistent over databases
'status': F('checkins__id').asc(nulls_first=True),
'-status': F('checkins__id').desc(nulls_last=True),
'timestamp': F('checkins__datetime').asc(nulls_first=True),
'-timestamp': F('checkins__datetime').desc(nulls_last=True),
'item': ('item__name', 'variation__value'),
'-item': ('-item__name', 'variation__value'),
'subevent': ('subevent__date_from', 'subevent__name'),
'-subevent': ('-subevent__date_from', '-subevent__name'),
'name': {'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
'-name': {'_order': F('display_name').desc(nulls_last=True),
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')},
}
class CheckinListList(EventPermissionRequiredMixin, ListView):
model = CheckinList
context_object_name = 'checkinlists'
paginate_by = 30
permission = 'can_view_orders'
template_name = 'pretixcontrol/checkin/lists.html'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related("limit_products")
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
if self.request.GET.get("subevent", "") != "":
s = self.request.GET.get("subevent", "")
qs = qs.filter(subevent_id=s)
return qs
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
model = CheckinList
form_class = CheckinListForm
template_name = 'pretixcontrol/checkin/list_edit.html'
permission = 'can_change_event_settings'
context_object_name = 'checkinlist'
def get_success_url(self) -> str:
return reverse('control:event.orders.checkinlists', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
@transaction.atomic
def form_valid(self, form):
form.instance.event = self.request.event
messages.success(self.request, _('The new check-in list has been created.'))
ret = super().form_valid(form)
form.instance.log_action('pretix.event.checkinlist.added', user=self.request.user,
data=dict(form.cleaned_data))
return ret
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
model = CheckinList
form_class = CheckinListForm
template_name = 'pretixcontrol/checkin/list_edit.html'
permission = 'can_change_event_settings'
context_object_name = 'checkinlist'
def get_object(self, queryset=None) -> CheckinList:
try:
return self.request.event.checkin_lists.get(
id=self.kwargs['list']
)
except CheckinList.DoesNotExist:
raise Http404(_("The requested list does not exist."))
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed():
self.object.log_action(
'pretix.event.checkinlist.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
return super().form_valid(form)
def get_success_url(self) -> str:
return reverse('control:event.orders.checkinlists.show', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'list': self.object.pk
})
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
model = CheckinList
template_name = 'pretixcontrol/checkin/list_delete.html'
permission = 'can_change_event_settings'
context_object_name = 'checkinlist'
def get_object(self, queryset=None) -> CheckinList:
try:
return self.request.event.checkin_lists.get(
id=self.kwargs['list']
)
except CheckinList.DoesNotExist:
raise Http404(_("The requested list does not exist."))
@transaction.atomic
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.log_action(action='pretix.event.orders.deleted', user=request.user)
self.object.delete()
messages.success(self.request, _('The selected list has been deleted.'))
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.orders.checkinlists', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})

View File

@@ -13,12 +13,14 @@ from django.template.loader import get_template
from django.utils import formats
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _, ungettext
from pretix.base.models import (
Event, Item, Order, OrderPosition, RequiredAction, SubEvent, Voucher,
WaitingListEntry,
)
from pretix.base.models.checkin import CheckinList
from pretix.control.forms.event import CommentForm
from pretix.control.signals import (
event_dashboard_widgets, user_dashboard_widgets,
@@ -34,6 +36,9 @@ NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="
def base_widgets(sender, subevent=None, **kwargs):
prodc = Item.objects.filter(
event=sender, active=True,
).filter(
(Q(available_until__isnull=True) | Q(available_until__gte=now())) &
(Q(available_from__isnull=True) | Q(available_from__lte=now()))
).count()
if subevent:
@@ -71,7 +76,7 @@ def base_widgets(sender, subevent=None, **kwargs):
'url': reverse('control:event.orders', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug
})
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
@@ -80,7 +85,7 @@ def base_widgets(sender, subevent=None, **kwargs):
'url': reverse('control:event.orders.overview', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug
})
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': NUM_WIDGET.format(
@@ -90,7 +95,7 @@ def base_widgets(sender, subevent=None, **kwargs):
'url': reverse('control:event.orders.overview', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug
})
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': NUM_WIDGET.format(num=prodc, text=_('Active products')),
@@ -186,7 +191,7 @@ def shop_state_widget(sender, **kwargs):
@receiver(signal=event_dashboard_widgets)
def checkin_widget(sender, **kwargs):
def checkin_widget(sender, subevent=None, **kwargs):
size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p')
checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False)
@@ -195,15 +200,22 @@ def checkin_widget(sender, **kwargs):
size_qs = size_qs.filter(item__admission=True)
checked_qs = checked_qs.filter(item__admission=True)
return [{
'content': NUM_WIDGET.format(num='{}/{}'.format(checked_qs.count(), size_qs.count()), text=_('Checked in')),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.orders.checkins', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug
widgets = []
qs = sender.checkin_lists.filter(subevent=subevent)
qs = CheckinList.annotate_with_numbers(qs, sender)
for cl in qs:
widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(cl.checkin_count, cl.position_count),
text=_('Checked in {list}').format(list=escape(cl.name))),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.orders.checkinlists.show', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
'list': cl.pk
})
})
}]
return widgets
@receiver(signal=event_dashboard_widgets)

View File

@@ -46,6 +46,8 @@ class ItemList(ListView):
def get_queryset(self):
return Item.objects.filter(
event=self.request.event
).annotate(
var_count=Count('variations')
).prefetch_related("category")

View File

@@ -15,6 +15,7 @@ from django.views.generic import ListView
from formtools.wizard.views import SessionWizardView
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.models import Event, Organizer, Quota, Team
from pretix.control.forms.event import (
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
@@ -118,6 +119,14 @@ class EventWizard(SessionWizardView):
ctx['organizer'] = self.get_cleaned_data_for_step('foundation').get('organizer')
return ctx
def render(self, form=None, **kwargs):
if self.steps.current != 'foundation':
fdata = self.get_cleaned_data_for_step('foundation')
if fdata is None:
return self.render_goto_step('foundation')
return super().render(form, **kwargs)
def get_form_kwargs(self, step=None):
kwargs = {
'user': self.request.user
@@ -135,7 +144,7 @@ class EventWizard(SessionWizardView):
basics_data = self.get_cleaned_data_for_step('basics')
copy_data = self.get_cleaned_data_for_step('copy')
with transaction.atomic():
with transaction.atomic(), language(basics_data['locale']):
event = form_dict['basics'].instance
event.organizer = foundation_data['organizer']
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
@@ -157,7 +166,7 @@ class EventWizard(SessionWizardView):
t.limit_events.add(event)
if event.has_subevents:
event.subevents.create(
se = event.subevents.create(
name=event.name,
date_from=event.date_from,
date_to=event.date_to,
@@ -183,6 +192,17 @@ class EventWizard(SessionWizardView):
if copy_data and copy_data['copy_from_event']:
from_event = copy_data['copy_from_event']
event.copy_data_from(from_event)
elif event.has_subevents:
event.checkin_lists.create(
name=str(se),
all_products=True,
subevent=se
)
else:
event.checkin_lists.create(
name=_('Default'),
all_products=True
)
event.settings.set('timezone', basics_data['timezone'])
event.settings.set('locale', basics_data['locale'])

View File

@@ -147,7 +147,7 @@ class OrderDetail(OrderView):
).select_related(
'item', 'variation', 'addon_to', 'tax_rule'
).prefetch_related(
'item__questions', 'answers', 'answers__question', 'checkins'
'item__questions', 'answers', 'answers__question', 'checkins', 'checkins__list'
).order_by('positionid')
positions = []
@@ -906,9 +906,21 @@ class ExportMixin:
responses = register_data_exporters.send(self.request.event)
for receiver, response in responses:
ex = response(self.request.event)
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
continue
# Use form parse cycle to generate useful defaults
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
test_form.is_valid()
initial = {
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
}
ex.form = ExporterForm(
data=(self.request.POST if self.request.method == 'POST' else None),
prefix=ex.identifier
prefix=ex.identifier,
initial=initial
)
ex.form.fields = ex.export_form_fields
exporters.append(ex)

View File

@@ -17,6 +17,7 @@ from pretix.base.models import Organizer, Team, TeamInvite, User
from pretix.base.models.event import EventMetaProperty
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.filter import OrganizerFilterForm
from pretix.control.forms.organizer import (
EventMetaPropertyForm, OrganizerDisplaySettingsForm, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
@@ -34,10 +35,22 @@ class OrganizerList(ListView):
paginate_by = 30
def get_queryset(self):
qs = Organizer.objects.all()
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.request.user.is_superuser:
return Organizer.objects.all()
return qs
else:
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
return qs.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
return ctx
@cached_property
def filter_form(self):
return OrganizerFilterForm(data=self.request.GET, request=self.request)
class InviteForm(forms.Form):

View File

@@ -11,13 +11,15 @@ from django.utils.functional import cached_property
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from pretix.base.models.checkin import CheckinList
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation
from pretix.control.forms.checkin import CheckinListForm
from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import (
QuotaFormSet, SubEventForm, SubEventItemForm, SubEventItemVariationForm,
SubEventMetaValueForm,
CheckinListFormSet, QuotaFormSet, SubEventForm, SubEventItemForm,
SubEventItemVariationForm, SubEventMetaValueForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.event import MetaDataEditorMixin
@@ -132,6 +134,41 @@ class SubEventEditorMixin(MetaDataEditorMixin):
data=(self.request.POST if self.request.method == "POST" else None)
)
@cached_property
def cl_formset(self):
extra = 0
kwargs = {}
if self.copy_from:
kwargs['initial'] = [
{
'name': cl.name,
'all_products': cl.all_products,
'limit_products': cl.limit_products.all(),
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
]
extra = len(kwargs['initial'])
elif not self.object:
kwargs['initial'] = [
{
'name': '',
'all_products': True,
}
]
extra = 1
formsetclass = inlineformset_factory(
SubEvent, CheckinList,
form=CheckinListForm, formset=CheckinListFormSet,
can_order=False, can_delete=True, extra=extra,
)
if self.object:
kwargs['queryset'] = self.object.checkinlist_set.prefetch_related('limit_products')
return formsetclass(self.request.POST if self.request.method == "POST" else None,
instance=self.object,
event=self.request.event, **kwargs)
@cached_property
def formset(self):
extra = 0
@@ -161,6 +198,38 @@ class SubEventEditorMixin(MetaDataEditorMixin):
instance=self.object,
event=self.request.event, **kwargs)
def save_cl_formset(self, obj):
for form in self.cl_formset.initial_forms:
if form in self.cl_formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user)
form.instance.delete()
form.instance.pk = None
elif form.has_changed():
form.instance.subevent = obj
form.instance.event = obj.event
form.save()
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
form.instance.log_action(
'pretix.event.checkinlist.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
for form in self.cl_formset.extra_forms:
if not form.has_changed():
continue
if self.formset._should_delete_form(form):
continue
form.instance.subevent = obj
form.instance.event = obj.event
form.save()
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
form.instance.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data)
def save_formset(self, obj):
for form in self.formset.initial_forms:
if form in self.formset.deleted_forms:
@@ -204,6 +273,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['cl_formset'] = self.cl_formset
ctx['itemvar_forms'] = self.itemvar_forms
ctx['meta_forms'] = self.meta_forms
return ctx
@@ -259,7 +329,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()
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
@@ -288,6 +358,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
@transaction.atomic
def form_valid(self, form):
self.save_formset(self.object)
self.save_cl_formset(self.object)
self.save_meta()
for f in self.itemvar_forms:
@@ -355,6 +426,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
form.instance.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
self.save_formset(form.instance)
self.save_cl_formset(form.instance)
for f in self.itemvar_forms:
f.instance.subevent = form.instance
f.save()

View File

@@ -1,6 +1,7 @@
import contextlib
from django.db import transaction
from django.db.models.expressions import OrderBy
class DummyRollbackException(Exception):
@@ -34,3 +35,25 @@ def casual_reads():
Kept for backwards compatibility.
"""
yield
class FixedOrderBy(OrderBy):
# Workaround for https://code.djangoproject.com/ticket/28848
template = '%(expression)s %(ordering)s'
def as_sql(self, compiler, connection, template=None, **extra_context):
if not template:
if self.nulls_last:
template = '%s NULLS LAST' % self.template
elif self.nulls_first:
template = '%s NULLS FIRST' % self.template
connection.ops.check_expression_support(self)
expression_sql, params = compiler.compile(self.expression)
placeholders = {
'expression': expression_sql,
'ordering': 'DESC' if self.descending else 'ASC',
}
placeholders.update(extra_context)
template = template or self.template
params = params * template.count('%(expression)s')
return (template % placeholders).rstrip(), params

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