Syncing fork to upstream (#1)
Sync master with master of pretix/pretix@300f8f6 * Automatically sort new products to the end * Drop "squash your commits" from the dev guide * Add variation descriptions and allow to order addons * Link to Django's runserver options in dev docs * Allow <br> tags in rich text * Copy from event: deal with deleted items * Make validate_cart useful together with addons * Fix collapsing panels in the addon choice step * Button text change if addons are present * Update translations * Squash migrations and bump version * Ticket PDFs: Do not hide attendee name if code is hidden * Add a user guide on payments * Link PayPal and Stripe documentation in the respective forms * Hide payment fees if they are all equal to 0.00 * Refs #39 -- New concept of "teams" (#478) * New models * CRUD UI * UI for adding/removing team members * Log display for teams * Fix invitations, move frontend * Drop old models (incomplete) * Drop more old stuff * Drop even more old stuff * Fix tests * Fix permission test * flake8 fix * Add tests fore the new code * Rebase migrations * Fix typo in method name * Update translations * Force ordering of events on dashboard * Fix typos in events * Prepare the pretixdroid API for an async mode in the app * Pretixdroid tests: Ignore microseconds (chopped by mysql) * pretixdroid API: Add related lookups * Add idempotenty nonces to pretixdroid API * pretixdroid: force-accepting unpaids and time display * Marked webfonts as binary files (#487) Webfonts now listed as binary in `.gitattributes`. Works on pretix/pretix#486 * Fix #456 -- Allow products to be excluded from ticket-generation (#483) * Added non-admission setting to event `ticket_download_nonadm` now setting in storage. Still need logic for order page/PDF generation. Works on pretix/pretix#456. * Download button considers `ticket_download_nonadm` Modified Django tags to look at item admission attribute and `ticket_download_nonadm` setting. Works on pretix/pretix#456. * Ticket output for non-admission disabled PDFs/etc. will only be permitted/generated for items with the `admission` attribute, or if the `ticket_download_nonadm` event setting is true. Applies to single and whole-order ticket downloads. Works on pretix/pretix#456. * Fixed product exclusion in PDF output Forgot PDF output was a plugin, now includes same check as base `BaseTicketOutput.generate_order`. Works on pretix/pretix#456 * Mail signature (#485) * added signature field -- no function yet * added mail signature feature * fixed style issue * fixed problem with signature default * added unit test for mail signatures * added unit test for mail signatures * [WIP] Fix #447 -- Sendmail plugin: Create new mail based on an old one (#476) * send old email content to the new one * error key event * test commit * query bad ID * query bad ID * query bad ID * query bad ID * Update pretixdroid API version * Refs #447 -- Extend copying old mails to subject and receipients * Fixed bugs and added test for date range rendering (#488) * fixed bug for same dates, added unit check for daterange * fixed local language override in unit test * Fix #297 -- pretixdroid: Show metrics in the control panel (#481) * add checkin status page add dashboard widget add checkin page under orders * modify checkin logic added new fields in checkin page added filter items * add tests for checkins & minor improvement * support addin_product & noadm setting logic * remove name ordering check test case * Fix #379 -- Add logo to event organizers (#431) * [WIP] Add logo to event organizers. * Fix indentation issues. * Refactor code Refactor code Refactor code * Add new migration * Take files into account for organizer sform (settings form) * Fix grammer * Make bootstrap form errors specific to each fieldset * Display logo on organizer's page * Fix PR issues Fix PR issues Fix PR issues * Reorder imports * Remove conflicting migration * Fix rebase conflict * Fix #41 -- Drag-and-drop ticket editor Undo/redo Useful toolbox Font selection Add text content Use hex for colors JS-side dump and load Save Load layout, proper undo/redo First steps to Python rendering More PDF rendering Copy and paste Buttons for keyboard actions Splash Screen Block unbeforeunload in dirty state Remove debugging output Preview Upload new PDFs via the editor Fix bugs during PDF reload, link in settings form New default ticket Add OpenSans BI Custom fonts, fix tests * Added bootstrap-colorpicker * Allow inline PDF display in CSP header * Add fontpack to list of plugins * Update German translation * Add ticketoutputpdf's assets to MANIFEST.i * Fix migration of old ticket styles * Fix iCal download URL * Multi-line location field, new field for admission time * Admission date and time in editor * Remove icon from "add to calendar" * Try to fix PDF display problems in Safari * Proxy cachedfiles that are used as editor previews * Check Event.presale_is_running in more places * Fix CSS generation with an empty color field * Fix missing placeholders and reformat the sendmail view * Fix bug that lead to wrong payment amount when switching payment method to PayPal later * Update translation
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,8 +18,9 @@ If you improved pretix in any way, we'd be very happy if you contribute it
|
||||
back to the main code base! The easiest way to do so is to `create a pull request`_
|
||||
on our `GitHub repository`_.
|
||||
|
||||
Before you do so, please `squash all your changes`_ into one single commit. Please
|
||||
use the test suite to check whether your changes break any existing features and run
|
||||
We recommend that you create a feature branch for every issue you work on so the changes can
|
||||
be reviewed individually.
|
||||
Please use the test suite to check whether your changes break any existing features and run
|
||||
the code style checks to confirm you are consistent with pretix's coding style. You'll
|
||||
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
|
||||
|
||||
@@ -34,4 +35,3 @@ Again: If you get stuck, do not hesitate to contact any of us, or Raphael person
|
||||
|
||||
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
|
||||
.. _GitHub repository: https://github.com/pretix/pretix
|
||||
.. _squash all your changes: https://davidwalsh.name/squash-commits-git
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,6 +83,10 @@ 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/
|
||||
|
||||
.. 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
|
||||
`Django's documentation`_ for more options.
|
||||
|
||||
.. _`checksandtests`:
|
||||
|
||||
Code checks and unit tests
|
||||
@@ -148,3 +152,7 @@ To build the documentation, run the following command from the ``doc/`` director
|
||||
make html
|
||||
|
||||
You will now find the generated documentation in the ``doc/_build/html/`` subdirectory.
|
||||
|
||||
|
||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||
|
||||
@@ -6,6 +6,7 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
user/index
|
||||
admin/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
@@ -21,6 +21,7 @@ same team:
|
||||
* `Pages`_
|
||||
* `Passbook/Wallet ticket output`_
|
||||
* `Cartshare`_
|
||||
* `Fontpack Free fonts`_
|
||||
|
||||
The following plugins are from independent third-party authors, so we can make
|
||||
no statements about their stability:
|
||||
@@ -34,3 +35,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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
User Guide
|
||||
==========
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
payments/index
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,14 @@
|
||||
Accepting payments
|
||||
==================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
overview
|
||||
fees
|
||||
paypal
|
||||
stripe
|
||||
banktransfer
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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/
|
||||
@@ -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.members.add(user)
|
||||
cat_tickets = ItemCategory.objects.create(
|
||||
event=event, name='Tickets'
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.3.0-alpha1"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-01 21:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0056_auto_20170414_1044'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='itemaddon',
|
||||
options={'ordering': ('position', 'pk')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemaddon',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemvariation',
|
||||
name='description',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, help_text='This is shown below the variation name in lists.', null=True, verbose_name='Description'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
@@ -269,7 +278,8 @@ class Event(LoggedModel):
|
||||
q.event = self
|
||||
q.save()
|
||||
for i in items:
|
||||
q.items.add(item_map[i.pk])
|
||||
if i.pk in item_map:
|
||||
q.items.add(item_map[i.pk])
|
||||
for v in vars:
|
||||
q.variations.add(variation_map[v.pk])
|
||||
|
||||
@@ -306,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)
|
||||
|
||||
@@ -310,6 +310,8 @@ class ItemVariation(models.Model):
|
||||
:type item: Item
|
||||
:param value: A string defining this variation
|
||||
:type value: str
|
||||
:param description: A short description
|
||||
:type description: str
|
||||
:param active: Whether this variation is being sold.
|
||||
:type active: bool
|
||||
:param default_price: This variation's default price
|
||||
@@ -327,6 +329,11 @@ class ItemVariation(models.Model):
|
||||
default=True,
|
||||
verbose_name=_("Active"),
|
||||
)
|
||||
description = I18nTextField(
|
||||
verbose_name=_("Description"),
|
||||
help_text=_("This is shown below the variation name in lists."),
|
||||
null=True, blank=True,
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
@@ -413,9 +420,14 @@ class ItemAddOn(models.Model):
|
||||
default=1,
|
||||
verbose_name=_('Maximum number')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('base_item', 'addon_category'),)
|
||||
ordering = ('position', 'pk')
|
||||
|
||||
def clean(self):
|
||||
if self.max_count < self.min_count:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ ALLOWED_TAGS = [
|
||||
'acronym',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'code',
|
||||
'em',
|
||||
'i',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'}),
|
||||
@@ -467,6 +477,15 @@ 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,
|
||||
@@ -586,7 +605,8 @@ 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'),
|
||||
@@ -624,6 +644,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()
|
||||
|
||||
@@ -211,6 +211,7 @@ class ItemVariationForm(I18nModelForm):
|
||||
'value',
|
||||
'active',
|
||||
'default_price',
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
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 +67,46 @@ 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):
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".svg", ".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 %}
|
||||
@@ -10,7 +10,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 +18,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 +55,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 +90,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 %}>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -24,6 +24,7 @@
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
@@ -31,8 +32,12 @@
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Add-On" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
|
||||
<div class="col-sm-4 text-right">
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,6 +56,7 @@
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
@@ -59,7 +65,11 @@
|
||||
<h3 class="panel-title">{% trans "Add-On" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.active layout='horizontal' %}
|
||||
{% bootstrap_field form.default_price layout='horizontal' %}
|
||||
{% bootstrap_field form.description layout='horizontal' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -69,6 +70,7 @@
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
|
||||
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
|
||||
{% bootstrap_field formset.empty_form.description layout='horizontal' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% 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>
|
||||
|
||||
@@ -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,22 @@
|
||||
{% 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.organizer_logo_image 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 %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from django.db.models import 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',
|
||||
'status': 'checkins__id',
|
||||
'-status': '-checkins__id',
|
||||
'timestamp': 'checkins__datetime',
|
||||
'-timestamp': '-checkins__datetime',
|
||||
'item': 'item__name',
|
||||
'-item': '-item__name',
|
||||
'name': ('display_name', {'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
|
||||
'-name': ('-display_name', {'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
|
||||
}
|
||||
@@ -13,7 +13,7 @@ from django.utils.formats import date_format
|
||||
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,
|
||||
@@ -168,6 +168,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 +228,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 +243,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,7 +264,8 @@ 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(
|
||||
|
||||
@@ -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,12 +311,13 @@ 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)
|
||||
resp = HttpResponse(pdf, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="invoice-preview.pdf"'
|
||||
resp['Content-Security-Policy'] = "style-src 'unsafe-inline'; script-src 'unsafe-inline'; object-src 'self'"
|
||||
resp['Content-Disposition'] = 'inline; filename="invoice-preview.pdf"'
|
||||
return resp
|
||||
|
||||
|
||||
@@ -328,7 +325,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 +361,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 +404,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):
|
||||
@@ -513,7 +510,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
|
||||
|
||||
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@cached_property
|
||||
def output(self):
|
||||
@@ -531,7 +528,11 @@ class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
fname, mimet, data = tickets.preview(self.request.event.pk, self.output.identifier)
|
||||
resp = HttpResponse(data, content_type=mimet)
|
||||
ftype = fname.split(".")[-1]
|
||||
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||
if mimet == "application/pdf":
|
||||
resp['Content-Security-Policy'] = "style-src 'unsafe-inline'; script-src 'unsafe-inline'; object-src 'self'"
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||
return resp
|
||||
|
||||
def get_error_url(self) -> str:
|
||||
@@ -545,7 +546,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 +638,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):
|
||||
@@ -840,9 +713,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 +729,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, Q
|
||||
from django.db.models import Count, F, Max, Q
|
||||
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
@@ -72,7 +72,7 @@ def item_move(request, item, up=True):
|
||||
if item.position != i:
|
||||
item.position = i
|
||||
item.save()
|
||||
messages.success(request, _('The order of items as been updated.'))
|
||||
messages.success(request, _('The order of items has been updated.'))
|
||||
|
||||
|
||||
@event_permission_required("can_change_items")
|
||||
@@ -803,6 +803,8 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
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={
|
||||
k: (form.cleaned_data.get(k).name
|
||||
@@ -943,7 +945,7 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
formsetclass = inlineformset_factory(
|
||||
Item, ItemAddOn,
|
||||
form=ItemAddOnForm, formset=ItemAddOnsFormSet,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
can_order=True, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
queryset=ItemAddOn.objects.filter(base_item=self.get_object()),
|
||||
@@ -963,12 +965,13 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
|
||||
forms = [
|
||||
ef for ef in self.formset.extra_forms + self.formset.initial_forms
|
||||
if ef not in self.formset.deleted_forms
|
||||
forms = self.formset.ordered_forms + [
|
||||
ef for ef in self.formset.extra_forms
|
||||
if ef not in self.formset.ordered_forms and ef not in self.formset.deleted_forms
|
||||
]
|
||||
for i, form in enumerate(forms):
|
||||
form.instance.base_item = self.get_object()
|
||||
form.instance.position = i
|
||||
created = not form.instance.pk
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
@@ -986,7 +989,7 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.get_object().category and self.get_object().category.is_addon:
|
||||
messages.error(self.request, _('Your cannot add addons to a product that is only available as an add-on '
|
||||
messages.error(self.request, _('You cannot add addons to a product that is only available as an add-on '
|
||||
'itself.'))
|
||||
return redirect(self.get_previous_url())
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'] = []
|
||||
@@ -82,141 +75,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 +138,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 +157,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
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ class WaitingListView(EventPermissionRequiredMixin, ListView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'assign' in request.POST:
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
messages.error(request, _('You do not have permission to do this'))
|
||||
return redirect(reverse('control:event.orders.waitinglist', kwargs={
|
||||
'event': request.event.slug,
|
||||
|
||||
@@ -6,12 +6,16 @@ def daterange(df, dt):
|
||||
lng = get_language()
|
||||
|
||||
if lng.startswith("de"):
|
||||
if df.year == dt.year and df.month == dt.month:
|
||||
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
|
||||
return "{}".format(_date(df, "j. F Y"))
|
||||
elif df.year == dt.year and df.month == dt.month:
|
||||
return "{}.–{}".format(_date(df, "j"), _date(dt, "j. F Y"))
|
||||
elif df.year == dt.year:
|
||||
return "{} – {}".format(_date(df, "j. F"), _date(dt, "j. F Y"))
|
||||
elif lng.startswith("en"):
|
||||
if df.year == dt.year and df.month == dt.month:
|
||||
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
|
||||
return "{}".format(_date(df, "N jS, Y"))
|
||||
elif df.year == dt.year and df.month == dt.month:
|
||||
return "{} – {}".format(_date(df, "N jS"), _date(dt, "jS, Y"))
|
||||
elif df.year == dt.year:
|
||||
return "{} – {}".format(_date(df, "N jS"), _date(dt, "N jS, Y"))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Date according to https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||
SHORT_DATE_FORMAT = 'Y-m-d'
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
||||
TIME_FORMAT = 'H:i'
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-04-17 15:09+0000\n"
|
||||
"PO-Revision-Date: 2017-04-17 17:10+0200\n"
|
||||
"POT-Creation-Date: 2017-05-11 10:12+0000\n"
|
||||
"PO-Revision-Date: 2017-05-11 12:13+0200\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.8.11\n"
|
||||
"X-Generator: Poedit 2.0.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
||||
@@ -46,6 +46,96 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:21
|
||||
msgid "QR Code"
|
||||
msgstr "QR-Code"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:157
|
||||
msgid "Sample product"
|
||||
msgstr "Beispielprodukt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:158
|
||||
msgid "Sample variation"
|
||||
msgstr "Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:159
|
||||
msgid "Sample product – sample variation"
|
||||
msgstr "Beispielprodukt – Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:160
|
||||
msgid "123.45 EUR"
|
||||
msgstr "123,45 EUR"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:161
|
||||
msgid "John Doe"
|
||||
msgstr "Max Mustermann"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:162
|
||||
msgid "Sample event name"
|
||||
msgstr "Beispielevent"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:163
|
||||
msgid "May 31st, 2017"
|
||||
msgstr "31. Mai 2017"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:164
|
||||
msgid "20:00"
|
||||
msgstr "20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:165
|
||||
msgid "19:00"
|
||||
msgstr "19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:166
|
||||
msgid "2017-05-31 20:00"
|
||||
msgstr "31.05.2016 20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:167
|
||||
msgid "2017-05-31 19:00"
|
||||
msgstr "31.05.2016 19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:168
|
||||
msgid "Random City"
|
||||
msgstr "Musterstadt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:213
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:362
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:368
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:370
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:372
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:376
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:612
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:644
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:657
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asyncdownload.js:28
|
||||
#: pretix/static/pretixbase/js/asynctask.js:42
|
||||
#: pretix/static/pretixbase/js/asynctask.js:94
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-04-17 15:09+0000\n"
|
||||
"PO-Revision-Date: 2017-04-17 17:09+0200\n"
|
||||
"POT-Creation-Date: 2017-05-11 10:12+0000\n"
|
||||
"PO-Revision-Date: 2017-05-11 12:13+0200\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.8.11\n"
|
||||
"X-Generator: Poedit 2.0.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
||||
@@ -46,6 +46,96 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:21
|
||||
msgid "QR Code"
|
||||
msgstr "QR-Code"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:157
|
||||
msgid "Sample product"
|
||||
msgstr "Beispielprodukt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:158
|
||||
msgid "Sample variation"
|
||||
msgstr "Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:159
|
||||
msgid "Sample product – sample variation"
|
||||
msgstr "Beispielprodukt – Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:160
|
||||
msgid "123.45 EUR"
|
||||
msgstr "123,45 EUR"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:161
|
||||
msgid "John Doe"
|
||||
msgstr "Max Mustermann"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:162
|
||||
msgid "Sample event name"
|
||||
msgstr "Beispielevent"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:163
|
||||
msgid "May 31st, 2017"
|
||||
msgstr "31. Mai 2017"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:164
|
||||
msgid "20:00"
|
||||
msgstr "20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:165
|
||||
msgid "19:00"
|
||||
msgstr "19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:166
|
||||
msgid "2017-05-31 20:00"
|
||||
msgstr "31.05.2016 20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:167
|
||||
msgid "2017-05-31 19:00"
|
||||
msgstr "31.05.2016 19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:168
|
||||
msgid "Random City"
|
||||
msgstr "Musterstadt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:213
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:362
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:368
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:370
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:372
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:376
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:612
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:644
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:657
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asyncdownload.js:28
|
||||
#: pretix/static/pretixbase/js/asynctask.js:42
|
||||
#: pretix/static/pretixbase/js/asynctask.js:94
|
||||
|
||||
@@ -17,7 +17,7 @@ def register_payment_provider(sender, **kwargs):
|
||||
@receiver(nav_event, dispatch_uid="payment_banktransfer_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -39,6 +39,10 @@ class Paypal(BasePaymentProvider):
|
||||
('client_id',
|
||||
forms.CharField(
|
||||
label=_('Client ID'),
|
||||
help_text=_('<a target="_blank" href="{docs_url}">{text}</a>').format(
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
|
||||
)
|
||||
)),
|
||||
('secret',
|
||||
forms.CharField(
|
||||
|
||||
@@ -57,7 +57,19 @@ def success(request, *args, **kwargs):
|
||||
@event_view(require_live=False)
|
||||
def abort(request, *args, **kwargs):
|
||||
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
if request.session.get('payment_paypal_order'):
|
||||
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
|
||||
else:
|
||||
order = None
|
||||
|
||||
if order:
|
||||
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}) + ('?paid=yes' if order.status == Order.STATUS_PAID else ''))
|
||||
else:
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import json
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.signals import logentry_display
|
||||
@@ -11,7 +14,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="pretixdroid_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
@@ -32,11 +35,33 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
return
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
|
||||
show_dt = False
|
||||
if 'datetime' in data:
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
show_dt = abs((logentry.datetime - dt).total_seconds()) > 60 or 'forced' in data
|
||||
tz = pytz.timezone(sender.settings.timezone)
|
||||
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
|
||||
if data.get('first'):
|
||||
return _('Position #{posid} has been scanned.'.format(
|
||||
posid=data.get('positionid')
|
||||
))
|
||||
if show_dt:
|
||||
return _('Position #{posid} has been scanned at {datetime}.').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
)
|
||||
else:
|
||||
return _('Position #{posid} has been scanned.').format(
|
||||
posid=data.get('positionid')
|
||||
)
|
||||
else:
|
||||
if data.get('forced'):
|
||||
return _(
|
||||
'A scan for position #{posid} at {datetime} has been uploaded even though it has '
|
||||
'been scanned already.'.format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
)
|
||||
)
|
||||
return _('Position #{posid} has been scanned and rejected because it has already been scanned before.'.format(
|
||||
posid=data.get('positionid')
|
||||
))
|
||||
|
||||
@@ -9,6 +9,8 @@ urlpatterns = [
|
||||
name='api.redeem'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/search/', views.ApiSearchView.as_view(),
|
||||
name='api.search'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/download/', views.ApiDownloadView.as_view(),
|
||||
name='api.download'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/status/', views.ApiStatusView.as_view(),
|
||||
name='api.status'),
|
||||
]
|
||||
|
||||