Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3acc29f51a | ||
|
|
4bae0d1b81 | ||
|
|
c38a850294 | ||
|
|
a7ec475c40 | ||
|
|
ac1467bd4b | ||
|
|
3f0af67345 | ||
|
|
4d14b6c096 | ||
|
|
cb789bc06c | ||
|
|
17dad33f8b | ||
|
|
1d5c160e1d | ||
|
|
f04b7fa365 | ||
|
|
fa011fbdce | ||
|
|
759c5374d9 | ||
|
|
f631acdf18 | ||
|
|
b2dfd8ab11 | ||
|
|
43f4803da7 | ||
|
|
019d8220b8 | ||
|
|
b946010bdb | ||
|
|
c3097b12c3 | ||
|
|
12a53710c3 | ||
|
|
983326e610 | ||
|
|
b4eb707b38 | ||
|
|
e44f34f0a9 | ||
|
|
3c762adbf4 | ||
|
|
ebabd20d09 | ||
|
|
8694e1901a | ||
|
|
0f2875e89a | ||
|
|
74d9921be1 | ||
|
|
41e56adfdb | ||
|
|
4ff1d302d9 | ||
|
|
d6e213d51a | ||
|
|
2a4deeba55 | ||
|
|
24b1d2afcb | ||
|
|
5cea3d824a | ||
|
|
78a8a7d744 | ||
|
|
a3bf85754a | ||
|
|
006b6fd5e8 | ||
|
|
8ff2c42070 | ||
|
|
c5d18c6884 | ||
|
|
48b3621f1e | ||
|
|
fb716eb498 | ||
|
|
5d8e294350 | ||
|
|
8a96d8c24e | ||
|
|
6396d2f922 | ||
|
|
ed7e90451b | ||
|
|
f5990dd5c4 | ||
|
|
f29f615aec | ||
|
|
f1300badb8 | ||
|
|
88a1cddb23 | ||
|
|
d53fbc52d1 | ||
|
|
ec9511c17a | ||
|
|
3f2bb3beae | ||
|
|
c8b96696b0 | ||
|
|
0bb6b53ebc | ||
|
|
7400c2373b | ||
|
|
635329f102 | ||
|
|
b5a2123d1c | ||
|
|
396c558b25 | ||
|
|
60e178d821 | ||
|
|
7769aaccea | ||
|
|
5ff6d0b014 | ||
|
|
7d9a1b5e0c | ||
|
|
127086a50e | ||
|
|
90d14c004f | ||
|
|
8c457c057e | ||
|
|
36215a989a | ||
|
|
74259bd4c6 | ||
|
|
4561b7dce2 | ||
|
|
89d6166dbe | ||
|
|
f451977a04 | ||
|
|
aac05727ed | ||
|
|
c9ae65a9a8 | ||
|
|
f4a9dbb546 | ||
|
|
3eec791584 | ||
|
|
57473da182 | ||
|
|
fd90752a12 | ||
|
|
0d3f5e0c32 | ||
|
|
c98b0aac90 | ||
|
|
3de85975d6 | ||
|
|
962cdef69a | ||
|
|
b301d20488 | ||
|
|
1b2895b0ca | ||
|
|
d6943bb1dd | ||
|
|
51d74503ae | ||
|
|
5eac3cf9cf | ||
|
|
27d6f24c3c | ||
|
|
d929b163db | ||
|
|
571b3fbfa8 | ||
|
|
db05f8eaa3 | ||
|
|
d8eba81efc | ||
|
|
429ef67bbf | ||
|
|
e5d9e69e3e | ||
|
|
dc32bdc474 | ||
|
|
c45b709d54 | ||
|
|
e1be084d98 | ||
|
|
20b8aafe11 | ||
|
|
c719adf235 | ||
|
|
d08a0bdb00 | ||
|
|
8294391ebc | ||
|
|
394c206133 | ||
|
|
2fcd561ff7 | ||
|
|
f95b77fd7a |
14
.gitattributes
vendored
@@ -3,4 +3,18 @@ src/static/lightbox/* linguist-vendored
|
||||
src/static/typeahead/* linguist-vendored
|
||||
src/static/moment/* linguist-vendored
|
||||
src/static/datetimepicker/* linguist-vendored
|
||||
src/static/colorpicker/* linguist-vendored
|
||||
src/static/fileupload/* linguist-vendored
|
||||
src/static/charts/* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.eot binary
|
||||
*.otf binary
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
*.zip binary
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.jpg binary
|
||||
|
||||
@@ -2,11 +2,31 @@ before_script:
|
||||
tests:
|
||||
stage: test
|
||||
script:
|
||||
- virtualenv-3.4 env
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache bash .travis.sh style
|
||||
- XDG_CACHE_HOME=/cache bash .travis.sh tests
|
||||
- XDG_CACHE_HOME=/cache bash .travis.sh doctests
|
||||
tags:
|
||||
- python3
|
||||
pypi:
|
||||
stage: release
|
||||
script:
|
||||
- cp /keys/.pypirc ~/.pypirc
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
|
||||
- cd src
|
||||
- python setup.py sdist upload
|
||||
- python setup.py bdist_wheel upload
|
||||
tags:
|
||||
- python3
|
||||
only:
|
||||
- release
|
||||
artifacts:
|
||||
paths:
|
||||
- src/dist/
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- release
|
||||
|
||||
@@ -51,7 +51,7 @@ If there is a problem, a status code in the ``5xx`` range will be returned.
|
||||
Performance monitoring
|
||||
----------------------
|
||||
|
||||
If you to generate detailled performance statistics of your pretix installation, there is an
|
||||
If you want to generate detailed performance statistics of your pretix installation, there is an
|
||||
endpoint at ``https://pretix.mydomain.com/metrics`` (no slash at the end) which returns a
|
||||
number of values in the text format understood by monitoring tools like Prometheus_. This data
|
||||
is only collected and exposed if you enable it in the :ref:`metrics-settings` section of your
|
||||
|
||||
@@ -55,16 +55,17 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'pretix'
|
||||
copyright = '2014-2016, Raphael Michel'
|
||||
copyright = '2014-2017, Raphael Michel'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.0.0'
|
||||
from pretix import __version__
|
||||
version = '.'.join(__version__.split('.')[:2])
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.0.0'
|
||||
release = __version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -25,7 +25,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, checkout_confirm_messages
|
||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, checkout_confirm_messages
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
|
||||
@@ -20,13 +20,10 @@ Organizers and events
|
||||
.. autoclass:: pretix.base.models.Organizer
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.OrganizerPermission
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.Event
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.EventPermission
|
||||
.. autoclass:: pretix.base.models.Team
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.RequiredAction
|
||||
@@ -67,7 +64,7 @@ Carts and Orders
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.QuestionAnswer
|
||||
:members:
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.Checkin
|
||||
:members:
|
||||
@@ -92,5 +89,3 @@ Vouchers
|
||||
|
||||
.. autoclass:: pretix.base.models.Voucher
|
||||
:members:
|
||||
|
||||
.. _cleanerversion: https://github.com/swisscom/cleanerversion
|
||||
|
||||
@@ -40,7 +40,7 @@ automatically). If you are working on Ubuntu or Debian, we strongly recommend up
|
||||
your pip and setuptools installation inside the virtual environment, otherwise some of
|
||||
the dependencies might fail::
|
||||
|
||||
pip3 install -U pip setuptools==28.6.1
|
||||
pip3 install -U pip setuptools
|
||||
|
||||
Working with the code
|
||||
---------------------
|
||||
@@ -81,7 +81,7 @@ and head to http://localhost:8000/
|
||||
|
||||
As we did not implement an overall front page yet, you need to go directly to
|
||||
http://localhost:8000/control/ for the admin view or, if you imported the test
|
||||
data as suggested above, to the event page at http://localhost:8000/bigevents/2017/
|
||||
data as suggested above, to the event page at http://localhost:8000/bigevents/2018/
|
||||
|
||||
.. note:: If you want the development server to listen on a different interface or
|
||||
port (for example because you develop on `pretixdroid`_), you can check
|
||||
|
||||
@@ -26,8 +26,11 @@ pretix/
|
||||
multidomain/
|
||||
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
||||
|
||||
static/
|
||||
Contains all static files (CSS, JavaScript, images)
|
||||
static/
|
||||
Contains all static files (CSS, JavaScript, images)
|
||||
|
||||
static/
|
||||
Contains some pretix plugins that ship with pretix itself
|
||||
|
||||
tests/
|
||||
This is the root directory for all test codes. It includes subdirectories ``base``,
|
||||
@@ -37,7 +40,8 @@ tests/
|
||||
|
||||
Language files
|
||||
--------------
|
||||
The language files live in ``locale/*/LC_MESSAGES/``.
|
||||
|
||||
The language files live in ``pretix/locale/*/LC_MESSAGES/``.
|
||||
|
||||
Static files
|
||||
------------
|
||||
@@ -49,27 +53,14 @@ We use libsass as a preprocessor for CSS. Our own sass code is built in the same
|
||||
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
|
||||
|
||||
pretix.control
|
||||
pretixcontrol has two main SCSS files, ``pretix/control/static/pretixcontrol/scss/main.scss`` and
|
||||
``pretix/control/static/pretixcontrol/scss/auth.scss``, importing everything else.
|
||||
pretixcontrol has two main SCSS files, ``pretix/static/pretixcontrol/scss/main.scss`` and
|
||||
``pretix/static/pretixcontrol/scss/auth.scss``, importing everything else.
|
||||
|
||||
pretix.presale
|
||||
pretixpresale has one main SCSS files, ``pretix/control/static/pretix/presale/scss/main.scss``,
|
||||
pretixpresale has one main SCSS files, ``pretix/pretixpresale/scss/main.scss``,
|
||||
importing everything else.
|
||||
|
||||
3rd-party assets
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Bootstrap
|
||||
Bootstrap lives vendored at ``static/bootstrap/``
|
||||
|
||||
Font Awesome
|
||||
Font Awesome lives vendored at ``static/fontawesome/``
|
||||
|
||||
jQuery
|
||||
jQuery lives as a single JavaScript file in ``static/jquery/js/``
|
||||
|
||||
jQuery plugin: Django formsets
|
||||
Our own modified version of `django-formset-js`_ is available as an independent
|
||||
django app and installed via ``pip``.
|
||||
|
||||
.. _django-formset-js: https://github.com/pretix/django-formset-js
|
||||
Most client-side 3rd-party assets are vendored in various subdirectories of ``pretix/static``.
|
||||
|
||||
@@ -6,6 +6,7 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
user/index
|
||||
admin/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
@@ -15,15 +15,26 @@ ways that pretix itself is:
|
||||
* PDF ticket output
|
||||
|
||||
The following plugins are not shipped with pretix but are maintained by the
|
||||
same team:
|
||||
same team. We update them regularly to make them compatible with the latest
|
||||
pretix releases:
|
||||
|
||||
* `SEPA direct debit`_
|
||||
* `Pages`_
|
||||
* `Passbook/Wallet ticket output`_
|
||||
* `Cartshare`_
|
||||
* `Fontpack Free fonts`_
|
||||
|
||||
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
|
||||
Please get in touch with the pretix team if you want to have them for your self-hosted
|
||||
pretix installation:
|
||||
|
||||
* Campaign tracking
|
||||
* Integration with Google Analytics and Facebook Pixel
|
||||
* Integration with Slack
|
||||
* Integration with MailChimp
|
||||
|
||||
The following plugins are from independent third-party authors, so we can make
|
||||
no statements about their stability:
|
||||
no statements about their stability or compatibility:
|
||||
|
||||
* `esPass ticket output`_
|
||||
* `IcePay integration`_
|
||||
@@ -34,3 +45,4 @@ no statements about their stability:
|
||||
.. _Pages: https://github.com/pretix/pretix-pages
|
||||
.. _esPass ticket output: https://github.com/esPass/pretix-espass
|
||||
.. _IcePay integration: https://github.com/chotee/pretix-icepay
|
||||
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free
|
||||
|
||||
@@ -4,6 +4,10 @@ pretixdroid HTTP API
|
||||
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
|
||||
uses to communicate with the pretix server.
|
||||
|
||||
.. warning:: This API is intended **only** to serve the pretixdroid Android app. There are no backwards compatibility
|
||||
guarantees on this API. We will not add features that are not required for the Android App. There will be
|
||||
a proper general-use API for pretix at a later point in time.
|
||||
|
||||
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
|
||||
|
||||
Redeems a ticket, i.e. checks the user in.
|
||||
@@ -19,6 +23,16 @@ uses to communicate with the pretix server.
|
||||
|
||||
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3
|
||||
|
||||
You can optionally include the additional parameter ``datetime`` in the body containing an ISO8601-encoded
|
||||
datetime of the entry attempt. If you don't, the current date and time will be used.
|
||||
|
||||
You can optionally include the additional parameter ``force`` to indicate that the request should be logged
|
||||
regardless of previous check-ins for the same ticket. This might be useful if you made the entry decision offline.
|
||||
|
||||
You can optionally include the additional parameter ``nonce`` with a globally unique random value to identify this
|
||||
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
|
||||
failure.
|
||||
|
||||
**Example successful response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -51,9 +65,9 @@ uses to communicate with the pretix server.
|
||||
* ``unknown_ticket`` - Secret does not match a ticket in the database
|
||||
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/search/
|
||||
|
||||
@@ -97,6 +111,46 @@ uses to communicate with the pretix server.
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/download/
|
||||
|
||||
Download data for all tickets.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pretixdroid/api/demoorga/democon/download/?key=ABCDEF HTTP/1.1
|
||||
Host: demo.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCE6",
|
||||
"item": "Standard ticket",
|
||||
"variation": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"version": 2
|
||||
}
|
||||
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/status/
|
||||
|
||||
Returns status information, such as the total number of tickets and the
|
||||
|
||||
9
doc/user/index.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
User Guide
|
||||
==========
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
payments/index
|
||||
31
doc/user/payments/banktransfer.rst
Normal file
@@ -0,0 +1,31 @@
|
||||
.. _`banktransfer`:
|
||||
|
||||
Bank transfer
|
||||
=============
|
||||
|
||||
To accept payments with bank transfer, you only need to fill one important field in pretix' settings: In "Bank
|
||||
account details" you should specify everything one needs to know to transfer money to you, e.g. your IBAN and BIC,
|
||||
the name of your bank and for international transfers, preferably also your address and the bank's address.
|
||||
|
||||
pretix will automatically tell the user to include the order code in the payment reference so incoming transfers can
|
||||
automatically be matched to payments.
|
||||
|
||||
Importing payment data
|
||||
----------------------
|
||||
|
||||
The easiest way to import payment data is to download a CSV file from your online banking. Most banks provide a CSV
|
||||
export of some sort. You can go to "Import bank data" in pretix to upload a new file:
|
||||
|
||||
.. image:: img/bank1.png
|
||||
|
||||
If you upload a file for the first time, pretix will not know what information is contained in which column as every
|
||||
bank builds completely different CSV files. Therefore, pretix will ask you for that information. It will show you the
|
||||
data of the file you imported and ask you to define the column's meanings. You can select one column that contains
|
||||
the payment date and one that contains the paid amount. You can select multiple columns that contain information
|
||||
about the payer or the payment reference. All other columns will be ignored.
|
||||
|
||||
Once you continue, pretix will try to match the payments to the respective orders automatically. It will tell you how
|
||||
many orders could be processed correctly and how many could not. You can then go back to the upload page to see all
|
||||
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.
|
||||
52
doc/user/payments/fees.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
Payment method fees
|
||||
===================
|
||||
|
||||
Most external payment providers like PayPal or Stripe charge substantial fees for your service. In general, you have
|
||||
two options to deal with this:
|
||||
|
||||
1. Pay the fees yourself
|
||||
|
||||
2. Add the fees to your customer's total
|
||||
|
||||
The choice totally depends on you and what your customers expect from you. Option two might be appropriate if you
|
||||
offer different payment methods and want to encourage your customers to use the ones that come you cheaper, but you
|
||||
might also decide to go for option one to make it easier for customers who don't have the option.
|
||||
|
||||
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
|
||||
in case you are interested, here are all the details:
|
||||
|
||||
Payment fee calculation
|
||||
-----------------------
|
||||
|
||||
If you configure a fee for a payment method, there are two possible ways for us to calculate this. Let's
|
||||
assume that your payment provider, e.g. PayPal, charges you 5 % fees and you want to charge your users the
|
||||
same 5 %, such that for a ticket with a list price of 100 € you will get your full 100 €.
|
||||
|
||||
**Method A: Calculate the fee from the subtotal and add it to the bill.**
|
||||
|
||||
For a ticket price of 100 €, this will lead to the following calculation:
|
||||
|
||||
============================================== ============
|
||||
Ticket price 100.00 €
|
||||
pretix calculates the fee as 5 % of 100 € +5.00 €
|
||||
Subtotal that will be paid by the customer 105.00 €
|
||||
PayPal calculates its fee as 5 % of 105 € -5.25 €
|
||||
End total that is on your bank account **99.75 €**
|
||||
============================================== ============
|
||||
|
||||
**Method B (default): Calculate the fee from the total value including the fee.**
|
||||
|
||||
For a ticket price of 100 €, this will lead to the following calculation:
|
||||
|
||||
===================================================== =============
|
||||
Ticket price 100.00 €
|
||||
pretix calculates the fee as 100/(100 - 5) % of 100 € +5.26 €
|
||||
Subtotal that will be paid by the customer 105.26 €
|
||||
PayPal calculates its fee as 5 % of 105 € -5.26 €
|
||||
End total that is on your bank account **100.00 €**
|
||||
===================================================== =============
|
||||
|
||||
Due to the various rounding steps performed by pretix and by the payment provider, the end total on
|
||||
your bank account might stil vary by one cent.
|
||||
BIN
doc/user/payments/img/bank1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
doc/user/payments/img/bank2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
doc/user/payments/img/paypal1.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
doc/user/payments/img/paypal2.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
doc/user/payments/img/paypal3.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
doc/user/payments/img/paypal4.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
doc/user/payments/img/paypal5.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
doc/user/payments/img/paypal6.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
doc/user/payments/img/paypal7.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
doc/user/payments/img/paypal8.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
doc/user/payments/img/paypal_pretix.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
doc/user/payments/img/stripe1.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
doc/user/payments/img/stripe2.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
14
doc/user/payments/index.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Accepting payments
|
||||
==================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
overview
|
||||
fees
|
||||
paypal
|
||||
stripe
|
||||
banktransfer
|
||||
|
||||
34
doc/user/payments/overview.rst
Normal file
@@ -0,0 +1,34 @@
|
||||
Payment method overview
|
||||
=======================
|
||||
|
||||
pretix allows you to accept payments using a variety of payment methods to fit the needs of very different events.
|
||||
This page gives you a short overview over them and links to more detailled descriptions in some cases.
|
||||
|
||||
Payment methods are built as pretix plugins. For this reason, you might first need to enable a certain plugin at
|
||||
"Settings" → "Plugins" in your event settings. Then, you can configure them in detail at "Settings" -> "Payment".
|
||||
|
||||
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.
|
||||
|
||||
|
||||
50
doc/user/payments/paypal.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
.. _`paypal`:
|
||||
|
||||
PayPal
|
||||
======
|
||||
|
||||
To integrate PayPal with pretix, you first need to have an active PayPal merchant account. If you do not already have a
|
||||
PayPal account, you can create one on `paypal.com`_.
|
||||
If you look into pretix' settings, you are required to fill in two keys:
|
||||
|
||||
.. image:: img/paypal_pretix.png
|
||||
|
||||
Unfortunately, it is not straightforward how to get those keys from PayPal's website. In order to do so, you
|
||||
need to go to `developer.paypal.com`_ to link the account to your pretix event.
|
||||
Click on "Log In" in the top-right corner and log in with your PayPal account.
|
||||
|
||||
.. image:: img/paypal2.png
|
||||
|
||||
Then, click on "Dashboard" in the top-right corner.
|
||||
|
||||
.. image:: img/paypal3.png
|
||||
|
||||
In the dashboard, scroll down until you see the headline "REST API Apps". Click "Create App".
|
||||
|
||||
.. image:: img/paypal4.png
|
||||
|
||||
Enter any name for the application that helps you to identify it later. Then confirm with "Create App".
|
||||
|
||||
.. image:: img/paypal5.png
|
||||
|
||||
On the next page, before you do anything else, switch the mode on the right to "Live" to get the correct keys.
|
||||
Then, copy the "Client ID" and the "Secret" and enter them into the appropriate fields in the payment settings in
|
||||
pretix.
|
||||
|
||||
.. image:: img/paypal6.png
|
||||
|
||||
Finally, we need to create a webhook. The webhook tells PayPal to notify pretix e.g. if a payment gets cancelled so
|
||||
pretix can cancel the ticket as well. If you have multiple events connected to your PayPal account, you need multiple
|
||||
webhooks. To create one, scroll a bit down and click "Add Webhook".
|
||||
|
||||
.. image:: img/paypal7.png
|
||||
|
||||
Then, enter the webhook URL that you find on the pretix settings page. It should look similar to the one in the
|
||||
screenshot but contain your event name. Tick the box "All events" and save.
|
||||
|
||||
.. image:: img/paypal8.png
|
||||
|
||||
That's it, you are ready to go!
|
||||
|
||||
.. _paypal.com: https://www.paypal.com/webapps/mpp/account-selection
|
||||
.. _developer.paypal.com: https://developer.paypal.com/
|
||||
28
doc/user/payments/stripe.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. _stripe:
|
||||
|
||||
Stripe
|
||||
======
|
||||
|
||||
To integrate Stripe with pretix, you first need to have an active Stripe merchant account. If you do not already have a
|
||||
Stripe account, you can create one on `stripe.com`_. Then, click on "API" in the left navigation of the Stripe
|
||||
Dashboard. As you can see in the following screenshot, you will be presented with two sets of API keys, one for test
|
||||
and one for live payments. In each set, there is a secret and a publishable keys.
|
||||
|
||||
.. image:: img/stripe1.png
|
||||
|
||||
Choose one of the two sets and copy the two keys to the appropriate fields in pretix' settings. To perform actual
|
||||
payments, you will need to use the live keys, but you can use the test keys to test the payment flow before you go live.
|
||||
In test mode, you cannot use your real credit card, but only `test cards`_ like ``4242424242424242`` that you can
|
||||
find in Stripe's documentation.
|
||||
|
||||
If you want Stripe to notify pretix automatically once a payment gets cancelled, so pretix can cancel the ticket as
|
||||
well, you need to create a so-called webhook. To do so, click "Webhooks" on top of the page in the Stripe dashboard
|
||||
that you are currently on. Then, click "Add endpoint" and enter the URL that you find directly below the key
|
||||
configuration in pretix' settings.
|
||||
|
||||
.. image:: img/stripe2.png
|
||||
|
||||
Again, you can choose between live mode and test mode here.
|
||||
|
||||
.. _stripe.com: https://dashboard.stripe.com/register
|
||||
.. _test cards: https://stripe.com/docs/testing#cards
|
||||
@@ -16,3 +16,5 @@ recursive-include pretix/plugins/statistics/templates *
|
||||
recursive-include pretix/plugins/statistics/static *
|
||||
recursive-include pretix/plugins/stripe/templates *
|
||||
recursive-include pretix/plugins/stripe/static *
|
||||
recursive-include pretix/plugins/ticketoutputpdf/templates *
|
||||
recursive-include pretix/plugins/ticketoutputpdf/static *
|
||||
|
||||
@@ -23,9 +23,6 @@ user.save()
|
||||
organizer = Organizer.objects.create(
|
||||
name='BigEvents LLC', slug='bigevents'
|
||||
)
|
||||
OrganizerPermission.objects.get_or_create(
|
||||
organizer=organizer, user=user
|
||||
)
|
||||
year = now().year + 1
|
||||
event = Event.objects.create(
|
||||
organizer=organizer, name='Demo Conference {}'.format(year),
|
||||
@@ -33,9 +30,13 @@ event = Event.objects.create(
|
||||
date_from=datetime(year, 9, 4, 17, 0, 0),
|
||||
date_to=datetime(year, 9, 6, 17, 0, 0),
|
||||
)
|
||||
EventPermission.objects.get_or_create(
|
||||
event=event, user=user
|
||||
t = Team.objects.get_or_create(
|
||||
organizer=organizer, name='Admin Team',
|
||||
all_events=True, can_create_events=True, can_change_teams=True,
|
||||
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
||||
)
|
||||
t[0].members.add(user)
|
||||
cat_tickets = ItemCategory.objects.create(
|
||||
event=event, name='Tickets'
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.3.0"
|
||||
__version__ = "1.4.1"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import csv
|
||||
import io
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
|
||||
114
src/pretix/base/migrations/0052_team_teaminvite.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-04-27 09:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.organizer
|
||||
|
||||
|
||||
def create_teams(apps, schema_editor):
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
Organizer = apps.get_model('pretixbase', 'Organizer')
|
||||
Team = apps.get_model('pretixbase', 'Team')
|
||||
TeamInvite = apps.get_model('pretixbase', 'TeamInvite')
|
||||
EventPermission = apps.get_model('pretixbase', 'EventPermission')
|
||||
OrganizerPermission = apps.get_model('pretixbase', 'OrganizerPermission')
|
||||
|
||||
for o in Organizer.objects.prefetch_related('events'):
|
||||
for e in o.events.all():
|
||||
teams = {}
|
||||
|
||||
for p in e.user_perms.all():
|
||||
pkey = (p.can_change_settings, p.can_change_items, p.can_view_orders,
|
||||
p.can_change_permissions, p.can_change_orders, p.can_view_vouchers,
|
||||
p.can_change_vouchers)
|
||||
if pkey not in teams:
|
||||
team = Team()
|
||||
team.can_change_event_settings = p.can_change_settings
|
||||
team.can_change_items = p.can_change_items
|
||||
team.can_view_orders = p.can_view_orders
|
||||
team.can_change_orders = p.can_change_orders
|
||||
team.can_view_vouchers = p.can_view_vouchers
|
||||
team.can_change_vouchers = p.can_change_vouchers
|
||||
team.organizer = o
|
||||
team.name = '{} Team {}'.format(
|
||||
str(e.name), len(teams) + 1
|
||||
)
|
||||
team.save()
|
||||
team.limit_events.add(e)
|
||||
|
||||
teams[pkey] = team
|
||||
|
||||
if p.user:
|
||||
teams[pkey].members.add(p.user)
|
||||
else:
|
||||
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
|
||||
|
||||
teams = {}
|
||||
for p in o.user_perms.all():
|
||||
pkey = (p.can_create_events, p.can_change_permissions)
|
||||
if pkey not in teams:
|
||||
team = Team()
|
||||
team.can_change_organizer_settings = True
|
||||
team.can_create_events = p.can_create_events
|
||||
team.can_change_teams = p.can_change_permissions
|
||||
team.organizer = o
|
||||
team.name = '{} Team {}'.format(
|
||||
str(o.name), len(teams) + 1
|
||||
)
|
||||
team.save()
|
||||
teams[pkey] = team
|
||||
|
||||
if p.user:
|
||||
teams[pkey].members.add(p.user)
|
||||
else:
|
||||
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0051_auto_20170206_2027_squashed_0057_auto_20170501_2116'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=190, verbose_name='Team name')),
|
||||
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
|
||||
('can_create_events', models.BooleanField(default=False, verbose_name='Can create events')),
|
||||
('can_change_teams', models.BooleanField(default=False, verbose_name='Can change permissions')),
|
||||
('can_change_organizer_settings', models.BooleanField(default=False, verbose_name='Can change organizer settings')),
|
||||
('can_change_event_settings', models.BooleanField(default=False, verbose_name='Can change event settings')),
|
||||
('can_change_items', models.BooleanField(default=False, verbose_name='Can change product settings')),
|
||||
('can_view_orders', models.BooleanField(default=False, verbose_name='Can view orders')),
|
||||
('can_change_orders', models.BooleanField(default=False, verbose_name='Can change orders')),
|
||||
('can_view_vouchers', models.BooleanField(default=False, verbose_name='Can view vouchers')),
|
||||
('can_change_vouchers', models.BooleanField(default=False, verbose_name='Can change vouchers')),
|
||||
('limit_events', models.ManyToManyField(to='pretixbase.Event', verbose_name='Limit to events')),
|
||||
('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL, verbose_name='Team members')),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='pretixbase.Organizer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Teams',
|
||||
'verbose_name': 'Team',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamInvite',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True)),
|
||||
('token', models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='pretixbase.Team')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
create_teams, migrations.RunPython.noop
|
||||
)
|
||||
]
|
||||
55
src/pretix/base/migrations/0058_auto_20170429_1020.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-04-29 10:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0052_team_teaminvite'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='eventpermission',
|
||||
name='event',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventpermission',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizerpermission',
|
||||
name='organizer',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizerpermission',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='permitted',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizer',
|
||||
name='permitted',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='can_change_teams',
|
||||
field=models.BooleanField(default=False, verbose_name='Can change teams and permissions'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='limit_events',
|
||||
field=models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventPermission',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OrganizerPermission',
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0059_checkin_nonce.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-04 07:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0058_auto_20170429_1020'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='nonce',
|
||||
field=models.CharField(blank=True, max_length=190, null=True),
|
||||
),
|
||||
]
|
||||
26
src/pretix/base/migrations/0060_auto_20170510_1027.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-10 10:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0059_checkin_nonce'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='date_admission',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Admission time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location'),
|
||||
),
|
||||
]
|
||||
27
src/pretix/base/migrations/0061_auto_20170521_0942.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-21 09:42
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def rename_placeholder(app, schema_editor):
|
||||
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
for setting in EventSettingsStore.objects.all():
|
||||
if setting.key == 'mail_text_order_placed':
|
||||
new_value = setting.value.replace('{paymentinfo}', '{payment_info}')
|
||||
setting.value = new_value
|
||||
cache.delete('hierarkey_{}_{}'.format('event', setting.object_id))
|
||||
setting.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0060_auto_20170510_1027'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rename_placeholder, migrations.RunPython.noop)
|
||||
]
|
||||
@@ -3,7 +3,7 @@ from .auth import U2FDevice, User
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventPermission, RequiredAction,
|
||||
Event, Event_SettingsStore, EventLock, RequiredAction,
|
||||
generate_invite_token,
|
||||
)
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
@@ -18,6 +18,6 @@ from .orders import (
|
||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, Organizer_SettingsStore, OrganizerPermission
|
||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
|
||||
@@ -81,6 +84,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._teamcache = {}
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
@@ -147,6 +154,103 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return LogEntry.objects.filter(content_type=ContentType.objects.get_for_model(User),
|
||||
object_id=self.pk)
|
||||
|
||||
def _get_teams_for_organizer(self, organizer):
|
||||
if 'o{}'.format(organizer.pk) not in self._teamcache:
|
||||
self._teamcache['o{}'.format(organizer.pk)] = list(self.teams.filter(organizer=organizer))
|
||||
return self._teamcache['o{}'.format(organizer.pk)]
|
||||
|
||||
def _get_teams_for_event(self, organizer, event):
|
||||
if 'e{}'.format(event.pk) not in self._teamcache:
|
||||
self._teamcache['e{}'.format(event.pk)] = list(self.teams.filter(organizer=organizer).filter(
|
||||
Q(all_events=True) | Q(limit_events=event)
|
||||
))
|
||||
return self._teamcache['e{}'.format(event.pk)]
|
||||
|
||||
class SuperuserPermissionSet:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular event
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> Union[set, SuperuserPermissionSet]:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return True
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
if teams:
|
||||
self._teamcache['e{}'.format(event.pk)] = teams
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None):
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return True
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
if teams:
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
"""
|
||||
Returns a queryset of events the user has any permissions to.
|
||||
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
|
||||
if self.is_superuser:
|
||||
return Event.objects.all()
|
||||
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=self.teams.filter(all_events=True).values_list('organizer', flat=True))
|
||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
|
||||
class U2FDevice(Device):
|
||||
json_data = models.TextField()
|
||||
|
||||
@@ -8,3 +8,4 @@ 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)
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.email import CustomSMTPBackend
|
||||
from pretix.base.models.base import LoggedModel
|
||||
@@ -21,7 +21,6 @@ from pretix.base.validators import EventSlugBlacklistValidator
|
||||
from pretix.helpers.daterange import daterange
|
||||
|
||||
from ..settings import settings_hierarkey
|
||||
from .auth import User
|
||||
from .organizer import Organizer
|
||||
|
||||
|
||||
@@ -79,8 +78,6 @@ class Event(LoggedModel):
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
|
||||
permitted = models.ManyToManyField(User, through='EventPermission',
|
||||
related_name="events", )
|
||||
currency = models.CharField(max_length=10,
|
||||
verbose_name=_("Default currency"),
|
||||
choices=CURRENCY_CHOICES,
|
||||
@@ -88,6 +85,8 @@ class Event(LoggedModel):
|
||||
date_from = models.DateTimeField(verbose_name=_("Event start time"))
|
||||
date_to = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Event end time"))
|
||||
date_admission = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Admission time"))
|
||||
is_public = models.BooleanField(default=False,
|
||||
verbose_name=_("Visible in public lists"),
|
||||
help_text=_("If selected, this event may show up on the ticket system's start page "
|
||||
@@ -102,7 +101,7 @@ class Event(LoggedModel):
|
||||
verbose_name=_("Start of presale"),
|
||||
help_text=_("No products will be sold before this date."),
|
||||
)
|
||||
location = I18nCharField(
|
||||
location = I18nTextField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
@@ -140,7 +139,7 @@ class Event(LoggedModel):
|
||||
return []
|
||||
return self.plugins.split(",")
|
||||
|
||||
def get_date_from_display(self, tz=None) -> str:
|
||||
def get_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
@@ -148,7 +147,17 @@ class Event(LoggedModel):
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
|
||||
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_time_from_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start time of the event, ignoring
|
||||
the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
return _date(
|
||||
self.date_from.astimezone(tz), "TIME_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_to_display(self, tz=None) -> str:
|
||||
@@ -307,69 +316,6 @@ def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class EventPermission(models.Model):
|
||||
"""
|
||||
The relation between an Event and a User who has permissions to
|
||||
access an event.
|
||||
|
||||
:param event: The event this permission refers to
|
||||
:type event: Event
|
||||
:param user: The user this permission set applies to
|
||||
:type user: User
|
||||
:param can_change_settings: If ``True``, the user can change all basic settings for this event.
|
||||
:type can_change_settings: bool
|
||||
:param can_change_items: If ``True``, the user can change and add items and related objects for this event.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the user can inspect details of all orders.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the user can change details of orders
|
||||
:type can_change_orders: bool
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True)
|
||||
invite_email = models.EmailField(null=True, blank=True)
|
||||
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
can_change_settings = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_permissions = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change permissions")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Event permission")
|
||||
verbose_name_plural = _("Event permissions")
|
||||
|
||||
def __str__(self):
|
||||
return _("%(name)s on %(object)s") % {
|
||||
'name': str(self.user),
|
||||
'object': str(self.event),
|
||||
}
|
||||
|
||||
|
||||
class EventLock(models.Model):
|
||||
event = models.CharField(max_length=36, primary_key=True)
|
||||
date = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
@@ -66,7 +67,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'code': co.code
|
||||
}),
|
||||
'val': co.code,
|
||||
'val': escape(co.code),
|
||||
}
|
||||
elif isinstance(co, Voucher):
|
||||
a_text = _('Voucher {val}…')
|
||||
@@ -76,7 +77,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'voucher': co.id
|
||||
}),
|
||||
'val': co.code[:6],
|
||||
'val': escape(co.code[:6]),
|
||||
}
|
||||
elif isinstance(co, Item):
|
||||
a_text = _('Product {val}')
|
||||
@@ -86,7 +87,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'item': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, Quota):
|
||||
a_text = _('Quota {val}')
|
||||
@@ -96,7 +97,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'quota': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, ItemCategory):
|
||||
a_text = _('Category {val}')
|
||||
@@ -106,7 +107,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'category': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, Question):
|
||||
a_text = _('Question {val}')
|
||||
@@ -116,7 +117,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'question': co.id
|
||||
}),
|
||||
'val': co.question,
|
||||
'val': escape(co.question),
|
||||
}
|
||||
|
||||
if a_text and a_map:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import string
|
||||
from datetime import datetime
|
||||
@@ -183,6 +184,10 @@ class Order(LoggedModel):
|
||||
def __str__(self):
|
||||
return self.full_code
|
||||
|
||||
@cached_property
|
||||
def meta_info_data(self):
|
||||
return json.loads(self.meta_info)
|
||||
|
||||
@property
|
||||
def full_code(self):
|
||||
"""
|
||||
|
||||
@@ -42,8 +42,6 @@ class Organizer(LoggedModel):
|
||||
],
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
permitted = models.ManyToManyField(User, through='OrganizerPermission',
|
||||
related_name="organizers")
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer")
|
||||
@@ -74,39 +72,131 @@ def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class OrganizerPermission(models.Model):
|
||||
class Team(LoggedModel):
|
||||
"""
|
||||
The relation between an Organizer and a User who has permissions to
|
||||
access an organizer profile.
|
||||
A team is a collection of people given certain access rights to one or more events of an organizer.
|
||||
|
||||
:param organizer: The organizer this relation refers to
|
||||
:param name: The name of this team
|
||||
:type name: str
|
||||
:param organizer: The organizer this team belongs to
|
||||
:type organizer: Organizer
|
||||
:param user: The user this set of permissions is valid for
|
||||
:type user: User
|
||||
:param can_create_events: Whether or not this user can create new events with this
|
||||
organizer account.
|
||||
:param members: A set of users who belong to this team
|
||||
:param all_events: Whether this team has access to all events of this organizer
|
||||
:type all_events: bool
|
||||
:param limit_events: A set of events this team has access to. Irrelevant if ``all_events`` is ``True``.
|
||||
:param can_create_events: Whether or not the members can create new events with this organizer account.
|
||||
:type can_create_events: bool
|
||||
:param can_change_teams: If ``True``, the members can change the teams of this organizer account.
|
||||
:type can_change_teams: bool
|
||||
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
|
||||
:type can_change_organizer_settings: bool
|
||||
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
|
||||
:type can_change_event_settings: bool
|
||||
:param can_change_items: If ``True``, the members can change and add items and related objects for the associated events.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the members can inspect details of all orders of the associated events.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the members can change details of orders of the associated events.
|
||||
:type can_change_orders: bool
|
||||
:param can_view_vouchers: If ``True``, the members can inspect details of all vouchers of the associated events.
|
||||
:type can_view_vouchers: bool
|
||||
:param can_change_vouchers: If ``True``, the members can change and create vouchers for the associated events.
|
||||
:type can_change_vouchers: bool
|
||||
"""
|
||||
organizer = models.ForeignKey(Organizer, related_name="teams", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=190, verbose_name=_("Team name"))
|
||||
members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members"))
|
||||
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, related_name="organizer_perms", on_delete=models.CASCADE, null=True, blank=True)
|
||||
invite_email = models.EmailField(null=True, blank=True)
|
||||
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
can_create_events = models.BooleanField(
|
||||
default=True,
|
||||
default=False,
|
||||
verbose_name=_("Can create events"),
|
||||
)
|
||||
can_change_permissions = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change permissions"),
|
||||
can_change_teams = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change teams and permissions"),
|
||||
)
|
||||
can_change_organizer_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change organizer settings")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer permission")
|
||||
verbose_name_plural = _("Organizer permissions")
|
||||
can_change_event_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("%(name)s on %(object)s") % {
|
||||
'name': str(self.user),
|
||||
'name': str(self.name),
|
||||
'object': str(self.organizer),
|
||||
}
|
||||
|
||||
def permission_set(self) -> set:
|
||||
attribs = dir(self)
|
||||
return {
|
||||
a for a in attribs if a.startswith('can_') and self.has_permission(a)
|
||||
}
|
||||
|
||||
@property
|
||||
def can_change_settings(self): # Legacy compatiblilty
|
||||
return self.can_change_event_settings
|
||||
|
||||
def has_permission(self, perm_name):
|
||||
try:
|
||||
return getattr(self, perm_name)
|
||||
except AttributeError:
|
||||
raise ValueError('Invalid required permission: %s' % perm_name)
|
||||
|
||||
def permission_for_event(self, event):
|
||||
if self.all_events:
|
||||
return event.organizer_id == self.organizer_id
|
||||
else:
|
||||
return self.limit_events.filter(pk=event.pk).exists()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Team")
|
||||
verbose_name_plural = _("Teams")
|
||||
|
||||
|
||||
class TeamInvite(models.Model):
|
||||
"""
|
||||
A TeamInvite represents someone who has been invited to a team but hasn't accept the invitation
|
||||
yet.
|
||||
|
||||
:param team: The team the person is invited to
|
||||
:type team: Team
|
||||
:param email: The email the invite has been sent to
|
||||
:type email: str
|
||||
:param token: The secret required to redeem the invite
|
||||
:type token: str
|
||||
"""
|
||||
team = models.ForeignKey(Team, related_name="invites", on_delete=models.CASCADE)
|
||||
email = models.EmailField(null=True, blank=True)
|
||||
token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("Invite to team '{team}' for '{email}'").format(
|
||||
team=str(self.team), email=self.email
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
@@ -150,9 +150,9 @@ class BasePaymentProvider:
|
||||
forms.BooleanField(
|
||||
label=_('Calculate the fee from the total value including the fee.'),
|
||||
help_text=_('We recommend you to enable this if you want your users to pay the payment fees of your '
|
||||
'payment provider. <a href="/control/help/payment/fee_reverse" target="_blank">Click here '
|
||||
'payment provider. <a href="{docs_url}" target="_blank">Click here '
|
||||
'for detailled information on what this does.</a> Don\'t forget to set the correct fees '
|
||||
'above!'),
|
||||
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
|
||||
required=False
|
||||
)),
|
||||
('_invoice_text',
|
||||
@@ -266,7 +266,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> "bool|str":
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called after the user selects this provider as his payment method.
|
||||
If you provided a form to the user to enter payment data, this method should
|
||||
@@ -394,14 +394,14 @@ class BasePaymentProvider:
|
||||
"""
|
||||
return False
|
||||
|
||||
def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def retry_prepare(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Deprecated, use order_prepare instead
|
||||
"""
|
||||
raise DeprecationWarning('retry_prepare is deprecated, use order_prepare instead')
|
||||
return self.order_prepare(request, order)
|
||||
|
||||
def order_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def order_prepare(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called if the user retries to pay an unpaid order (after the user filled in
|
||||
e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
|
||||
@@ -409,6 +409,10 @@ class BasePaymentProvider:
|
||||
|
||||
It should return and report errors the same way as :py:meth:`checkout_prepare`, but
|
||||
receives an ``Order`` object instead of a cart object.
|
||||
|
||||
Note: The ``Order`` object given to this method might be different from the version
|
||||
stored in the database as it's total will already contain the payment fee for the
|
||||
new payment method.
|
||||
"""
|
||||
form = self.payment_form(request)
|
||||
if form.is_valid():
|
||||
@@ -458,7 +462,7 @@ class BasePaymentProvider:
|
||||
return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
|
||||
'please transfer the money back manually.')
|
||||
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called if the event administrator confirms the refund.
|
||||
|
||||
@@ -523,7 +527,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
def order_control_refund_render(self, order: Order) -> str:
|
||||
return ''
|
||||
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called if the event administrator confirms the refund.
|
||||
|
||||
|
||||
@@ -114,6 +114,16 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
subject = "[%s] %s" % (prefix, subject)
|
||||
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
signature = str(event.settings.get('mail_text_signature'))
|
||||
if signature:
|
||||
signature = signature.format(event=event.name)
|
||||
signature_md = signature.replace('\n', '<br>\n')
|
||||
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
|
||||
htmlctx['signature'] = signature_md
|
||||
body_plain += signature
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
body_plain += _(
|
||||
"You are receiving this email because you placed an order for {event}."
|
||||
).format(event=event.name)
|
||||
|
||||
@@ -414,7 +414,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'paymentinfo': str(pprov.order_pending_mail_render(order)),
|
||||
'payment_info': str(pprov.order_pending_mail_render(order)),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
},
|
||||
@@ -512,7 +512,7 @@ class OrderChangeManager:
|
||||
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||
raise OrderError(self.error_messages['product_without_variation'])
|
||||
price = item.default_price if variation is None else variation.price
|
||||
if not price:
|
||||
if price is None:
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
self._totaldiff = price - position.price
|
||||
self._quotadiff.update(variation.quotas.all() if variation else item.quotas.all())
|
||||
|
||||
@@ -163,6 +163,10 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'ticket_download_nonadm': {
|
||||
'default': 'True',
|
||||
'type': bool
|
||||
},
|
||||
'last_order_modification_date': {
|
||||
'default': None,
|
||||
'type': datetime
|
||||
@@ -187,6 +191,10 @@ DEFAULTS = {
|
||||
'default': settings.MAIL_FROM,
|
||||
'type': str
|
||||
},
|
||||
'mail_text_signature': {
|
||||
'type': LazyI18nString,
|
||||
'default': ""
|
||||
},
|
||||
'mail_text_resend_link': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
@@ -232,7 +240,7 @@ Your {event} team"""))
|
||||
we successfully received your order for {event} with a total value
|
||||
of {total} {currency}. Please complete your payment before {date}.
|
||||
|
||||
{paymentinfo}
|
||||
{payment_info}
|
||||
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
@@ -50,7 +50,7 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
responses.append((receiver, response))
|
||||
return responses
|
||||
return sorted(responses, key=lambda r: (receiver.__module__, receiver.__name__))
|
||||
|
||||
|
||||
class DeprecatedSignal(django.dispatch.Signal):
|
||||
|
||||
@@ -148,6 +148,18 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if signature %}
|
||||
<tr>
|
||||
<td class="gap"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="order containertd">
|
||||
<div class="content">
|
||||
{{ signature | safe }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="footer">
|
||||
<div>
|
||||
|
||||
13
src/pretix/base/templatetags/escapejson.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
|
||||
from pretix.helpers.escapejson import escapejson
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter("escapejson")
|
||||
@stringfilter
|
||||
def escapejs_filter(value):
|
||||
"""Hex encodes characters for use in a application/json type script."""
|
||||
return escapejson(value)
|
||||
@@ -1,6 +1,12 @@
|
||||
import urllib.parse
|
||||
|
||||
import bleach
|
||||
import markdown
|
||||
from bleach import DEFAULT_CALLBACKS
|
||||
from django import template
|
||||
from django.core import signing
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
@@ -42,14 +48,24 @@ ALLOWED_ATTRIBUTES = {
|
||||
}
|
||||
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not is_safe_url(url):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
return attrs
|
||||
|
||||
|
||||
@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])
|
||||
return mark_safe(body_md)
|
||||
|
||||
@@ -54,12 +54,15 @@ class BaseTicketOutput:
|
||||
|
||||
If you override this method, make sure that positions that are addons (i.e. ``addon_to``
|
||||
is set) are only outputted if the event setting ``ticket_download_addons`` is active.
|
||||
Do the same for positions that are non-admission without ``ticket_download_nonadm`` active.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for pos in order.positions.all():
|
||||
if pos.addon_to_id and not self.event.settings.ticket_download_addons:
|
||||
continue
|
||||
if not pos.item.admission and not self.event.settings.ticket_download_nonadm:
|
||||
continue
|
||||
fname, __, content = self.generate(pos)
|
||||
zipf.writestr('{}-{}{}'.format(
|
||||
order.code, pos.positionid, os.path.splitext(fname)[1]
|
||||
|
||||
@@ -33,7 +33,7 @@ def contextprocessor(request):
|
||||
|
||||
_js_payment_weekdays_disabled = '[]'
|
||||
_nav_event = []
|
||||
if hasattr(request, 'event'):
|
||||
if getattr(request, 'event', None) and hasattr(request, 'organizer'):
|
||||
for receiver, response in nav_event.send(request.event, request=request):
|
||||
_nav_event += response
|
||||
if request.event.settings.get('payment_term_weekdays'):
|
||||
@@ -45,12 +45,12 @@ def contextprocessor(request):
|
||||
if not hasattr(request, 'event'):
|
||||
for receiver, response in nav_global.send(request, request=request):
|
||||
_nav_global += response
|
||||
ctx['nav_global'] = _nav_global
|
||||
ctx['nav_global'] = sorted(_nav_global, key=lambda n: n['label'])
|
||||
|
||||
_nav_topbar = []
|
||||
for receiver, response in nav_topbar.send(request, request=request):
|
||||
_nav_topbar += response
|
||||
ctx['nav_topbar'] = _nav_topbar
|
||||
ctx['nav_topbar'] = sorted(_nav_topbar, key=lambda n: n['label'])
|
||||
|
||||
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
|
||||
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
|
||||
|
||||
@@ -2,6 +2,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
@@ -26,7 +27,7 @@ class EventWizardFoundationForm(forms.Form):
|
||||
self.fields['organizer'] = forms.ModelChoiceField(
|
||||
label=_("Organizer"),
|
||||
queryset=Organizer.objects.filter(
|
||||
id__in=self.user.organizer_perms.filter(can_create_events=True).values_list('organizer', flat=True)
|
||||
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
|
||||
),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=None,
|
||||
@@ -111,6 +112,16 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
|
||||
class EventWizardCopyForm(forms.Form):
|
||||
|
||||
@staticmethod
|
||||
def copy_from_queryset(user):
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=user.teams.filter(
|
||||
all_events=True, can_change_event_settings=True, can_change_items=True
|
||||
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
|
||||
can_change_event_settings=True, can_change_items=True
|
||||
).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('organizer')
|
||||
kwargs.pop('locales')
|
||||
@@ -118,11 +129,7 @@ class EventWizardCopyForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['copy_from_event'] = forms.ModelChoiceField(
|
||||
label=_("Copy configuration from"),
|
||||
queryset=Event.objects.filter(
|
||||
id__in=self.user.event_perms.filter(
|
||||
can_change_items=True, can_change_settings=True
|
||||
).values_list('event', flat=True)
|
||||
),
|
||||
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=_('Do not copy'),
|
||||
required=False
|
||||
@@ -136,6 +143,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
@@ -146,6 +154,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
'currency',
|
||||
'date_from',
|
||||
'date_to',
|
||||
'date_admission',
|
||||
'is_public',
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
@@ -154,6 +163,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
|
||||
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_presale_start'}),
|
||||
@@ -249,10 +259,12 @@ class EventSettingsForm(SettingsForm):
|
||||
)
|
||||
attendee_emails_asked = forms.BooleanField(
|
||||
label=_("Ask for email addresses per ticket"),
|
||||
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be send "
|
||||
"to that email address. If you enable this option, the system will additionally ask for "
|
||||
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
|
||||
"only to that email address. If you enable this option, the system will additionally ask for "
|
||||
"individual email addresses for every admission ticket. This might be useful if you want to "
|
||||
"obtain individual addresses for every attendee even in case of group orders."),
|
||||
"obtain individual addresses for every attendee even in case of group orders. However, "
|
||||
"pretix will send the order confirmation only to the one primary email address, not to the "
|
||||
"per-attendee addresses."),
|
||||
required=False
|
||||
)
|
||||
attendee_emails_required = forms.BooleanField(
|
||||
@@ -467,13 +479,22 @@ class MailSettingsForm(SettingsForm):
|
||||
label=_("Sender address"),
|
||||
help_text=_("Sender address for outgoing emails")
|
||||
)
|
||||
|
||||
mail_text_signature = I18nFormField(
|
||||
label=_("Signature"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will be attached to every email. Available placeholders: {event}"),
|
||||
validators=[PlaceholderValidator(['{event}'])]
|
||||
)
|
||||
|
||||
mail_text_order_placed = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}, "
|
||||
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {payment_info}, {url}, "
|
||||
"{invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{paymentinfo}',
|
||||
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{payment_info}',
|
||||
'{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_order_paid = I18nFormField(
|
||||
@@ -586,11 +607,12 @@ class DisplaySettingsForm(SettingsForm):
|
||||
validators=[
|
||||
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
|
||||
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
|
||||
]
|
||||
],
|
||||
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
|
||||
)
|
||||
logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your events name and date '
|
||||
'in the page header. We will show your logo with a maximal height of 120 pixels.')
|
||||
@@ -624,6 +646,11 @@ class TicketSettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
|
||||
)
|
||||
ticket_download_nonadm = forms.BooleanField(
|
||||
label=_("Generate tickets for non-admission products"),
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
|
||||
)
|
||||
|
||||
def prepare_fields(self):
|
||||
# See clean()
|
||||
|
||||
@@ -29,7 +29,7 @@ class GlobalSettingsForm(SettingsForm):
|
||||
))
|
||||
])
|
||||
responses = register_global_settings.send(self)
|
||||
for r, response in responses:
|
||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
for key, value in response.items():
|
||||
# We need to be this explicit, since OrderedDict.update does not retain ordering
|
||||
self.fields[key] = value
|
||||
|
||||
@@ -2,6 +2,7 @@ import copy
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
from django.forms import BooleanField, ModelMultipleChoiceField
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.utils.translation import ugettext as __, ugettext_lazy as _
|
||||
@@ -120,7 +121,20 @@ class ItemCreateForm(I18nModelForm):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.cleaned_data.get('copy_from'):
|
||||
self.instance.description = self.cleaned_data['copy_from'].description
|
||||
self.instance.active = self.cleaned_data['copy_from'].active
|
||||
self.instance.available_from = self.cleaned_data['copy_from'].available_from
|
||||
self.instance.available_until = self.cleaned_data['copy_from'].available_until
|
||||
self.instance.require_voucher = self.cleaned_data['copy_from'].require_voucher
|
||||
self.instance.hide_without_voucher = self.cleaned_data['copy_from'].hide_without_voucher
|
||||
self.instance.allow_cancel = self.cleaned_data['copy_from'].allow_cancel
|
||||
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.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
if self.cleaned_data.get('has_variations'):
|
||||
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
|
||||
for variation in self.cleaned_data['copy_from'].variations.all():
|
||||
@@ -131,8 +145,9 @@ class ItemCreateForm(I18nModelForm):
|
||||
item=instance, value=__('Standard')
|
||||
)
|
||||
|
||||
for question in Question.objects.filter(items=self.cleaned_data.get('copy_from')):
|
||||
question.items.add(instance)
|
||||
if self.cleaned_data.get('copy_from'):
|
||||
for question in self.cleaned_data['copy_from'].questions.all():
|
||||
question.items.add(instance)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.models import Organizer, Team
|
||||
from pretix.control.forms import ExtFileField
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
|
||||
|
||||
@@ -65,3 +69,60 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
instance.get_cache().clear()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['limit_events'].queryset = organizer.events.all()
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
|
||||
'can_change_teams', 'can_change_organizer_settings',
|
||||
'can_change_event_settings', 'can_change_items',
|
||||
'can_view_orders', 'can_change_orders',
|
||||
'can_view_vouchers', 'can_change_vouchers']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if self.instance.pk and not data['can_change_teams']:
|
||||
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists():
|
||||
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
|
||||
'the permission to change teams and permissions.'))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
locales = forms.MultipleChoiceField(
|
||||
choices=settings.LANGUAGES,
|
||||
label=_("Use languages"),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=_('Choose all languages that your organizer homepage should be available in.')
|
||||
)
|
||||
|
||||
organizer_homepage_text = I18nFormField(
|
||||
label=_('Homepage text'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_('This will be displayed on the organizer homepage.')
|
||||
)
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. We will show your logo with a maximal height of 120 pixels.')
|
||||
)
|
||||
|
||||
@@ -121,7 +121,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
|
||||
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
|
||||
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.')
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.team.created': _('The team has been created.'),
|
||||
'pretix.team.changed': _('The team settings have been modified.'),
|
||||
'pretix.team.deleted': _('The team has been deleted.'),
|
||||
}
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
@@ -149,6 +152,23 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
|
||||
return _('The settings of a ticket output provider have been changed.')
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.added':
|
||||
return _('{user} has been added to the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.removed':
|
||||
return _('{user} has been removed from the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.joined':
|
||||
return _('{user} has joined the team using the invite sent to {email}.').format(
|
||||
user=data.get('email'), email=data.get('invite_email')
|
||||
)
|
||||
|
||||
if logentry.action_type == 'pretix.team.invite.created':
|
||||
return _('{user} has been invited to the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.invite.deleted':
|
||||
return _('The invite for {user} has been revoked.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.user.settings.changed':
|
||||
text = str(_('Your account settings have been changed.'))
|
||||
if 'email' in data:
|
||||
|
||||
@@ -9,9 +9,7 @@ from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, EventPermission, Organizer, OrganizerPermission,
|
||||
)
|
||||
from pretix.base.models import Event, Organizer
|
||||
|
||||
|
||||
class PermissionMiddleware(MiddlewareMixin):
|
||||
@@ -61,53 +59,23 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
events = Event.objects.all() if request.user.is_superuser else request.user.events
|
||||
request.user.events_cache = events.order_by(
|
||||
"organizer", "date_from").prefetch_related("organizer")
|
||||
events = request.user.get_events_with_any_permission()
|
||||
request.user.events_cache = events.order_by("organizer", "date_from").prefetch_related("organizer")
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
try:
|
||||
if request.user.is_superuser:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
else:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
permitted__id__exact=request.user.id,
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
request.organizer = request.event.organizer
|
||||
except IndexError:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer').first()
|
||||
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event):
|
||||
raise Http404(_("The selected event was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
request.organizer = request.event.organizer
|
||||
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
|
||||
elif 'organizer' in url.kwargs:
|
||||
try:
|
||||
if request.user.is_superuser:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
else:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
permitted__id__exact=request.user.id,
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
except IndexError:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
).first()
|
||||
if not request.organizer or not request.user.has_organizer_permission(request.organizer):
|
||||
raise Http404(_("The selected organizer was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import EventPermission, OrganizerPermission
|
||||
|
||||
|
||||
def event_permission_required(permission):
|
||||
"""
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
users having the given permission for the event the request is associated with.
|
||||
"""
|
||||
if permission == 'can_change_settings':
|
||||
# Legacy support
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
if not request.user.is_authenticated: # NOQA
|
||||
# just a double check, should not ever happen
|
||||
raise PermissionDenied()
|
||||
if request.user.is_superuser:
|
||||
|
||||
allowed = (
|
||||
request.user.is_superuser
|
||||
or request.user.has_event_permission(request.organizer, request.event, permission)
|
||||
)
|
||||
if allowed:
|
||||
return function(request, *args, **kw)
|
||||
try:
|
||||
perm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
except EventPermission.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
allowed = not permission
|
||||
try:
|
||||
if permission:
|
||||
allowed = getattr(perm, permission)
|
||||
except AttributeError:
|
||||
pass
|
||||
if allowed or request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -55,29 +47,23 @@ def organizer_permission_required(permission):
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
users having the given permission for the event the request is associated with.
|
||||
"""
|
||||
if permission == 'can_change_settings':
|
||||
# Legacy support
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
if not request.user.is_authenticated: # NOQA
|
||||
# just a double check, should not ever happen
|
||||
raise PermissionDenied()
|
||||
if request.user.is_superuser:
|
||||
|
||||
allowed = (
|
||||
request.user.is_superuser
|
||||
or request.user.has_organizer_permission(request.organizer, permission)
|
||||
)
|
||||
if allowed:
|
||||
return function(request, *args, **kw)
|
||||
try:
|
||||
perm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
except OrganizerPermission.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
allowed = not permission
|
||||
try:
|
||||
if permission:
|
||||
allowed = getattr(perm, permission)
|
||||
except AttributeError:
|
||||
pass
|
||||
if allowed or request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{{ html_head|safe }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -65,7 +68,7 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-calendar"></i>
|
||||
{{ request.event }} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="{% url "control:events" %}">{% trans "Event overview" %}</a></li>
|
||||
<li><a href="{% url "control:events" %}">{% trans "Events overview" %}</a></li>
|
||||
{% regroup request.user.events_cache by organizer as event_list %}
|
||||
{% for g in event_list %}
|
||||
<li class="dropdown-header">{{ g.grouper }}</li>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Check-ins" %}{% 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>
|
||||
<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>
|
||||
{% if entries|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No check-in 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>{% 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>
|
||||
<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>
|
||||
<a href="?{% url_replace request 'ordering' 'name'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Status" %} <a href="?{% url_replace request 'ordering' '-status'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'status'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Timestamp" %} <a href="?{% url_replace request 'ordering' '-timestamp'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'timestamp'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
{% with e.checkins.first as checkin %}
|
||||
<tr>
|
||||
<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 }}</td>
|
||||
<td>{{ e.order.email }}</td>
|
||||
<td>
|
||||
{% if e.addon_to %}
|
||||
{{ e.addon_to.attendee_name }}
|
||||
{% elif e.attendee_name %}
|
||||
{{ e.attendee_name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not checkin %}
|
||||
<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" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,10 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you have been invited to the team of an event that uses pretix for their
|
||||
you have been invited to a team on pretix, a platform to perform event
|
||||
ticket sales.
|
||||
|
||||
Event: {{ event }}
|
||||
Organizer: {{ organizer }}
|
||||
Team: {{ team }}
|
||||
|
||||
If you want to join that team, just click on the following link:
|
||||
{{ url }}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you have been invited to the team of an event organizer that uses pretix
|
||||
for their ticket sales.
|
||||
|
||||
Organizer: {{ organizer }}
|
||||
|
||||
If you want to join that team, just click on the following link:
|
||||
{{ url }}
|
||||
|
||||
If you do not want to join, you can safely ignore or delete this email.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% block title %}{{ request.event.name }}{% endblock %}
|
||||
|
||||
{% block nav %}
|
||||
@@ -10,7 +11,7 @@
|
||||
{% trans "Dashboard" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if request.eventperm.can_change_settings or request.eventperm.can_change_permissions %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset or 'can_change_permissions' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-wrench fa-fw"></i>
|
||||
@@ -18,7 +19,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_change_items %}
|
||||
{% if 'can_change_items' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="has-children">
|
||||
@@ -55,7 +56,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_view_orders %}
|
||||
{% if 'can_view_orders' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="has-children">
|
||||
@@ -90,10 +91,16 @@
|
||||
{% 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 %}
|
||||
{% if request.eventperm.can_view_vouchers %}
|
||||
{% if 'can_view_vouchers' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.vouchers' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.vouchers" %}class="active"{% endif %}>
|
||||
@@ -106,7 +113,9 @@
|
||||
<li>
|
||||
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
|
||||
{% if nav.children %}class="has-children"{% endif %}>
|
||||
{% if nav.icon %}
|
||||
{% if nav.icon and "." in nav.icon %}
|
||||
<img src="{% static nav.icon %}" class="fa-img">
|
||||
{% elif nav.icon %}
|
||||
<i class="fa fa-{{ nav.icon }} fa-fw"></i>
|
||||
{% endif %}
|
||||
{{ nav.label }}
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
{% trans "Customer actions" %}
|
||||
</option>
|
||||
{% for up in userlist %}
|
||||
<option value="{{ up.user_id }}" {% if request.GET.user == up.user_id %}selected="selected"{% endif %}>
|
||||
{{ up.user }}
|
||||
</option>
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}" {% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<legend>{% trans "E-mail settings" %}</legend>
|
||||
{% bootstrap_field form.mail_prefix layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_from layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail content" %}</legend>
|
||||
|
||||
@@ -1,88 +1,19 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal form-permissions">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Permissions" %}</legend>
|
||||
{% bootstrap_formset_errors formset %}
|
||||
{{ formset.management_form }}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Change settings" %}</th>
|
||||
<th>{% trans "Change products" %}</th>
|
||||
<th>{% trans "View orders" %}</th>
|
||||
<th>{% trans "Change orders" %}</th>
|
||||
<th>{% trans "Change permissions" %}</th>
|
||||
<th>{% trans "View vouchers" %}</th>
|
||||
<th>{% trans "Change vouchers" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in formset %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ form.id }}
|
||||
{% if form.instance.user %}
|
||||
{{ form.instance.user }}
|
||||
{% else %}
|
||||
{{ form.instance.invite_email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.can_change_settings }}</td>
|
||||
<td>{{ form.can_change_items }}</td>
|
||||
<td>{{ form.can_view_orders }}</td>
|
||||
<td>{{ form.can_change_orders }}</td>
|
||||
<td>{{ form.can_change_permissions }}</td>
|
||||
<td>{{ form.can_view_vouchers }}</td>
|
||||
<td>{{ form.can_change_vouchers }}</td>
|
||||
<td>{{ form.DELETE }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<strong>{% trans "Adding a new user" %}</strong><br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they already have a
|
||||
pretix account, they will immediately be added to the event. Otherwise, they will
|
||||
be sent an email with an invitation.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{% bootstrap_field add_form.user layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ add_form.can_change_settings }}</td>
|
||||
<td>{{ add_form.can_change_items }}</td>
|
||||
<td>{{ add_form.can_view_orders }}</td>
|
||||
<td>{{ add_form.can_change_orders }}</td>
|
||||
<td>{{ add_form.can_change_permissions }}</td>
|
||||
<td>{{ add_form.can_view_vouchers }}</td>
|
||||
<td>{{ add_form.can_change_vouchers }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<div class="section-moved">
|
||||
<img src="{% static "pretixcontrol/img/moved.svg" %}" class="img-moved">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Permission settings have moved and are now configured as part of an organizer account instead
|
||||
of every event on its own.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.teams" organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-link btn-lg">{% trans "Go to the organizer team settings" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.location layout="horizontal" %}
|
||||
{% bootstrap_field form.date_admission layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
{% bootstrap_field form.is_public layout="horizontal" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<h1>{% trans "Settings" %}</h1>
|
||||
<ul class="nav nav-pills">
|
||||
{% if request.eventperm.can_change_settings %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
<li {% if "event.settings" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% trans "General" %}
|
||||
@@ -41,8 +41,6 @@
|
||||
{% trans "Invoicing" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_change_permissions %}
|
||||
<li {% if "event.settings.permissions" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.settings.permissions' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Permissions" %}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{% bootstrap_field form.ticket_download layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_date layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_addons layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_nonadm layout="horizontal" %}
|
||||
{% for provider in providers %}
|
||||
<div class="panel panel-default ticketoutput-panel">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Help center" %}</h1>
|
||||
{% block inner %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -1,68 +0,0 @@
|
||||
{% extends "pretixcontrol/help/base.html" %}
|
||||
{% block title %}Payment fee calculation{% endblock %}
|
||||
{% block inner %}
|
||||
<h2>Payment fee calculation</h2>
|
||||
<p>
|
||||
If you configure a fee for a payment method, there are two possible ways for us to calculate this. Let's
|
||||
assume that your payment provider, e.g. PayPal, charges you 5 % fees and you want to charge your users the
|
||||
same 5 %, such that for a ticket with a list price of 100 € you will get your full 100 €.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Method A: Calculate the fee from the subtotal and add it to the bill.</strong> For a ticket price of
|
||||
100 €, this will lead to the following calculation:
|
||||
<table class="table helper-width-auto">
|
||||
<tr>
|
||||
<td>Ticket price</td>
|
||||
<td class="text-right">100.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>pretix calculates the fee as 5% of 100 €</td>
|
||||
<td class="text-right">+ 5.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtotal that will be paid by the customer</td>
|
||||
<td class="text-right">105.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PayPal calculates its fee as 5% of 105 €</td>
|
||||
<td class="text-right">- 5.25 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>End total that is on your bank account</td>
|
||||
<td class="text-right"><strong>99.75 €</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Method B (default): Calculate the fee from the total value including the fee.</strong> For a ticket
|
||||
price of 100 €, this will lead to the following calculation:
|
||||
<table class="table helper-width-auto">
|
||||
<tr>
|
||||
<td>Ticket price</td>
|
||||
<td class="text-right">100.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>pretix calculates the fee as 100/(100 - 5)% of 100 €</td>
|
||||
<td class="text-right">+ 5.26 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtotal that will be paid by the customer</td>
|
||||
<td class="text-right">105.26 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PayPal calculates its fee as 5% of 105.26 €</td>
|
||||
<td class="text-right">- 5.26 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>End total that is on your bank account</td>
|
||||
<td class="text-right"><strong>100.00 €</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="alert-warning alert">
|
||||
Due to the various rounding steps performed by us and by the payment provider, the end total on
|
||||
your bank account might stil vary by one cent.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% load formset_tags %}
|
||||
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
{% block inside %}
|
||||
@@ -58,7 +59,7 @@
|
||||
<div class="chart" id="question_chart" data-type="{{ question.type }}">
|
||||
|
||||
</div>
|
||||
<script type="application/json" id="question-chart-data">{{ stats_json|safe }}</script>
|
||||
<script type="application/json" id="question-chart-data">{{ stats_json|escapejson }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<table class="table table-bordered table-hover">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}
|
||||
{% if request.eventperm.can_change_items %}
|
||||
{% if 'can_change_items' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" event=request.event.slug organizer=request.event.organizer.slug quota=quota.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
@@ -20,7 +21,7 @@
|
||||
<div class="chart" id="quota_chart">
|
||||
|
||||
</div>
|
||||
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|safe }}</script>
|
||||
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|escapejson }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<legend>{% trans "Availability calculation" %}</legend>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% endblocktrans %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right" %}
|
||||
</h1>
|
||||
{% if request.eventperm.can_change_orders %}
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
{% if order.status == 'n' or order.status == 'p' or order.status == 'e' %}
|
||||
<form action="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
method="post">
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
{% if order.changable and request.eventperm.can_change_orders %}
|
||||
{% if order.changable and 'can_change_orders' in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Change products" %}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
|
||||
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
{% if 'can_change_organizer_settings' in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "organizer" == url_name %}class="active"{% endif %}>
|
||||
@@ -17,10 +19,10 @@
|
||||
{% trans "Events" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if request.orgaperm.can_change_permissions %}
|
||||
<li {% if "organizer.teams" == url_name %}class="active"{% endif %}>
|
||||
{% if 'can_change_teams' in request.orgapermset %}
|
||||
<li {% if "organizer.team" in url_name %}class="active"{% endif %}>
|
||||
<a href="{% url "control:organizer.teams" organizer=organizer.slug %}">
|
||||
{% trans "Permissions" %}
|
||||
{% trans "Teams" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,17 +4,24 @@
|
||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Organizer" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% if form.domain %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_form_errors sform %}
|
||||
{% bootstrap_field sform.locales layout="horizontal" %}
|
||||
{% bootstrap_field sform.organizer_logo_image layout="horizontal" %}
|
||||
{% bootstrap_field sform.organizer_homepage_text layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h2>{% trans "Delete team:" %} {{ team.name }}</h2>
|
||||
{% if not possible %}
|
||||
<p>{% blocktrans %}You cannot delete the team because there would be noone left who could change team permissions afterwards.{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the team?{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug%}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if team %}
|
||||
<h2>{% trans "Team:" %} {{ team.name }}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans "Create a new team" %}</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You will be able to add team members in the next step.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer permissions" %}</legend>
|
||||
{% bootstrap_field form.can_create_events layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_teams layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_organizer_settings layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Event permissions" %}</legend>
|
||||
|
||||
{% bootstrap_field form.all_events layout="horizontal" %}
|
||||
{% bootstrap_field form.limit_events layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_event_settings layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_items layout="horizontal" %}
|
||||
{% bootstrap_field form.can_view_orders layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_orders layout="horizontal" %}
|
||||
{% bootstrap_field form.can_view_vouchers layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_vouchers layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h2>
|
||||
{% trans "Team:" %} {{ team.name }}
|
||||
<a href="{% url "control:organizer.team.edit" organizer=organizer.slug team=team.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Trick browsers into taking this as a default -->
|
||||
<button type="submit" class="btn btn-primary btn-sm btn-block nearly-gone"></button>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Member" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in team.members.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ u.email }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" name="remove-member" value="{{ u.id }}"
|
||||
class="btn btn-danger btn-sm btn-block">
|
||||
<i class="fa fa-times"></i> {% trans "Remove" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for i in team.invites.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ i.email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" name="remove-invite" value="{{ i.id }}"
|
||||
class="btn btn-danger btn-sm btn-block">
|
||||
<i class="fa fa-times"></i> {% trans "Remove" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>
|
||||
{% bootstrap_field add_form.user layout='inline' %}<br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they already have a
|
||||
pretix account, they will immediately be added to the event. Otherwise, they will
|
||||
be sent an email with an invitation.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" class="btn btn-primary btn-sm btn-block">
|
||||
<i class="fa fa-plus"></i> {% trans "Add" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</form>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Team history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=team %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -2,83 +2,57 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<form action="" method="post" class="form-horizontal form-permissions">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can use the following list to control who can create new events in the name of
|
||||
this organizer and who can add more people to this list. This does <strong>not</strong>
|
||||
control who has access to a particular event. You can control the access to an
|
||||
event in the "Permissions" section of the event's settings. A user does not need to
|
||||
be on the list here to get access to an event.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Everyone on this list can control the organizer settings on this page." %}
|
||||
</p>
|
||||
|
||||
{% bootstrap_formset_errors formset %}
|
||||
{{ formset.management_form }}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Create events" %}</th>
|
||||
<th>{% trans "Change permissions" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in formset %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ form.id }}
|
||||
{% if form.instance.user %}
|
||||
{{ form.instance.user }}
|
||||
{% else %}
|
||||
{{ form.instance.invite_email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.can_create_events }}</td>
|
||||
<td>{{ form.can_change_permissions }}</td>
|
||||
<td>{{ form.DELETE }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<strong>{% trans "Adding a new user" %}</strong><br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they
|
||||
already have a pretix account, they will immediately be added to the team.
|
||||
Otherwise, they will be sent an email with an invitation.
|
||||
<p>
|
||||
{% trans "The list below shows all teams that exist within this organizer." %}
|
||||
</p>
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url "control:organizer.team.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new team" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Team name" %}</th>
|
||||
<th>{% trans "Members" %}</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in teams %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:organizer.team" organizer=request.organizer.slug team=t.id %}">
|
||||
{{ t.name }}
|
||||
</a>
|
||||
</strong></td>
|
||||
<td>
|
||||
{{ t.memcount }}
|
||||
{% if t.invcount %}
|
||||
{% blocktrans trimmed with count=t.invcount %}
|
||||
+ {{ count }} invited
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{% bootstrap_field add_form.user layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ add_form.can_create_events }}</td>
|
||||
<td>{{ add_form.can_change_permissions }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if t.all_events %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
{{ t.eventcount }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.team" organizer=request.organizer.slug team=t.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-list"></i></a>
|
||||
<a href="{% url "control:organizer.team.edit" organizer=request.organizer.slug team=t.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.team.delete" organizer=request.organizer.slug team=t.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
@@ -38,6 +38,37 @@
|
||||
<li>
|
||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
||||
<p>
|
||||
<a data-toggle="collapse" href="#no_scan">
|
||||
{% trans "Can't scan the barcode?" %}
|
||||
</a>
|
||||
</p>
|
||||
<div class="collapse" id="no_scan">
|
||||
<ol class="multi-step-tutorial">
|
||||
<li>
|
||||
{% trans 'Use the "provide a key" option of your authenticator app.' %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans 'In "Account name", type your login name for pretix.' %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans 'In "Secret"/"Account Key", enter the following code:' %}
|
||||
<div>
|
||||
<code id="otp_secret" style="white-space: pre">{{ secretGrouped }}</code>
|
||||
<button type="button" class="btn btn-default btn-xs btn-clipboard" data-clipboard-target="#otp_secret">
|
||||
<span class="fa fa-clipboard" aria-hidden="true"></span>
|
||||
{% trans "copy" %}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<small>Spaces don't matter.</small>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{% trans 'If present, make sure "Time-based"/"TOTP" and 6 digit codes are selected.' %}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Enter the displayed code here:" %}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if request.eventperm.can_change_orders %}
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
<form method="post" class="col-md-6"
|
||||
action="{% url "control:event.orders.waitinglist.auto" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
data-asynctask>
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="{% if request.eventperm.can_change_orders %}col-md-6{% else %}col-md-12{% endif %}">
|
||||
<div class="{% if 'can_change_orders' in request.eventpermset %}col-md-6{% else %}col-md-12{% endif %}">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% trans "Sales estimate" %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, dashboards, event, global_settings, help, item, main, orders,
|
||||
auth, checkin, dashboards, event, global_settings, item, main, orders,
|
||||
organizer, user, vouchers, waitinglist,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,14 @@ urlpatterns = [
|
||||
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.OrganizerTeamView.as_view(), name='organizer.teams'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
|
||||
name='organizer.team'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/edit$', organizer.TeamUpdateView.as_view(),
|
||||
name='organizer.team.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
|
||||
name='organizer.team.delete'),
|
||||
url(r'^events/$', main.EventList.as_view(), name='events'),
|
||||
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
|
||||
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
|
||||
@@ -131,6 +138,6 @@ 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'^checkins/$', checkin.CheckInView.as_view(), name='event.orders.checkins'),
|
||||
])),
|
||||
url(r'^help/(?P<topic>[a-zA-Z0-9_/]+)$', help.HelpView.as_view(), name='help'),
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.contrib.auth import (
|
||||
)
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -23,9 +24,7 @@ from u2flib_server.utils import rand_bytes
|
||||
from pretix.base.forms.auth import (
|
||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
EventPermission, OrganizerPermission, U2FDevice, User,
|
||||
)
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
@@ -108,35 +107,30 @@ def invite(request, token):
|
||||
ctx = {}
|
||||
|
||||
try:
|
||||
perm = EventPermission.objects.get(invite_token=token)
|
||||
desc = perm.event.name
|
||||
except EventPermission.DoesNotExist:
|
||||
try:
|
||||
perm = OrganizerPermission.objects.get(invite_token=token)
|
||||
desc = perm.organizer.name
|
||||
except OrganizerPermission.DoesNotExist:
|
||||
messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar '
|
||||
'and make sure it is correct and that the link has not been used before.'))
|
||||
return redirect('control:auth.login')
|
||||
inv = TeamInvite.objects.get(token=token)
|
||||
except TeamInvite.DoesNotExist:
|
||||
messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar '
|
||||
'and make sure it is correct and that the link has not been used before.'))
|
||||
return redirect('control:auth.login')
|
||||
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
if isinstance(perm, EventPermission):
|
||||
EventPermission.objects.get(event=perm.event, user=request.user)
|
||||
else:
|
||||
OrganizerPermission.objects.get(organizer=perm.organizer, user=request.user)
|
||||
if inv.team.members.filter(pk=request.user.pk).exists():
|
||||
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
|
||||
'this team.').format(desc))
|
||||
'this team.').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
else:
|
||||
with transaction.atomic():
|
||||
inv.team.members.add(request.user)
|
||||
inv.team.log_action(
|
||||
'pretix.team.member.joined', data={
|
||||
'email': request.user.email,
|
||||
'invite_email': inv.email,
|
||||
'user': request.user.pk
|
||||
}
|
||||
)
|
||||
inv.delete()
|
||||
messages.success(request, _('You are now part of the team "{}".').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
except (EventPermission.DoesNotExist, OrganizerPermission.DoesNotExist):
|
||||
pass
|
||||
|
||||
perm.invite_token = None
|
||||
perm.invite_email = None
|
||||
perm.user = request.user
|
||||
perm.save()
|
||||
messages.success(request, _('You have now access to "{}".').format(desc))
|
||||
return redirect('control:index')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = RegistrationForm(data=request.POST)
|
||||
@@ -151,14 +145,20 @@ def invite(request, token):
|
||||
auth_login(request, user)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
|
||||
perm.invite_token = None
|
||||
perm.invite_email = None
|
||||
perm.user = user
|
||||
perm.save()
|
||||
messages.success(request, _('Welcome to pretix! You have now access to "{}".').format(desc))
|
||||
with transaction.atomic():
|
||||
inv.team.members.add(request.user)
|
||||
inv.team.log_action(
|
||||
'pretix.team.member.joined', data={
|
||||
'email': user.email,
|
||||
'invite_email': inv.email,
|
||||
'user': user.pk
|
||||
}
|
||||
)
|
||||
inv.delete()
|
||||
messages.success(request, _('Welcome to pretix! You are now part of the team "{}".').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
else:
|
||||
form = RegistrationForm(initial={'email': perm.invite_email})
|
||||
form = RegistrationForm(initial={'email': inv.email})
|
||||
ctx['form'] = form
|
||||
return render(request, 'pretixcontrol/auth/invite.html', ctx)
|
||||
|
||||
|
||||
82
src/pretix/control/views/checkin.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.views.generic import ListView
|
||||
|
||||
from pretix.base.models import Checkin, Item, OrderPosition
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
|
||||
|
||||
class CheckInView(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):
|
||||
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event, order__status='p')
|
||||
|
||||
# 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 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 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)
|
||||
)
|
||||
|
||||
if self.request.GET.get("item", "") != "":
|
||||
u = self.request.GET.get("item", "")
|
||||
qs = qs.filter(item_id__in=(u,))
|
||||
|
||||
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 type(mapped_field) is tuple:
|
||||
qs = qs.annotate(**mapped_field[1]).order_by(mapped_field[0])
|
||||
else:
|
||||
qs = qs.order_by(mapped_field)
|
||||
|
||||
return qs.distinct()
|
||||
|
||||
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)
|
||||
return ctx
|
||||
|
||||
@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',
|
||||
'-item': '-item__name',
|
||||
'name': (F('display_name').asc(nulls_first=True),
|
||||
{'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
|
||||
'-name': (F('display_name').desc(nulls_last=True),
|
||||
{'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
|
||||
}
|
||||
@@ -10,10 +10,11 @@ from django.shortcuts import render
|
||||
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.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, Order, OrderPosition, Voucher, WaitingListEntry,
|
||||
Item, Order, OrderPosition, Voucher, WaitingListEntry,
|
||||
)
|
||||
from pretix.control.signals import (
|
||||
event_dashboard_widgets, user_dashboard_widgets,
|
||||
@@ -138,7 +139,7 @@ def quota_widgets(sender, **kwargs):
|
||||
status, left = q.availability()
|
||||
widgets.append({
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
|
||||
text=_('{quota} left').format(quota=q.name)),
|
||||
text=_('{quota} left').format(quota=escape(q.name))),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.items.quotas.show', kwargs={
|
||||
@@ -168,6 +169,27 @@ def shop_state_widget(sender, **kwargs):
|
||||
}]
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def checkin_widget(sender, **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)
|
||||
|
||||
# if this setting is False, we check only items for admission
|
||||
if not sender.settings.ticket_download_nonadm:
|
||||
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
|
||||
})
|
||||
}]
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def welcome_wizard_widget(sender, **kwargs):
|
||||
template = get_template('pretixcontrol/event/dashboard_widget_welcome.html')
|
||||
@@ -207,11 +229,12 @@ def event_index(request, organizer, event):
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event):
|
||||
widgets.extend(result)
|
||||
|
||||
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders')
|
||||
qs = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not request.eventperm.can_view_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
if not request.eventperm.can_view_vouchers:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||
|
||||
a_qs = request.event.requiredaction_set.filter(done=False)
|
||||
@@ -221,7 +244,7 @@ def event_index(request, organizer, event):
|
||||
ctx = {
|
||||
'widgets': rearrange(widgets),
|
||||
'logs': qs[:5],
|
||||
'actions': a_qs[:5] if request.eventperm.can_change_orders else [],
|
||||
'actions': a_qs[:5] if can_change_orders else [],
|
||||
'has_domain': has_domain
|
||||
}
|
||||
|
||||
@@ -242,11 +265,13 @@ def event_index(request, organizer, event):
|
||||
def user_event_widgets(**kwargs):
|
||||
user = kwargs.pop('user')
|
||||
widgets = []
|
||||
events = Event.objects.filter(permitted__id__exact=user.pk).select_related("organizer").order_by('-date_from')
|
||||
|
||||
events = user.get_events_with_any_permission().order_by('-date_from', 'name').select_related('organizer')
|
||||
for event in events:
|
||||
widgets.append({
|
||||
'content': '<div class="event">{event}<span class="from">{df}</span><span class="to">{dt}</span></div>'.format(
|
||||
event=event.name, df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
|
||||
event=escape(event.name),
|
||||
df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
|
||||
dt=date_format(event.date_to, 'SHORT_DATE_FORMAT') if event.date_to else ''
|
||||
),
|
||||
'display_size': 'small',
|
||||
|
||||
@@ -2,14 +2,12 @@ import re
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.forms import modelformset_factory
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils import translation
|
||||
@@ -22,14 +20,12 @@ from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from pytz import timezone
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Event, EventPermission, Item, ItemVariation, LogEntry, Order,
|
||||
RequiredAction, User, Voucher,
|
||||
CachedTicket, Event, Item, ItemVariation, LogEntry, Order, RequiredAction,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.signals import (
|
||||
event_live_issues, register_payment_providers, register_ticket_outputs,
|
||||
)
|
||||
@@ -50,7 +46,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Event
|
||||
form_class = EventUpdateForm
|
||||
template_name = 'pretixcontrol/event/settings.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@cached_property
|
||||
def object(self) -> Event:
|
||||
@@ -115,7 +111,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
model = Event
|
||||
context_object_name = 'event'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/plugins.html'
|
||||
|
||||
def get_object(self, queryset=None) -> Event:
|
||||
@@ -178,7 +174,7 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
|
||||
class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
model = Event
|
||||
context_object_name = 'event'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/payment.html'
|
||||
|
||||
def get_object(self, queryset=None) -> Event:
|
||||
@@ -264,7 +260,7 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
|
||||
|
||||
class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
|
||||
model = Event
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
@@ -300,7 +296,7 @@ class InvoiceSettings(EventSettingsFormView):
|
||||
model = Event
|
||||
form_class = InvoiceSettingsForm
|
||||
template_name = 'pretixcontrol/event/invoicing.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
if 'preview' in self.request.POST:
|
||||
@@ -315,7 +311,7 @@ class InvoiceSettings(EventSettingsFormView):
|
||||
|
||||
|
||||
class InvoicePreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
pdf = build_preview_invoice_pdf(request.event)
|
||||
@@ -328,7 +324,7 @@ class DisplaySettings(EventSettingsFormView):
|
||||
model = Event
|
||||
form_class = DisplaySettingsForm
|
||||
template_name = 'pretixcontrol/event/display.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.display', kwargs={
|
||||
@@ -364,7 +360,7 @@ class MailSettings(EventSettingsFormView):
|
||||
model = Event
|
||||
form_class = MailSettingsForm
|
||||
template_name = 'pretixcontrol/event/mail.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.mail', kwargs={
|
||||
@@ -407,7 +403,7 @@ class MailSettings(EventSettingsFormView):
|
||||
|
||||
|
||||
class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
# return the origin text if key is missing in dict
|
||||
class SafeDict(dict):
|
||||
@@ -440,7 +436,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
def items(self):
|
||||
return {
|
||||
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company',
|
||||
'event', 'paymentinfo', 'url', 'invoice_name'],
|
||||
'event', 'payment_info', 'url', 'invoice_name'],
|
||||
'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'],
|
||||
'mail_text_order_free': ['event', 'url', 'invoice_name', 'invoice_company'],
|
||||
'mail_text_resend_link': ['event', 'url', 'invoice_name', 'invoice_company'],
|
||||
@@ -471,7 +467,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
'code': '68CYU2H6ZTP3WLK5',
|
||||
'invoice_name': _('John Doe'),
|
||||
'invoice_company': _('Sample Corporation'),
|
||||
'paymentinfo': _('Please transfer money to this bank account: 9999-9999-9999-9999')
|
||||
'payment_info': _('Please transfer money to this bank account: 9999-9999-9999-9999')
|
||||
}
|
||||
|
||||
def generate_order_url(self, code, secret):
|
||||
@@ -513,7 +509,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
|
||||
|
||||
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@cached_property
|
||||
def output(self):
|
||||
@@ -545,7 +541,7 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
||||
model = Event
|
||||
form_class = TicketSettingsForm
|
||||
template_name = 'pretixcontrol/event/tickets.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
@@ -637,140 +633,12 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
||||
return providers
|
||||
|
||||
|
||||
class EventPermissionForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = EventPermission
|
||||
fields = (
|
||||
'can_change_settings', 'can_change_items', 'can_change_permissions', 'can_view_orders',
|
||||
'can_change_orders', 'can_view_vouchers', 'can_change_vouchers'
|
||||
)
|
||||
|
||||
|
||||
class EventPermissionCreateForm(EventPermissionForm):
|
||||
user = forms.EmailField(required=False, label=_('User'))
|
||||
|
||||
|
||||
class EventPermissions(EventPermissionRequiredMixin, TemplateView):
|
||||
model = Event
|
||||
form_class = TicketSettingsForm
|
||||
template_name = 'pretixcontrol/event/permissions.html'
|
||||
permission = 'can_change_permissions'
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
fs = modelformset_factory(
|
||||
EventPermission,
|
||||
form=EventPermissionForm,
|
||||
can_delete=True, can_order=False, extra=0
|
||||
)
|
||||
return fs(data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix="formset",
|
||||
queryset=EventPermission.objects.filter(event=self.request.event))
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return EventPermissionCreateForm(data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix="add")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['add_form'] = self.add_form
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.invite_email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'event': self.request.event.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.invite_token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, *args, **kwargs):
|
||||
if self.formset.is_valid() and self.add_form.is_valid():
|
||||
if self.add_form.has_changed():
|
||||
logdata = {
|
||||
k: v for k, v in self.add_form.cleaned_data.items()
|
||||
}
|
||||
|
||||
try:
|
||||
self.add_form.instance.event = self.request.event
|
||||
self.add_form.instance.event_id = self.request.event.id
|
||||
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
||||
self.add_form.instance.user_id = self.add_form.instance.user.id
|
||||
except User.DoesNotExist:
|
||||
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
|
||||
if EventPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
|
||||
event=self.request.event).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this event.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
self.add_form.save()
|
||||
self._send_invite(self.add_form.instance)
|
||||
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.invited', user=self.request.user, data=logdata
|
||||
)
|
||||
else:
|
||||
if EventPermission.objects.filter(user=self.add_form.instance.user,
|
||||
event=self.request.event).exists():
|
||||
messages.error(self.request, _('This user already has permissions for this event.'))
|
||||
return self.get(*args, **kwargs)
|
||||
self.add_form.save()
|
||||
logdata['user'] = self.add_form.instance.user_id
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.added', user=self.request.user, data=logdata
|
||||
)
|
||||
for form in self.formset.forms:
|
||||
if form.has_changed():
|
||||
changedata = {
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
changedata['user'] = form.instance.user_id
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.changed', user=self.request.user, data=changedata
|
||||
)
|
||||
if form.instance.user_id == self.request.user.pk:
|
||||
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
|
||||
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
for form in self.formset.deleted_forms:
|
||||
logdata = {
|
||||
k: v for k, v in form.cleaned_data.items()
|
||||
}
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.deleted', user=self.request.user, data=logdata
|
||||
)
|
||||
|
||||
self.formset.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.permissions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug
|
||||
})
|
||||
|
||||
|
||||
class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/live.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -801,7 +669,7 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
issues.append(_('You need to configure at least one quota to sell anything.'))
|
||||
|
||||
responses = event_live_issues.send(self.request.event)
|
||||
for receiver, response in responses:
|
||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
if response:
|
||||
issues.append(response)
|
||||
|
||||
@@ -840,9 +708,9 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not self.request.eventperm.can_view_orders:
|
||||
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
if not self.request.eventperm.can_view_vouchers:
|
||||
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||
|
||||
if self.request.GET.get('user') == 'yes':
|
||||
@@ -856,7 +724,7 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['userlist'] = self.request.event.user_perms.select_related('user')
|
||||
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
from django import template
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import View
|
||||
|
||||
from pretix.base.models import Organizer
|
||||
|
||||
|
||||
class HelpView(View):
|
||||
model = Organizer
|
||||
context_object_name = 'organizers'
|
||||
template_name = 'pretixcontrol/organizers/index.html'
|
||||
paginate_by = 30
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# In a security review, this came up as a possible path traversal issue. However, the URL regex
|
||||
# does not allow any dots in the argument (which forbids traversing upwards in the directory tree).
|
||||
# Even if it *was* possbile, it'd be loaded through django's template loader and therefore limited
|
||||
# to TEMPLATE_DIR.
|
||||
try:
|
||||
locale = request.LANGUAGE_CODE
|
||||
return render(request, 'pretixcontrol/help/%s.%s.html' % (kwargs.get('topic'), locale), {})
|
||||
except template.TemplateDoesNotExist:
|
||||
try:
|
||||
return render(request, 'pretixcontrol/help/%s.html' % kwargs.get('topic'), {})
|
||||
except template.TemplateDoesNotExist:
|
||||
raise Http404('')
|
||||
@@ -4,7 +4,7 @@ from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, Max, Q
|
||||
from django.db.models import Count, F, Q
|
||||
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
@@ -792,18 +792,6 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.cleaned_data['copy_from']:
|
||||
form.instance.description = form.cleaned_data['copy_from'].description
|
||||
form.instance.active = form.cleaned_data['copy_from'].active
|
||||
form.instance.available_from = form.cleaned_data['copy_from'].available_from
|
||||
form.instance.available_until = form.cleaned_data['copy_from'].available_until
|
||||
form.instance.require_voucher = form.cleaned_data['copy_from'].require_voucher
|
||||
form.instance.hide_without_voucher = form.cleaned_data['copy_from'].hide_without_voucher
|
||||
form.instance.allow_cancel = form.cleaned_data['copy_from'].allow_cancel
|
||||
form.instance.min_per_order = form.cleaned_data['copy_from'].min_per_order
|
||||
form.instance.max_per_order = form.cleaned_data['copy_from'].max_per_order
|
||||
|
||||
form.instance.position = (self.request.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import ListView
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
|
||||
from pretix.base.models import Event, EventPermission, OrganizerPermission
|
||||
from pretix.base.models import Event, Team
|
||||
from pretix.control.forms.event import (
|
||||
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
|
||||
)
|
||||
@@ -20,22 +20,13 @@ class EventList(ListView):
|
||||
template_name = 'pretixcontrol/events/index.html'
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
return Event.objects.all().select_related("organizer").prefetch_related(
|
||||
"_settings_objects", "organizer___settings_objects"
|
||||
)
|
||||
else:
|
||||
return Event.objects.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
).select_related("organizer").prefetch_related(
|
||||
"_settings_objects", "organizer___settings_objects"
|
||||
)
|
||||
return self.request.user.get_events_with_any_permission().select_related('organizer').prefetch_related(
|
||||
'_settings_objects', 'organizer___settings_objects'
|
||||
)
|
||||
|
||||
|
||||
def condition_copy(wizard):
|
||||
return EventPermission.objects.filter(
|
||||
user=wizard.request.user, can_change_settings=True, can_change_items=True
|
||||
).exists()
|
||||
return EventWizardCopyForm.copy_from_queryset(wizard.request.user).exists()
|
||||
|
||||
|
||||
class EventWizard(SessionWizardView):
|
||||
@@ -55,8 +46,7 @@ class EventWizard(SessionWizardView):
|
||||
|
||||
def get_context_data(self, form, **kwargs):
|
||||
ctx = super().get_context_data(form, **kwargs)
|
||||
ctx['has_organizer'] = OrganizerPermission.objects.filter(user=self.request.user,
|
||||
can_create_events=True).exists()
|
||||
ctx['has_organizer'] = self.request.user.teams.filter(can_create_events=True).exists()
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self, step=None):
|
||||
@@ -81,7 +71,20 @@ class EventWizard(SessionWizardView):
|
||||
event.organizer = foundation_data['organizer']
|
||||
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||
form_dict['basics'].save()
|
||||
EventPermission.objects.create(event=event, user=self.request.user)
|
||||
|
||||
has_control_rights = self.request.user.teams.filter(
|
||||
organizer=event.organizer, all_events=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_change_orders=True, can_change_vouchers=True
|
||||
).exists()
|
||||
if not has_control_rights:
|
||||
t = Team.objects.create(
|
||||
organizer=event.organizer, name=_('Team {event}').format(event=event.name),
|
||||
can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
|
||||
can_change_vouchers=True
|
||||
)
|
||||
t.members.add(self.request.user)
|
||||
t.limit_events.add(event)
|
||||
|
||||
logdata = {}
|
||||
for f in form_list:
|
||||
|
||||
@@ -674,7 +674,7 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
|
||||
if not self.exporter.form.is_valid():
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(*args, **kwargs)
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
cf = CachedFile()
|
||||
cf.date = now()
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.forms import modelformset_factory
|
||||
from django.shortcuts import redirect
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, ListView, UpdateView
|
||||
from django.views.generic import (
|
||||
CreateView, DeleteView, DetailView, ListView, UpdateView,
|
||||
)
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Organizer, OrganizerPermission, User
|
||||
from pretix.base.models import Organizer, Team, TeamInvite, User
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
|
||||
from pretix.control.forms.organizer import (
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
)
|
||||
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
@@ -28,25 +32,14 @@ class OrganizerList(ListView):
|
||||
if self.request.user.is_superuser:
|
||||
return Organizer.objects.all()
|
||||
else:
|
||||
return Organizer.objects.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
)
|
||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||
|
||||
|
||||
class OrganizerPermissionForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = OrganizerPermission
|
||||
fields = (
|
||||
'can_create_events', 'can_change_permissions'
|
||||
)
|
||||
|
||||
|
||||
class OrganizerPermissionCreateForm(OrganizerPermissionForm):
|
||||
class InviteForm(forms.Form):
|
||||
user = forms.EmailField(required=False, label=_('User'))
|
||||
|
||||
|
||||
class OrganizerDetailViewMixin:
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['nav_organizer'] = []
|
||||
@@ -55,6 +48,7 @@ class OrganizerDetailViewMixin:
|
||||
for recv, retv in nav_organizer.send(sender=self.request.organizer, request=self.request,
|
||||
organizer=self.request.organizer):
|
||||
ctx['nav_organizer'] += retv
|
||||
ctx['nav_organizer'].sort(key=lambda n: n['label'])
|
||||
return ctx
|
||||
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
@@ -82,141 +76,55 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
|
||||
permission = 'can_change_permissions'
|
||||
context_object_name = 'organizer'
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
fs = modelformset_factory(
|
||||
OrganizerPermission,
|
||||
form=OrganizerPermissionForm,
|
||||
can_delete=True, can_order=False, extra=0
|
||||
)
|
||||
return fs(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and 'formset-TOTAL_FORMS' in self.request.POST
|
||||
else None
|
||||
),
|
||||
prefix="formset",
|
||||
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return OrganizerPermissionCreateForm(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and 'formset-TOTAL_FORMS' in self.request.POST
|
||||
else None
|
||||
),
|
||||
prefix="add"
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['add_form'] = self.add_form
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.invite_email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation_organizer.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.invite_token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, *args, **kwargs):
|
||||
if self.formset.is_valid() and self.add_form.is_valid():
|
||||
if self.add_form.has_changed():
|
||||
logdata = {
|
||||
k: v for k, v in self.add_form.cleaned_data.items()
|
||||
}
|
||||
|
||||
try:
|
||||
self.add_form.instance.organizer = self.request.organizer
|
||||
self.add_form.instance.organizer_id = self.request.organizer.id
|
||||
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
||||
self.add_form.instance.user_id = self.add_form.instance.user.id
|
||||
except User.DoesNotExist:
|
||||
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
|
||||
if OrganizerPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
|
||||
organizer=self.request.organizer).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this team.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
self.add_form.save()
|
||||
self._send_invite(self.add_form.instance)
|
||||
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.invited', user=self.request.user, data=logdata
|
||||
)
|
||||
else:
|
||||
if OrganizerPermission.objects.filter(user=self.add_form.instance.user,
|
||||
organizer=self.request.organizer).exists():
|
||||
messages.error(self.request, _('This user already has permissions for this team.'))
|
||||
return self.get(*args, **kwargs)
|
||||
self.add_form.save()
|
||||
logdata['user'] = self.add_form.instance.user_id
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.added', user=self.request.user, data=logdata
|
||||
)
|
||||
for form in self.formset.forms:
|
||||
if form.has_changed():
|
||||
changedata = {
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
changedata['user'] = form.instance.user_id
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.changed', user=self.request.user, data=changedata
|
||||
)
|
||||
if form.instance.user_id == self.request.user.pk:
|
||||
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
|
||||
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
for form in self.formset.deleted_forms:
|
||||
logdata = {
|
||||
k: v for k, v in form.cleaned_data.items()
|
||||
}
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.deleted', user=self.request.user, data=logdata
|
||||
)
|
||||
|
||||
self.formset.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizer.teams', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
|
||||
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = Organizer
|
||||
form_class = OrganizerUpdateForm
|
||||
template_name = 'pretixcontrol/organizers/edit.html'
|
||||
permission = None
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'organizer'
|
||||
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
@cached_property
|
||||
def object(self) -> Organizer:
|
||||
return self.request.organizer
|
||||
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
return self.object
|
||||
|
||||
@cached_property
|
||||
def sform(self):
|
||||
return OrganizerSettingsForm(
|
||||
obj=self.object,
|
||||
prefix='settings',
|
||||
data=self.request.POST if self.request.method == 'POST' else None,
|
||||
files=self.request.FILES if self.request.method == 'POST' else None
|
||||
)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['sform'] = self.sform
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.sform.save()
|
||||
if self.sform.has_changed():
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.settings',
|
||||
user=self.request.user,
|
||||
data={
|
||||
k: (self.sform.cleaned_data.get(k).name
|
||||
if isinstance(self.sform.cleaned_data.get(k), File)
|
||||
else self.sform.cleaned_data.get(k))
|
||||
for k in self.sform.changed_data
|
||||
}
|
||||
)
|
||||
if form.has_changed():
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.changed',
|
||||
user=self.request.user,
|
||||
data={k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
)
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
@@ -231,6 +139,13 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.sform.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class OrganizerCreate(CreateView):
|
||||
model = Organizer
|
||||
@@ -243,14 +158,268 @@ class OrganizerCreate(CreateView):
|
||||
raise PermissionDenied() # TODO
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The new organizer has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
OrganizerPermission.objects.create(
|
||||
organizer=form.instance, user=self.request.user,
|
||||
can_create_events=True
|
||||
t = Team.objects.create(
|
||||
organizer=form.instance, name=_('Administrators'),
|
||||
all_events=True, can_create_events=True, can_change_teams=True,
|
||||
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
||||
)
|
||||
t.members.add(self.request.user)
|
||||
return ret
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizers')
|
||||
|
||||
|
||||
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/teams.html'
|
||||
permission = 'can_change_teams'
|
||||
context_object_name = 'teams'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.teams.annotate(
|
||||
memcount=Count('members', distinct=True),
|
||||
eventcount=Count('limit_events', distinct=True),
|
||||
invcount=Count('invites', distinct=True)
|
||||
).all()
|
||||
|
||||
|
||||
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/team_edit.html'
|
||||
permission = 'can_change_teams'
|
||||
form_class = TeamForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.team', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'team': self.object.pk
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The team has been created. You can now add members to the team.'))
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
form.instance.members.add(self.request.user)
|
||||
form.instance.log_action('pretix.team.created', user=self.request.user, data={
|
||||
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
})
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/team_edit.html'
|
||||
permission = 'can_change_teams'
|
||||
context_object_name = 'team'
|
||||
form_class = TeamForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.team', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'team': self.object.pk
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.object.log_action('pretix.team.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/team_delete.html'
|
||||
permission = 'can_change_teams'
|
||||
context_object_name = 'team'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.teams', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['possible'] = self.is_allowed()
|
||||
return context
|
||||
|
||||
def is_allowed(self) -> bool:
|
||||
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists()
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
success_url = self.get_success_url()
|
||||
self.object = self.get_object()
|
||||
if self.is_allowed():
|
||||
self.object.log_action('pretix.team.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected team has been deleted.'))
|
||||
return redirect(success_url)
|
||||
else:
|
||||
messages.error(request, _('The selected team cannot be deleted.'))
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
template_name = 'pretixcontrol/organizers/team_members.html'
|
||||
context_object_name = 'team'
|
||||
permission = 'can_change_teams'
|
||||
model = Team
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return InviteForm(data=self.request.POST if self.request.method == "POST" else None)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['add_form'] = self.add_form
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'team': instance.team.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
if 'remove-member' in request.POST:
|
||||
try:
|
||||
user = User.objects.get(pk=request.POST.get('remove-member'))
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists()
|
||||
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
|
||||
messages.error(self.request, _('You cannot remove the last member from this team as noone would '
|
||||
'be left with the permission to change teams.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
self.object.members.remove(user)
|
||||
self.object.log_action(
|
||||
'pretix.team.member.removed', user=self.request.user, data={
|
||||
'email': user.email,
|
||||
'user': user.pk
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The member has been removed from the team.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
elif 'remove-invite' in request.POST:
|
||||
try:
|
||||
invite = self.object.invites.get(pk=request.POST.get('remove-invite'))
|
||||
except TeamInvite.DoesNotExist:
|
||||
messages.error(self.request, _('Invalid invite selected.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
invite.delete()
|
||||
self.object.log_action(
|
||||
'pretix.team.invite.deleted', user=self.request.user, data={
|
||||
'email': invite.email
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The invite has been revoked.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
elif self.add_form.is_valid() and self.add_form.has_changed():
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
||||
except User.DoesNotExist:
|
||||
if self.object.invites.filter(email=self.add_form.cleaned_data['user']).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this team.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
invite = self.object.invites.create(email=self.add_form.cleaned_data['user'])
|
||||
self._send_invite(invite)
|
||||
self.object.log_action(
|
||||
'pretix.team.invite.created', user=self.request.user, data={
|
||||
'email': self.add_form.cleaned_data['user']
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The new member has been invited to the team.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
if self.object.members.filter(pk=user.pk).exists():
|
||||
messages.error(self.request, _('This user already has permissions for this team.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
self.object.members.add(user)
|
||||
self.object.log_action(
|
||||
'pretix.team.member.added', user=self.request.user,
|
||||
data={
|
||||
'email': user.email,
|
||||
'user': user.pk,
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The new member has been added to the team.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
else:
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizer.team', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'team': self.object.pk
|
||||
})
|
||||
|
||||
@@ -262,6 +262,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
||||
ctx = super().get_context_data()
|
||||
|
||||
ctx['secret'] = base64.b32encode(self.device.bin_key).decode('utf-8')
|
||||
ctx['secretGrouped'] = " ".join([ctx['secret'].lower()[(i * 4): (i + 1) * 4] for i in range(len(ctx['secret']) // 4)])
|
||||
ctx['qrdata'] = 'otpauth://totp/{label}%3A%20{user}?issuer={label}&secret={secret}&digits={digits}'.format(
|
||||
label=quote(settings.PRETIX_INSTANCE_NAME), user=quote(self.request.user.email),
|
||||
secret=ctx['secret'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import csv
|
||||
import io
|
||||
|
||||
from defusedcsv import csv
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
|
||||