Compare commits

...

102 Commits

Author SHA1 Message Date
Raphael Michel
3acc29f51a Bump release 2017-08-21 15:12:05 +02:00
Raphael Michel
4bae0d1b81 [SECURITY] Rewrite all links in rich texts 2017-08-21 15:11:54 +02:00
Raphael Michel
c38a850294 [SECURITY] Fix XSS vulnerability in Lightbox caption 2017-08-21 15:09:44 +02:00
Raphael Michel
a7ec475c40 [SECURITY] Do not allow SVG files for logos 2017-08-21 15:08:39 +02:00
Raphael Michel
ac1467bd4b [SECURITY] Fix XSS injection vulnerabilities in question answers, event, quota and product names 2017-08-21 15:08:26 +02:00
Raphael Michel
3f0af67345 [SECURITY] Update to morris.js master to fix a XSS vulnerability 2017-08-21 15:07:56 +02:00
Raphael Michel
4d14b6c096 [SECURITY] Use defusedcsv for exports 2017-08-21 15:06:58 +02:00
Raphael Michel
cb789bc06c Ignore style in tests, change import order 2017-06-01 13:28:28 +02:00
Raphael Michel
17dad33f8b Bump version to 1.4.0 2017-06-01 12:57:29 +02:00
Raphael Michel
1d5c160e1d Document contact_form_fields 2017-06-01 12:57:11 +02:00
Raphael Michel
f04b7fa365 Update translations 2017-06-01 12:56:53 +02:00
Raphael Michel
fa011fbdce Update django-redis 2017-05-31 07:51:16 +02:00
Raphael Michel
759c5374d9 Fix chardet pinning in setup.py 2017-05-30 19:22:28 +02:00
Raphael Michel
f631acdf18 Force chardet update 2017-05-30 19:12:44 +02:00
Raphael Michel
b2dfd8ab11 Bankimport: Force re-annotation for badly parsed files 2017-05-30 18:43:53 +02:00
Raphael Michel
43f4803da7 Fix name being used instead of email in CSV files 2017-05-30 18:00:21 +02:00
Raphael Michel
019d8220b8 Fix wrong text with banktransfer orders manually marked as paid 2017-05-30 17:57:50 +02:00
Raphael Michel
b946010bdb Add event date range to ticket editor 2017-05-30 17:18:03 +02:00
Raphael Michel
c3097b12c3 Fix typo 2017-05-30 17:14:17 +02:00
Raphael Michel
12a53710c3 Fix test_csvparser case that changed with newer chardet 2017-05-27 14:28:39 +02:00
Raphael Michel
983326e610 Stop pinning chardet/setuptools versions 2017-05-27 12:50:18 +02:00
Claudio Luck
b4eb707b38 bump required mt940 to v4.7 2017-05-27 11:59:21 +02:00
Claudio Luck
e44f34f0a9 mt940: also consider non-SWIFT field NS 2017-05-27 11:59:21 +02:00
Claudio Luck
3c762adbf4 mt940: payer is not always available 2017-05-27 11:59:21 +02:00
Raphael Michel
ebabd20d09 [Django 1.11] Refs #481 -- Explicit sorting of NULLs 2017-05-26 09:44:11 +02:00
Raphael Michel
8694e1901a [Django 1.11] Port AddOnVariationField 2017-05-26 09:44:11 +02:00
Tobias Kunze
0f2875e89a [Django 1.11] Upgrade to Django 1.11 2017-05-26 09:44:11 +02:00
Raphael Michel
74d9921be1 Fix error 500 during building error 400 responses 2017-05-26 09:37:48 +02:00
Tobias Kunze
41e56adfdb Make checkin lists available by default (#508) 2017-05-25 16:47:09 +02:00
Raphael Michel
4ff1d302d9 Fix missing argument 2017-05-25 14:55:04 +02:00
Raphael Michel
d6e213d51a Disable inline pdfs again due to Safari issues 2017-05-25 14:43:31 +02:00
Raphael Michel
2a4deeba55 List commercial plugins 2017-05-24 14:34:20 +02:00
Raphael Michel
24b1d2afcb Disable CssAbsoluteFilter 2017-05-24 14:34:20 +02:00
Leah Oswald
5cea3d824a Fix some typos in the backup and monitoring documentation (#506)
* fix missing word typo in backup and monitoring documentation

* fix another typo in backup and monitoring documentation
2017-05-24 13:40:12 +02:00
Raphael Michel
78a8a7d744 Add Order.meta_info_data 2017-05-23 12:38:00 +02:00
Raphael Michel
a3bf85754a Add signal contact_form_fields 2017-05-23 11:43:05 +02:00
Raphael Michel
006b6fd5e8 Add support for image icons in navigation 2017-05-23 11:42:42 +02:00
Raphael Michel
8ff2c42070 Fix order changing with zero-priced products 2017-05-22 21:13:05 +02:00
Raphael Michel
c5d18c6884 Clear settings cache after migration 2017-05-22 14:17:52 +02:00
Heok Hong Low
48b3621f1e Fix #499 -- Refactor paymentinfo to payment_info (#501)
* Refactor paymentinfo to payment_info, resolve #499

* Fix calling of object attribute on tuple

* Minor update to setup documentation

* Do not use short words for typochecks

* Text clarification

* Refactor paymentinfo to payment_info, resolve #499

* Include data migration for existing event settings, resolve #499
2017-05-22 14:06:19 +02:00
Raphael Michel
fb716eb498 Add add-ons to ticket editor 2017-05-22 14:04:14 +02:00
Tobias Kunze
5d8e294350 Fix typo 2017-05-22 09:25:15 +02:00
Raphael Michel
8a96d8c24e Text clarification 2017-05-21 10:47:12 +02:00
Raphael Michel
6396d2f922 Do not use short words for typochecks 2017-05-21 10:47:12 +02:00
lhhong
ed7e90451b Minor update to setup documentation 2017-05-21 10:29:56 +02:00
lhhong
f5990dd5c4 Fix calling of object attribute on tuple 2017-05-21 10:26:58 +02:00
Knuth
f29f615aec added a missing string and translation for it 2017-05-18 11:57:36 +02:00
Raphael Michel
f1300badb8 Bump version 2017-05-18 11:17:06 +02:00
Raphael Michel
88a1cddb23 Add release config for GitLab CI 2017-05-18 09:50:51 +02:00
Raphael Michel
d53fbc52d1 Fix #322 -- Do not use ManifestStaticFileStorage during testing 2017-05-17 18:20:13 +02:00
Raphael Michel
ec9511c17a Smaller documentation updates 2017-05-17 18:19:28 +02:00
Raphael Michel
3f2bb3beae Add date to the metadata section 2017-05-17 18:06:19 +02:00
Raphael Michel
c8b96696b0 Update/fix German translation 2017-05-17 18:06:19 +02:00
Raphael Michel
0bb6b53ebc Change client-side TIME_FORMAT for locale en as well 2017-05-17 18:06:19 +02:00
Raphael Michel
7400c2373b Remove debugging output 2017-05-17 18:06:19 +02:00
Knuth
635329f102 added missing spaces in DE and DE_informal 2017-05-17 17:05:49 +02:00
scabux
b5a2123d1c Added configurable text for organizer page (#494) 2017-05-17 15:53:30 +02:00
Raphael Michel
396c558b25 Fix critical bug in item creation 2017-05-17 15:47:35 +02:00
Ian Williams
60e178d821 Fix #443 -- Show private key in 2FA wizard (#490)
* 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

* Revert "Syncing fork to upstream (#1)"

This reverts commit 847d409a00.
Merged wrong, my bad.

* Formatted OTP secret

New variable `secretGrouped` in `2fa_confirm_totp.html`, user-friendly
version of OTP secret (split every 4 characters).
Works on pretix/pretix#443.

* Adds manual secret entry OTP setup screen

`secretGrouped` exposed in user-friendly fashion. Includes short
instructions, copy-to-clipboard button, and js to hide instructions
unless user clicks on "Can't scan the barcode?" link.
Works on pretix/pretix#443.

* Minor indentation issuer

Fixed indentation issue (L40).
Works on pretix/pretix#443.

* Minor spacing issues

L265 of `user.py` failing flake8 tests, minor spacing fixes.

* Fixes indentation in `2fa_confirm_totp.html`

Per https://github.com/pretix/pretix/pull/490#discussion_r116165041,
fixes an issue with indentation.
Works on pretix/pretix#443, member of pretix/pretix#490.

* Removed `aria-*` attributes

Per https://github.com/pretix/pretix/pull/490#discussion_r116165115,
removes `aria` attributes from sub-tutorial.
Works on pretix/pretix#443, member of pretix/pretix#490.

* Pretix capitalization issue

Per https://github.com/pretix/pretix/pull/490#discussion_r116165193,
fixes an issue with capitalization of pretix.
Works on pretix/pretix#443, member of pretix/pretix#490.
2017-05-17 09:31:28 +02:00
Raphael Michel
7769aaccea Fix #492 -- Force deterministic ordering of some signals 2017-05-15 13:09:23 +02:00
Raphael Michel
5ff6d0b014 Auto-detect common typos in email addresses 2017-05-15 11:33:18 +02:00
Raphael Michel
7d9a1b5e0c Update translation 2017-05-11 12:15:23 +02:00
Raphael Michel
127086a50e Fix bug that lead to wrong payment amount when switching payment method to PayPal later 2017-05-10 19:12:55 +02:00
Raphael Michel
90d14c004f Fix missing placeholders and reformat the sendmail view 2017-05-10 15:14:23 +02:00
Raphael Michel
8c457c057e Fix CSS generation with an empty color field 2017-05-10 15:09:07 +02:00
Raphael Michel
36215a989a Check Event.presale_is_running in more places 2017-05-10 15:04:01 +02:00
Raphael Michel
74259bd4c6 Proxy cachedfiles that are used as editor previews 2017-05-10 15:02:54 +02:00
Raphael Michel
4561b7dce2 Try to fix PDF display problems in Safari 2017-05-10 15:02:28 +02:00
Raphael Michel
89d6166dbe Remove icon from "add to calendar" 2017-05-10 13:28:07 +02:00
Raphael Michel
f451977a04 Admission date and time in editor 2017-05-10 13:16:39 +02:00
Raphael Michel
aac05727ed Multi-line location field, new field for admission time 2017-05-10 12:58:57 +02:00
Raphael Michel
c9ae65a9a8 Fix iCal download URL 2017-05-10 12:57:59 +02:00
Raphael Michel
f4a9dbb546 Fix migration of old ticket styles 2017-05-09 17:23:17 +02:00
Raphael Michel
3eec791584 Add ticketoutputpdf's assets to MANIFEST.i 2017-05-09 11:55:25 +02:00
Raphael Michel
57473da182 Update German translation 2017-05-09 11:21:42 +02:00
Raphael Michel
fd90752a12 Add fontpack to list of plugins 2017-05-09 11:10:10 +02:00
Raphael Michel
0d3f5e0c32 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
2017-05-09 11:10:10 +02:00
Raphael Michel
c98b0aac90 Added bootstrap-colorpicker 2017-05-09 11:10:10 +02:00
Raphael Michel
3de85975d6 Allow inline PDF display in CSP header 2017-05-09 11:10:10 +02:00
Bolutife Lawrence
962cdef69a 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
2017-05-08 17:46:35 +02:00
jlwt90
b301d20488 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
2017-05-08 17:31:37 +02:00
scabux
1b2895b0ca 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
2017-05-07 10:40:36 +02:00
Raphael Michel
d6943bb1dd Refs #447 -- Extend copying old mails to subject and receipients 2017-05-06 15:13:53 +02:00
Raphael Michel
51d74503ae Update pretixdroid API version 2017-05-06 14:40:28 +02:00
asv-hungvt
5eac3cf9cf [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
2017-05-06 14:39:48 +02:00
scabux
27d6f24c3c 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
2017-05-06 11:12:38 +02:00
Ian Williams
d929b163db 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
2017-05-06 11:11:11 +02:00
Ian Williams
571b3fbfa8 Marked webfonts as binary files (#487)
Webfonts now listed as binary in `.gitattributes`.
Works on pretix/pretix#486
2017-05-06 10:50:47 +02:00
Raphael Michel
db05f8eaa3 pretixdroid: force-accepting unpaids and time display 2017-05-04 11:57:29 +02:00
Raphael Michel
d8eba81efc Add idempotenty nonces to pretixdroid API 2017-05-04 09:32:36 +02:00
Raphael Michel
429ef67bbf pretixdroid API: Add related lookups 2017-05-03 19:17:37 +02:00
Raphael Michel
e5d9e69e3e Pretixdroid tests: Ignore microseconds (chopped by mysql) 2017-05-03 18:51:16 +02:00
Raphael Michel
dc32bdc474 Prepare the pretixdroid API for an async mode in the app 2017-05-03 18:06:32 +02:00
Raphael Michel
c45b709d54 Fix typos in events 2017-05-03 18:04:41 +02:00
Raphael Michel
e1be084d98 Force ordering of events on dashboard 2017-05-03 18:02:10 +02:00
Raphael Michel
20b8aafe11 Update translations 2017-05-03 17:23:44 +02:00
Raphael Michel
c719adf235 Fix typo in method name 2017-05-03 17:11:55 +02:00
Raphael Michel
d08a0bdb00 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
2017-05-03 16:55:37 +02:00
Raphael Michel
8294391ebc Hide payment fees if they are all equal to 0.00 2017-05-02 19:12:42 +02:00
Raphael Michel
394c206133 Link PayPal and Stripe documentation in the respective forms 2017-05-02 19:07:19 +02:00
Raphael Michel
2fcd561ff7 Add a user guide on payments 2017-05-02 18:59:18 +02:00
Raphael Michel
f95b77fd7a Ticket PDFs: Do not hide attendee name if code is hidden 2017-05-02 17:07:09 +02:00
217 changed files with 99494 additions and 3301 deletions

14
.gitattributes vendored
View File

@@ -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

View File

@@ -2,11 +2,31 @@ before_script:
tests:
stage: test
script:
- virtualenv-3.4 env
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache bash .travis.sh style
- XDG_CACHE_HOME=/cache bash .travis.sh tests
- XDG_CACHE_HOME=/cache bash .travis.sh doctests
tags:
- python3
pypi:
stage: release
script:
- cp /keys/.pypirc ~/.pypirc
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
- cd src
- python setup.py sdist upload
- python setup.py bdist_wheel upload
tags:
- python3
only:
- release
artifacts:
paths:
- src/dist/
stages:
- test
- build
- release

View File

@@ -51,7 +51,7 @@ If there is a problem, a status code in the ``5xx`` range will be returned.
Performance monitoring
----------------------
If you to generate detailled performance statistics of your pretix installation, there is an
If you want to generate detailed performance statistics of your pretix installation, there is an
endpoint at ``https://pretix.mydomain.com/metrics`` (no slash at the end) which returns a
number of values in the text format understood by monitoring tools like Prometheus_. This data
is only collected and exposed if you enable it in the :ref:`metrics-settings` section of your

View File

@@ -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.

View File

@@ -25,7 +25,7 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, checkout_confirm_messages
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, checkout_confirm_messages
.. automodule:: pretix.presale.signals

View File

@@ -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

View File

@@ -40,7 +40,7 @@ automatically). If you are working on Ubuntu or Debian, we strongly recommend up
your pip and setuptools installation inside the virtual environment, otherwise some of
the dependencies might fail::
pip3 install -U pip setuptools==28.6.1
pip3 install -U pip setuptools
Working with the code
---------------------
@@ -81,7 +81,7 @@ and head to http://localhost:8000/
As we did not implement an overall front page yet, you need to go directly to
http://localhost:8000/control/ for the admin view or, if you imported the test
data as suggested above, to the event page at http://localhost:8000/bigevents/2017/
data as suggested above, to the event page at http://localhost:8000/bigevents/2018/
.. note:: If you want the development server to listen on a different interface or
port (for example because you develop on `pretixdroid`_), you can check

View File

@@ -26,8 +26,11 @@ pretix/
multidomain/
Additional code implementing our customized :ref:`URL handling <urlconf>`.
static/
Contains all static files (CSS, JavaScript, images)
static/
Contains all static files (CSS, JavaScript, images)
static/
Contains some pretix plugins that ship with pretix itself
tests/
This is the root directory for all test codes. It includes subdirectories ``base``,
@@ -37,7 +40,8 @@ tests/
Language files
--------------
The language files live in ``locale/*/LC_MESSAGES/``.
The language files live in ``pretix/locale/*/LC_MESSAGES/``.
Static files
------------
@@ -49,27 +53,14 @@ We use libsass as a preprocessor for CSS. Our own sass code is built in the same
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
pretix.control
pretixcontrol has two main SCSS files, ``pretix/control/static/pretixcontrol/scss/main.scss`` and
``pretix/control/static/pretixcontrol/scss/auth.scss``, importing everything else.
pretixcontrol has two main SCSS files, ``pretix/static/pretixcontrol/scss/main.scss`` and
``pretix/static/pretixcontrol/scss/auth.scss``, importing everything else.
pretix.presale
pretixpresale has one main SCSS files, ``pretix/control/static/pretix/presale/scss/main.scss``,
pretixpresale has one main SCSS files, ``pretix/pretixpresale/scss/main.scss``,
importing everything else.
3rd-party assets
^^^^^^^^^^^^^^^^
Bootstrap
Bootstrap lives vendored at ``static/bootstrap/``
Font Awesome
Font Awesome lives vendored at ``static/fontawesome/``
jQuery
jQuery lives as a single JavaScript file in ``static/jquery/js/``
jQuery plugin: Django formsets
Our own modified version of `django-formset-js`_ is available as an independent
django app and installed via ``pip``.
.. _django-formset-js: https://github.com/pretix/django-formset-js
Most client-side 3rd-party assets are vendored in various subdirectories of ``pretix/static``.

View File

@@ -6,6 +6,7 @@ Contents:
.. toctree::
:maxdepth: 2
user/index
admin/index
development/index
plugins/index

View File

@@ -15,15 +15,26 @@ ways that pretix itself is:
* PDF ticket output
The following plugins are not shipped with pretix but are maintained by the
same team:
same team. We update them regularly to make them compatible with the latest
pretix releases:
* `SEPA direct debit`_
* `Pages`_
* `Passbook/Wallet ticket output`_
* `Cartshare`_
* `Fontpack Free fonts`_
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
Please get in touch with the pretix team if you want to have them for your self-hosted
pretix installation:
* Campaign tracking
* Integration with Google Analytics and Facebook Pixel
* Integration with Slack
* Integration with MailChimp
The following plugins are from independent third-party authors, so we can make
no statements about their stability:
no statements about their stability or compatibility:
* `esPass ticket output`_
* `IcePay integration`_
@@ -34,3 +45,4 @@ no statements about their stability:
.. _Pages: https://github.com/pretix/pretix-pages
.. _esPass ticket output: https://github.com/esPass/pretix-espass
.. _IcePay integration: https://github.com/chotee/pretix-icepay
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free

View File

@@ -4,6 +4,10 @@ pretixdroid HTTP API
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
uses to communicate with the pretix server.
.. warning:: This API is intended **only** to serve the pretixdroid Android app. There are no backwards compatibility
guarantees on this API. We will not add features that are not required for the Android App. There will be
a proper general-use API for pretix at a later point in time.
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
Redeems a ticket, i.e. checks the user in.
@@ -19,6 +23,16 @@ uses to communicate with the pretix server.
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3
You can optionally include the additional parameter ``datetime`` in the body containing an ISO8601-encoded
datetime of the entry attempt. If you don't, the current date and time will be used.
You can optionally include the additional parameter ``force`` to indicate that the request should be logged
regardless of previous check-ins for the same ticket. This might be useful if you made the entry decision offline.
You can optionally include the additional parameter ``nonce`` with a globally unique random value to identify this
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
failure.
**Example successful response**:
.. sourcecode:: http
@@ -51,9 +65,9 @@ uses to communicate with the pretix server.
* ``unknown_ticket`` - Secret does not match a ticket in the database
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. http:get:: /pretixdroid/api/(organizer)/(event)/search/
@@ -97,6 +111,46 @@ uses to communicate with the pretix server.
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. http:get:: /pretixdroid/api/(organizer)/(event)/download/
Download data for all tickets.
**Example request**:
.. sourcecode:: http
GET /pretixdroid/api/demoorga/democon/download/?key=ABCDEF HTTP/1.1
Host: demo.pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: text/json
{
"results": [
{
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
"order": "ABCE6",
"item": "Standard ticket",
"variation": null,
"attendee_name": "Peter Higgs",
"redeemed": false,
"paid": true
},
...
],
"version": 2
}
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. http:get:: /pretixdroid/api/(organizer)/(event)/status/
Returns status information, such as the total number of tickets and the

9
doc/user/index.rst Normal file
View File

@@ -0,0 +1,9 @@
User Guide
==========
Contents:
.. toctree::
:maxdepth: 2
payments/index

View File

@@ -0,0 +1,31 @@
.. _`banktransfer`:
Bank transfer
=============
To accept payments with bank transfer, you only need to fill one important field in pretix' settings: In "Bank
account details" you should specify everything one needs to know to transfer money to you, e.g. your IBAN and BIC,
the name of your bank and for international transfers, preferably also your address and the bank's address.
pretix will automatically tell the user to include the order code in the payment reference so incoming transfers can
automatically be matched to payments.
Importing payment data
----------------------
The easiest way to import payment data is to download a CSV file from your online banking. Most banks provide a CSV
export of some sort. You can go to "Import bank data" in pretix to upload a new file:
.. image:: img/bank1.png
If you upload a file for the first time, pretix will not know what information is contained in which column as every
bank builds completely different CSV files. Therefore, pretix will ask you for that information. It will show you the
data of the file you imported and ask you to define the column's meanings. You can select one column that contains
the payment date and one that contains the paid amount. You can select multiple columns that contain information
about the payer or the payment reference. All other columns will be ignored.
Once you continue, pretix will try to match the payments to the respective orders automatically. It will tell you how
many orders could be processed correctly and how many could not. You can then go back to the upload page to see all
transfers from your bank statement that are not yet matched to an order. Using the input field and the buttons on the
left of each transaction, you can manually enter an order code to match it to or just discard it from the list, e.g.
if the transaction is not related to the event at all.

View File

@@ -0,0 +1,52 @@
Payment method fees
===================
Most external payment providers like PayPal or Stripe charge substantial fees for your service. In general, you have
two options to deal with this:
1. Pay the fees yourself
2. Add the fees to your customer's total
The choice totally depends on you and what your customers expect from you. Option two might be appropriate if you
offer different payment methods and want to encourage your customers to use the ones that come you cheaper, but you
might also decide to go for option one to make it easier for customers who don't have the option.
If you go for the second option, you can configure pretix to charge the payment method fees to your user. You can
define both an absolute fee as well as a percental fee based on the order total. If you do so, there are two
different ways in which pretix can calculate the fee. Normally, it is fine to just go with the default setting, but
in case you are interested, here are all the details:
Payment fee calculation
-----------------------
If you configure a fee for a payment method, there are two possible ways for us to calculate this. Let's
assume that your payment provider, e.g. PayPal, charges you 5 % fees and you want to charge your users the
same 5 %, such that for a ticket with a list price of 100 € you will get your full 100 €.
**Method A: Calculate the fee from the subtotal and add it to the bill.**
For a ticket price of 100 €, this will lead to the following calculation:
============================================== ============
Ticket price 100.00 €
pretix calculates the fee as 5 % of 100 € +5.00 €
Subtotal that will be paid by the customer 105.00 €
PayPal calculates its fee as 5 % of 105 € -5.25 €
End total that is on your bank account **99.75 €**
============================================== ============
**Method B (default): Calculate the fee from the total value including the fee.**
For a ticket price of 100 €, this will lead to the following calculation:
===================================================== =============
Ticket price 100.00 €
pretix calculates the fee as 100/(100 - 5) % of 100 € +5.26 €
Subtotal that will be paid by the customer 105.26 €
PayPal calculates its fee as 5 % of 105 € -5.26 €
End total that is on your bank account **100.00 €**
===================================================== =============
Due to the various rounding steps performed by pretix and by the payment provider, the end total on
your bank account might stil vary by one cent.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,14 @@
Accepting payments
==================
Contents:
.. toctree::
:maxdepth: 2
overview
fees
paypal
stripe
banktransfer

View File

@@ -0,0 +1,34 @@
Payment method overview
=======================
pretix allows you to accept payments using a variety of payment methods to fit the needs of very different events.
This page gives you a short overview over them and links to more detailled descriptions in some cases.
Payment methods are built as pretix plugins. For this reason, you might first need to enable a certain plugin at
"Settings" → "Plugins" in your event settings. Then, you can configure them in detail at "Settings" -> "Payment".
If you host pretix on your own server, you might need to install a plugin first for some of the payment methods listed
on this page as well as for additional ones.
:ref:`stripe`
Stripe is a US-based company that offers you an easy way to accept credit card payments from all over the world.
To accept payments with Stripe, you need to have a Stripe merchant account that is easy to create. Click on the link
above to get more details about the Stripe integration into pretix.
:ref:`paypal`
If you want to accept online payments via PayPal, you can do so using pretix. You will need a PayPal merchant
account and it is a little bit complicated to obtain the required technical details, but we've got you covered.
Click on the link above to learn more.
:ref:`banktransfer`
Classical IBAN wire transfers are a common payment method in central Europe that has the large benefit that it
often does not cause any additional fees. However, it requires you to invest some more effort as you need to
check your bank account for incoming payments regularly. We provide some tools to make this easier for you.
SEPA debit
In some Europen countries, a very popular online payment method is SEPA direct debit. If you want to offer this
option in your pretix ticket shop, we provide a convenient plugin that allows users to enter their SEPA bank
account details and issue a SEPA mandate. You will then need to regularly download a SEPA XML file from pretix
and upload it to your bank's interface to actually perform the debits.

View File

@@ -0,0 +1,50 @@
.. _`paypal`:
PayPal
======
To integrate PayPal with pretix, you first need to have an active PayPal merchant account. If you do not already have a
PayPal account, you can create one on `paypal.com`_.
If you look into pretix' settings, you are required to fill in two keys:
.. image:: img/paypal_pretix.png
Unfortunately, it is not straightforward how to get those keys from PayPal's website. In order to do so, you
need to go to `developer.paypal.com`_ to link the account to your pretix event.
Click on "Log In" in the top-right corner and log in with your PayPal account.
.. image:: img/paypal2.png
Then, click on "Dashboard" in the top-right corner.
.. image:: img/paypal3.png
In the dashboard, scroll down until you see the headline "REST API Apps". Click "Create App".
.. image:: img/paypal4.png
Enter any name for the application that helps you to identify it later. Then confirm with "Create App".
.. image:: img/paypal5.png
On the next page, before you do anything else, switch the mode on the right to "Live" to get the correct keys.
Then, copy the "Client ID" and the "Secret" and enter them into the appropriate fields in the payment settings in
pretix.
.. image:: img/paypal6.png
Finally, we need to create a webhook. The webhook tells PayPal to notify pretix e.g. if a payment gets cancelled so
pretix can cancel the ticket as well. If you have multiple events connected to your PayPal account, you need multiple
webhooks. To create one, scroll a bit down and click "Add Webhook".
.. image:: img/paypal7.png
Then, enter the webhook URL that you find on the pretix settings page. It should look similar to the one in the
screenshot but contain your event name. Tick the box "All events" and save.
.. image:: img/paypal8.png
That's it, you are ready to go!
.. _paypal.com: https://www.paypal.com/webapps/mpp/account-selection
.. _developer.paypal.com: https://developer.paypal.com/

View File

@@ -0,0 +1,28 @@
.. _stripe:
Stripe
======
To integrate Stripe with pretix, you first need to have an active Stripe merchant account. If you do not already have a
Stripe account, you can create one on `stripe.com`_. Then, click on "API" in the left navigation of the Stripe
Dashboard. As you can see in the following screenshot, you will be presented with two sets of API keys, one for test
and one for live payments. In each set, there is a secret and a publishable keys.
.. image:: img/stripe1.png
Choose one of the two sets and copy the two keys to the appropriate fields in pretix' settings. To perform actual
payments, you will need to use the live keys, but you can use the test keys to test the payment flow before you go live.
In test mode, you cannot use your real credit card, but only `test cards`_ like ``4242424242424242`` that you can
find in Stripe's documentation.
If you want Stripe to notify pretix automatically once a payment gets cancelled, so pretix can cancel the ticket as
well, you need to create a so-called webhook. To do so, click "Webhooks" on top of the page in the Stripe dashboard
that you are currently on. Then, click "Add endpoint" and enter the URL that you find directly below the key
configuration in pretix' settings.
.. image:: img/stripe2.png
Again, you can choose between live mode and test mode here.
.. _stripe.com: https://dashboard.stripe.com/register
.. _test cards: https://stripe.com/docs/testing#cards

View File

@@ -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 *

View File

@@ -23,9 +23,6 @@ user.save()
organizer = Organizer.objects.create(
name='BigEvents LLC', slug='bigevents'
)
OrganizerPermission.objects.get_or_create(
organizer=organizer, user=user
)
year = now().year + 1
event = Event.objects.create(
organizer=organizer, name='Demo Conference {}'.format(year),
@@ -33,9 +30,13 @@ event = Event.objects.create(
date_from=datetime(year, 9, 4, 17, 0, 0),
date_to=datetime(year, 9, 6, 17, 0, 0),
)
EventPermission.objects.get_or_create(
event=event, user=user
t = Team.objects.get_or_create(
organizer=organizer, name='Admin Team',
all_events=True, can_create_events=True, can_change_teams=True,
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
)
t[0].members.add(user)
cat_tickets = ItemCategory.objects.create(
event=event, name='Tickets'
)

View File

@@ -1 +1 @@
__version__ = "1.3.0"
__version__ = "1.4.1"

View File

@@ -1,9 +1,9 @@
import csv
import io
from collections import OrderedDict
from decimal import Decimal
import pytz
from defusedcsv import csv
from django import forms
from django.db.models import Sum
from django.dispatch import receiver

View File

@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-27 09:11
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.organizer
def create_teams(apps, schema_editor):
Event = apps.get_model('pretixbase', 'Event')
Organizer = apps.get_model('pretixbase', 'Organizer')
Team = apps.get_model('pretixbase', 'Team')
TeamInvite = apps.get_model('pretixbase', 'TeamInvite')
EventPermission = apps.get_model('pretixbase', 'EventPermission')
OrganizerPermission = apps.get_model('pretixbase', 'OrganizerPermission')
for o in Organizer.objects.prefetch_related('events'):
for e in o.events.all():
teams = {}
for p in e.user_perms.all():
pkey = (p.can_change_settings, p.can_change_items, p.can_view_orders,
p.can_change_permissions, p.can_change_orders, p.can_view_vouchers,
p.can_change_vouchers)
if pkey not in teams:
team = Team()
team.can_change_event_settings = p.can_change_settings
team.can_change_items = p.can_change_items
team.can_view_orders = p.can_view_orders
team.can_change_orders = p.can_change_orders
team.can_view_vouchers = p.can_view_vouchers
team.can_change_vouchers = p.can_change_vouchers
team.organizer = o
team.name = '{} Team {}'.format(
str(e.name), len(teams) + 1
)
team.save()
team.limit_events.add(e)
teams[pkey] = team
if p.user:
teams[pkey].members.add(p.user)
else:
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
teams = {}
for p in o.user_perms.all():
pkey = (p.can_create_events, p.can_change_permissions)
if pkey not in teams:
team = Team()
team.can_change_organizer_settings = True
team.can_create_events = p.can_create_events
team.can_change_teams = p.can_change_permissions
team.organizer = o
team.name = '{} Team {}'.format(
str(o.name), len(teams) + 1
)
team.save()
teams[pkey] = team
if p.user:
teams[pkey].members.add(p.user)
else:
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0051_auto_20170206_2027_squashed_0057_auto_20170501_2116'),
]
operations = [
migrations.CreateModel(
name='Team',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190, verbose_name='Team name')),
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
('can_create_events', models.BooleanField(default=False, verbose_name='Can create events')),
('can_change_teams', models.BooleanField(default=False, verbose_name='Can change permissions')),
('can_change_organizer_settings', models.BooleanField(default=False, verbose_name='Can change organizer settings')),
('can_change_event_settings', models.BooleanField(default=False, verbose_name='Can change event settings')),
('can_change_items', models.BooleanField(default=False, verbose_name='Can change product settings')),
('can_view_orders', models.BooleanField(default=False, verbose_name='Can view orders')),
('can_change_orders', models.BooleanField(default=False, verbose_name='Can change orders')),
('can_view_vouchers', models.BooleanField(default=False, verbose_name='Can view vouchers')),
('can_change_vouchers', models.BooleanField(default=False, verbose_name='Can change vouchers')),
('limit_events', models.ManyToManyField(to='pretixbase.Event', verbose_name='Limit to events')),
('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL, verbose_name='Team members')),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='pretixbase.Organizer')),
],
options={
'verbose_name_plural': 'Teams',
'verbose_name': 'Team',
},
),
migrations.CreateModel(
name='TeamInvite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('token', models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='pretixbase.Team')),
],
),
migrations.RunPython(
create_teams, migrations.RunPython.noop
)
]

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-29 10:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0052_team_teaminvite'),
]
operations = [
migrations.RemoveField(
model_name='eventpermission',
name='event',
),
migrations.RemoveField(
model_name='eventpermission',
name='user',
),
migrations.RemoveField(
model_name='organizerpermission',
name='organizer',
),
migrations.RemoveField(
model_name='organizerpermission',
name='user',
),
migrations.RemoveField(
model_name='event',
name='permitted',
),
migrations.RemoveField(
model_name='organizer',
name='permitted',
),
migrations.AlterField(
model_name='team',
name='can_change_teams',
field=models.BooleanField(default=False, verbose_name='Can change teams and permissions'),
),
migrations.AlterField(
model_name='team',
name='limit_events',
field=models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events'),
),
migrations.DeleteModel(
name='EventPermission',
),
migrations.DeleteModel(
name='OrganizerPermission',
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-04 07:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0058_auto_20170429_1020'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='nonce',
field=models.CharField(blank=True, max_length=190, null=True),
),
]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-10 10:27
from __future__ import unicode_literals
import i18nfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0059_checkin_nonce'),
]
operations = [
migrations.AddField(
model_name='event',
name='date_admission',
field=models.DateTimeField(blank=True, null=True, verbose_name='Admission time'),
),
migrations.AlterField(
model_name='event',
name='location',
field=i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location'),
),
]

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-21 09:42
from __future__ import unicode_literals
from django.core.cache import cache
from django.db import migrations
def rename_placeholder(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for setting in EventSettingsStore.objects.all():
if setting.key == 'mail_text_order_placed':
new_value = setting.value.replace('{paymentinfo}', '{payment_info}')
setting.value = new_value
cache.delete('hierarkey_{}_{}'.format('event', setting.object_id))
setting.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0060_auto_20170510_1027'),
]
operations = [
migrations.RunPython(rename_placeholder, migrations.RunPython.noop)
]

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -13,7 +13,7 @@ from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import CustomSMTPBackend
from pretix.base.models.base import LoggedModel
@@ -21,7 +21,6 @@ from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.daterange import daterange
from ..settings import settings_hierarkey
from .auth import User
from .organizer import Organizer
@@ -79,8 +78,6 @@ class Event(LoggedModel):
verbose_name=_("Short form"),
)
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
permitted = models.ManyToManyField(User, through='EventPermission',
related_name="events", )
currency = models.CharField(max_length=10,
verbose_name=_("Default currency"),
choices=CURRENCY_CHOICES,
@@ -88,6 +85,8 @@ class Event(LoggedModel):
date_from = models.DateTimeField(verbose_name=_("Event start time"))
date_to = models.DateTimeField(null=True, blank=True,
verbose_name=_("Event end time"))
date_admission = models.DateTimeField(null=True, blank=True,
verbose_name=_("Admission time"))
is_public = models.BooleanField(default=False,
verbose_name=_("Visible in public lists"),
help_text=_("If selected, this event may show up on the ticket system's start page "
@@ -102,7 +101,7 @@ class Event(LoggedModel):
verbose_name=_("Start of presale"),
help_text=_("No products will be sold before this date."),
)
location = I18nCharField(
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
@@ -140,7 +139,7 @@ class Event(LoggedModel):
return []
return self.plugins.split(",")
def get_date_from_display(self, tz=None) -> str:
def get_date_from_display(self, tz=None, show_times=True) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
@@ -148,7 +147,17 @@ class Event(LoggedModel):
tz = tz or pytz.timezone(self.settings.timezone)
return _date(
self.date_from.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
)
def get_time_from_display(self, tz=None) -> str:
"""
Returns a formatted string containing the start time of the event, ignoring
the ``show_times`` setting.
"""
tz = tz or pytz.timezone(self.settings.timezone)
return _date(
self.date_from.astimezone(tz), "TIME_FORMAT"
)
def get_date_to_display(self, tz=None) -> str:
@@ -307,69 +316,6 @@ def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
class EventPermission(models.Model):
"""
The relation between an Event and a User who has permissions to
access an event.
:param event: The event this permission refers to
:type event: Event
:param user: The user this permission set applies to
:type user: User
:param can_change_settings: If ``True``, the user can change all basic settings for this event.
:type can_change_settings: bool
:param can_change_items: If ``True``, the user can change and add items and related objects for this event.
:type can_change_items: bool
:param can_view_orders: If ``True``, the user can inspect details of all orders.
:type can_view_orders: bool
:param can_change_orders: If ``True``, the user can change details of orders
:type can_change_orders: bool
"""
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True)
invite_email = models.EmailField(null=True, blank=True)
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
can_change_settings = models.BooleanField(
default=True,
verbose_name=_("Can change event settings")
)
can_change_items = models.BooleanField(
default=True,
verbose_name=_("Can change product settings")
)
can_view_orders = models.BooleanField(
default=True,
verbose_name=_("Can view orders")
)
can_change_permissions = models.BooleanField(
default=True,
verbose_name=_("Can change permissions")
)
can_change_orders = models.BooleanField(
default=True,
verbose_name=_("Can change orders")
)
can_view_vouchers = models.BooleanField(
default=True,
verbose_name=_("Can view vouchers")
)
can_change_vouchers = models.BooleanField(
default=True,
verbose_name=_("Can change vouchers")
)
class Meta:
verbose_name = _("Event permission")
verbose_name_plural = _("Event permissions")
def __str__(self):
return _("%(name)s on %(object)s") % {
'name': str(self.user),
'object': str(self.event),
}
class EventLock(models.Model):
event = models.CharField(max_length=36, primary_key=True)
date = models.DateTimeField(auto_now=True)

View File

@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
@@ -66,7 +67,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'code': co.code
}),
'val': co.code,
'val': escape(co.code),
}
elif isinstance(co, Voucher):
a_text = _('Voucher {val}')
@@ -76,7 +77,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'voucher': co.id
}),
'val': co.code[:6],
'val': escape(co.code[:6]),
}
elif isinstance(co, Item):
a_text = _('Product {val}')
@@ -86,7 +87,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'item': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, Quota):
a_text = _('Quota {val}')
@@ -96,7 +97,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'quota': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
@@ -106,7 +107,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'category': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, Question):
a_text = _('Question {val}')
@@ -116,7 +117,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'question': co.id
}),
'val': co.question,
'val': escape(co.question),
}
if a_text and a_map:

View File

@@ -1,4 +1,5 @@
import copy
import json
import os
import string
from datetime import datetime
@@ -183,6 +184,10 @@ class Order(LoggedModel):
def __str__(self):
return self.full_code
@cached_property
def meta_info_data(self):
return json.loads(self.meta_info)
@property
def full_code(self):
"""

View File

@@ -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
)

View File

@@ -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.

View File

@@ -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)

View File

@@ -414,7 +414,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
'order': order.code,
'secret': order.secret
}),
'paymentinfo': str(pprov.order_pending_mail_render(order)),
'payment_info': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
@@ -512,7 +512,7 @@ class OrderChangeManager:
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = item.default_price if variation is None else variation.price
if not price:
if price is None:
raise OrderError(self.error_messages['product_invalid'])
self._totaldiff = price - position.price
self._quotadiff.update(variation.quotas.all() if variation else item.quotas.all())

View File

@@ -163,6 +163,10 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'ticket_download_nonadm': {
'default': 'True',
'type': bool
},
'last_order_modification_date': {
'default': None,
'type': datetime
@@ -187,6 +191,10 @@ DEFAULTS = {
'default': settings.MAIL_FROM,
'type': str
},
'mail_text_signature': {
'type': LazyI18nString,
'default': ""
},
'mail_text_resend_link': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
@@ -232,7 +240,7 @@ Your {event} team"""))
we successfully received your order for {event} with a total value
of {total} {currency}. Please complete your payment before {date}.
{paymentinfo}
{payment_info}
You can change your order details and view the status of your order at
{url}

View File

@@ -50,7 +50,7 @@ class EventPluginSignal(django.dispatch.Signal):
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses
return sorted(responses, key=lambda r: (receiver.__module__, receiver.__name__))
class DeprecatedSignal(django.dispatch.Signal):

View File

@@ -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>

View File

@@ -0,0 +1,13 @@
from django import template
from django.template.defaultfilters import stringfilter
from pretix.helpers.escapejson import escapejson
register = template.Library()
@register.filter("escapejson")
@stringfilter
def escapejs_filter(value):
"""Hex encodes characters for use in a application/json type script."""
return escapejson(value)

View File

@@ -1,6 +1,12 @@
import urllib.parse
import bleach
import markdown
from bleach import DEFAULT_CALLBACKS
from django import template
from django.core import signing
from django.urls import reverse
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
register = template.Library()
@@ -42,14 +48,24 @@ ALLOWED_ATTRIBUTES = {
}
def safelink_callback(attrs, new=False):
url = attrs.get((None, 'href'), '/')
if not is_safe_url(url):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'
return attrs
@register.filter
def rich_text(text: str, **kwargs):
"""
Processes markdown and cleans HTML in a text input.
"""
text = str(text)
body_md = bleach.linkify(bleach.clean(
markdown.markdown(text),
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
))
), callbacks=DEFAULT_CALLBACKS + [safelink_callback])
return mark_safe(body_md)

View File

@@ -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]

View File

@@ -33,7 +33,7 @@ def contextprocessor(request):
_js_payment_weekdays_disabled = '[]'
_nav_event = []
if hasattr(request, 'event'):
if getattr(request, 'event', None) and hasattr(request, 'organizer'):
for receiver, response in nav_event.send(request.event, request=request):
_nav_event += response
if request.event.settings.get('payment_term_weekdays'):
@@ -45,12 +45,12 @@ def contextprocessor(request):
if not hasattr(request, 'event'):
for receiver, response in nav_global.send(request, request=request):
_nav_global += response
ctx['nav_global'] = _nav_global
ctx['nav_global'] = sorted(_nav_global, key=lambda n: n['label'])
_nav_topbar = []
for receiver, response in nav_topbar.send(request, request=request):
_nav_topbar += response
ctx['nav_topbar'] = _nav_topbar
ctx['nav_topbar'] = sorted(_nav_topbar, key=lambda n: n['label'])
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')

View File

@@ -2,6 +2,7 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models import Q
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
@@ -26,7 +27,7 @@ class EventWizardFoundationForm(forms.Form):
self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"),
queryset=Organizer.objects.filter(
id__in=self.user.organizer_perms.filter(can_create_events=True).values_list('organizer', flat=True)
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
),
widget=forms.RadioSelect,
empty_label=None,
@@ -111,6 +112,16 @@ class EventWizardBasicsForm(I18nModelForm):
class EventWizardCopyForm(forms.Form):
@staticmethod
def copy_from_queryset(user):
return Event.objects.filter(
Q(organizer_id__in=user.teams.filter(
all_events=True, can_change_event_settings=True, can_change_items=True
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
can_change_event_settings=True, can_change_items=True
).values_list('limit_events__id', flat=True))
)
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
kwargs.pop('locales')
@@ -118,11 +129,7 @@ class EventWizardCopyForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['copy_from_event'] = forms.ModelChoiceField(
label=_("Copy configuration from"),
queryset=Event.objects.filter(
id__in=self.user.event_perms.filter(
can_change_items=True, can_change_settings=True
).values_list('event', flat=True)
),
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
widget=forms.RadioSelect,
empty_label=_('Do not copy'),
required=False
@@ -136,6 +143,7 @@ class EventUpdateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
self.fields['location'].widget.attrs['rows'] = '3'
class Meta:
model = Event
@@ -146,6 +154,7 @@ class EventUpdateForm(I18nModelForm):
'currency',
'date_from',
'date_to',
'date_admission',
'is_public',
'presale_start',
'presale_end',
@@ -154,6 +163,7 @@ class EventUpdateForm(I18nModelForm):
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_presale_start'}),
@@ -249,10 +259,12 @@ class EventSettingsForm(SettingsForm):
)
attendee_emails_asked = forms.BooleanField(
label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be send "
"to that email address. If you enable this option, the system will additionally ask for "
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
"only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders."),
"obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation only to the one primary email address, not to the "
"per-attendee addresses."),
required=False
)
attendee_emails_required = forms.BooleanField(
@@ -467,13 +479,22 @@ class MailSettingsForm(SettingsForm):
label=_("Sender address"),
help_text=_("Sender address for outgoing emails")
)
mail_text_signature = I18nFormField(
label=_("Signature"),
required=False,
widget=I18nTextarea,
help_text=_("This will be attached to every email. Available placeholders: {event}"),
validators=[PlaceholderValidator(['{event}'])]
)
mail_text_order_placed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}, "
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {payment_info}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{paymentinfo}',
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{payment_info}',
'{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_text_order_paid = I18nFormField(
@@ -586,11 +607,12 @@ class DisplaySettingsForm(SettingsForm):
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
]
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
help_text=_('If you provide a logo image, we will by default not show your events name and date '
'in the page header. We will show your logo with a maximal height of 120 pixels.')
@@ -624,6 +646,11 @@ class TicketSettingsForm(SettingsForm):
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
)
ticket_download_nonadm = forms.BooleanField(
label=_("Generate tickets for non-admission products"),
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
)
def prepare_fields(self):
# See clean()

View File

@@ -29,7 +29,7 @@ class GlobalSettingsForm(SettingsForm):
))
])
responses = register_global_settings.send(self)
for r, response in responses:
for r, response in sorted(responses, key=lambda r: str(r[0])):
for key, value in response.items():
# We need to be this explicit, since OrderedDict.update does not retain ordering
self.fields[key] = value

View File

@@ -2,6 +2,7 @@ import copy
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.forms import BooleanField, ModelMultipleChoiceField
from django.forms.formsets import DELETION_FIELD_NAME
from django.utils.translation import ugettext as __, ugettext_lazy as _
@@ -120,7 +121,20 @@ class ItemCreateForm(I18nModelForm):
)
def save(self, *args, **kwargs):
if self.cleaned_data.get('copy_from'):
self.instance.description = self.cleaned_data['copy_from'].description
self.instance.active = self.cleaned_data['copy_from'].active
self.instance.available_from = self.cleaned_data['copy_from'].available_from
self.instance.available_until = self.cleaned_data['copy_from'].available_until
self.instance.require_voucher = self.cleaned_data['copy_from'].require_voucher
self.instance.hide_without_voucher = self.cleaned_data['copy_from'].hide_without_voucher
self.instance.allow_cancel = self.cleaned_data['copy_from'].allow_cancel
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)
if self.cleaned_data.get('has_variations'):
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
for variation in self.cleaned_data['copy_from'].variations.all():
@@ -131,8 +145,9 @@ class ItemCreateForm(I18nModelForm):
item=instance, value=__('Standard')
)
for question in Question.objects.filter(items=self.cleaned_data.get('copy_from')):
question.items.add(instance)
if self.cleaned_data.get('copy_from'):
for question in self.cleaned_data['copy_from'].questions.all():
question.items.add(instance)
return instance

View File

@@ -1,8 +1,12 @@
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nModelForm
from pretix.base.models import Organizer
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Organizer, Team
from pretix.control.forms import ExtFileField
from pretix.multidomain.models import KnownDomain
@@ -65,3 +69,60 @@ class OrganizerUpdateForm(OrganizerForm):
instance.get_cache().clear()
return instance
class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields['limit_events'].queryset = organizer.events.all()
class Meta:
model = Team
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
'can_change_teams', 'can_change_organizer_settings',
'can_change_event_settings', 'can_change_items',
'can_view_orders', 'can_change_orders',
'can_view_vouchers', 'can_change_vouchers']
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events'
}),
}
def clean(self):
data = super().clean()
if self.instance.pk and not data['can_change_teams']:
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
can_change_teams=True, members__isnull=False
).exists():
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
'the permission to change teams and permissions.'))
return data
class OrganizerSettingsForm(SettingsForm):
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=forms.CheckboxSelectMultiple,
help_text=_('Choose all languages that your organizer homepage should be available in.')
)
organizer_homepage_text = I18nFormField(
label=_('Homepage text'),
required=False,
widget=I18nTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
organizer_logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. We will show your logo with a maximal height of 120 pixels.')
)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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 }}

View File

@@ -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 %}

View File

@@ -1,5 +1,6 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block nav %}
@@ -10,7 +11,7 @@
{% trans "Dashboard" %}
</a>
</li>
{% if request.eventperm.can_change_settings or request.eventperm.can_change_permissions %}
{% if 'can_change_event_settings' in request.eventpermset or 'can_change_permissions' in request.eventpermset %}
<li>
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-wrench fa-fw"></i>
@@ -18,7 +19,7 @@
</a>
</li>
{% endif %}
{% if request.eventperm.can_change_items %}
{% if 'can_change_items' in request.eventpermset %}
<li>
<a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}"
class="has-children">
@@ -55,7 +56,7 @@
</ul>
</li>
{% endif %}
{% if request.eventperm.can_view_orders %}
{% if 'can_view_orders' in request.eventpermset %}
<li>
<a href="{% url 'control:event.orders' organizer=request.event.organizer.slug event=request.event.slug %}"
class="has-children">
@@ -90,10 +91,16 @@
{% trans "Waiting list" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.checkins' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.checkins" %}class="active"{% endif %}>
{% trans "Check-ins" %}
</a>
</li>
</ul>
</li>
{% endif %}
{% if request.eventperm.can_view_vouchers %}
{% if 'can_view_vouchers' in request.eventpermset %}
<li>
<a href="{% url 'control:event.vouchers' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.vouchers" %}class="active"{% endif %}>
@@ -106,7 +113,9 @@
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="has-children"{% endif %}>
{% if nav.icon %}
{% if nav.icon and "." in nav.icon %}
<img src="{% static nav.icon %}" class="fa-img">
{% elif nav.icon %}
<i class="fa fa-{{ nav.icon }} fa-fw"></i>
{% endif %}
{{ nav.label }}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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" %}

View File

@@ -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">

View File

@@ -1,7 +0,0 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Help center" %}</h1>
{% block inner %}
{% endblock %}
{% endblock %}

View File

@@ -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 %}

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% load formset_tags %}
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %}
@@ -58,7 +59,7 @@
<div class="chart" id="question_chart" data-type="{{ question.type }}">
</div>
<script type="application/json" id="question-chart-data">{{ stats_json|safe }}</script>
<script type="application/json" id="question-chart-data">{{ stats_json|escapejson }}</script>
</div>
<div class="col-md-5 col-xs-12">
<table class="table table-bordered table-hover">

View File

@@ -1,12 +1,13 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% load eventsignal %}
{% block title %}{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %}
<h1>
{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}
{% if request.eventperm.can_change_items %}
{% if 'can_change_items' in request.eventpermset %}
<a href="{% url "control:event.items.quotas.edit" event=request.event.slug organizer=request.event.organizer.slug quota=quota.pk %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
@@ -20,7 +21,7 @@
<div class="chart" id="quota_chart">
</div>
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|safe }}</script>
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|escapejson }}</script>
</div>
<div class="col-md-5 col-xs-12">
<legend>{% trans "Availability calculation" %}</legend>

View File

@@ -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" %}

View File

@@ -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 %}

View File

@@ -4,17 +4,24 @@
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizer" %}</h1>
<form action="" method="post" class="form-horizontal">
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
{% if form.domain %}
{% bootstrap_field form.domain layout="horizontal" %}
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>
{% bootstrap_form_errors sform %}
{% bootstrap_field sform.locales layout="horizontal" %}
{% bootstrap_field sform.organizer_logo_image layout="horizontal" %}
{% bootstrap_field sform.organizer_homepage_text layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -38,6 +38,37 @@
<li>
{% trans "Add a new account to the app by scanning the following barcode:" %}
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
<p>
<a data-toggle="collapse" href="#no_scan">
{% trans "Can't scan the barcode?" %}
</a>
</p>
<div class="collapse" id="no_scan">
<ol class="multi-step-tutorial">
<li>
{% trans 'Use the "provide a key" option of your authenticator app.' %}
</li>
<li>
{% trans 'In "Account name", type your login name for pretix.' %}
</li>
<li>
{% trans 'In "Secret"/"Account Key", enter the following code:' %}
<div>
<code id="otp_secret" style="white-space: pre">{{ secretGrouped }}</code>
<button type="button" class="btn btn-default btn-xs btn-clipboard" data-clipboard-target="#otp_secret">
<span class="fa fa-clipboard" aria-hidden="true"></span>
{% trans "copy" %}
</button>
</div>
<div>
<small>Spaces don't matter.</small>
</div>
</li>
<li>
{% trans 'If present, make sure "Time-based"/"TOTP" and 6 digit codes are selected.' %}
</li>
</ol>
</div>
</li>
<li>
{% trans "Enter the displayed code here:" %}

View File

@@ -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" %}

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -0,0 +1,82 @@
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce
from django.views.generic import ListView
from pretix.base.models import Checkin, Item, OrderPosition
from pretix.control.permissions import EventPermissionRequiredMixin
class CheckInView(EventPermissionRequiredMixin, ListView):
model = Checkin
context_object_name = 'entries'
paginate_by = 30
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
def get_queryset(self):
qs = OrderPosition.objects.filter(order__event=self.request.event, order__status='p')
# if this setting is False, we check only items for admission
if not self.request.event.settings.ticket_download_nonadm:
qs = qs.filter(item__admission=True)
if self.request.GET.get("status", "") != "":
p = self.request.GET.get("status", "")
if p == '1':
# records with check-in record
qs = qs.filter(checkins__isnull=False)
elif p == '0':
qs = qs.filter(checkins__isnull=True)
if self.request.GET.get("user", "") != "":
u = self.request.GET.get("user", "")
qs = qs.filter(
Q(order__email__icontains=u) | Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
)
if self.request.GET.get("item", "") != "":
u = self.request.GET.get("item", "")
qs = qs.filter(item_id__in=(u,))
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.filter(position__order__event=self.request.event))
).select_related('order', 'item', 'addon_to')
if self.request.GET.get("ordering", "") != "":
p = self.request.GET.get("ordering", "")
keys_allowed = self.get_ordering_keys_mappings()
if p in keys_allowed:
mapped_field = keys_allowed[p]
if type(mapped_field) is tuple:
qs = qs.annotate(**mapped_field[1]).order_by(mapped_field[0])
else:
qs = qs.order_by(mapped_field)
return qs.distinct()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = Item.objects.filter(event=self.request.event)
ctx['filtered'] = ("status" in self.request.GET or "user" in self.request.GET or "item" in self.request.GET)
return ctx
@staticmethod
def get_ordering_keys_mappings():
return {
'code': 'order__code',
'-code': '-order__code',
'email': 'order__email',
'-email': '-order__email',
# Set nulls_first to be consistent over databases
'status': F('checkins__id').asc(nulls_first=True),
'-status': F('checkins__id').desc(nulls_last=True),
'timestamp': F('checkins__datetime').asc(nulls_first=True),
'-timestamp': F('checkins__datetime').desc(nulls_last=True),
'item': 'item__name',
'-item': '-item__name',
'name': (F('display_name').asc(nulls_first=True),
{'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
'-name': (F('display_name').desc(nulls_last=True),
{'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
}

View File

@@ -10,10 +10,11 @@ from django.shortcuts import render
from django.template.loader import get_template
from django.utils import formats
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import (
Event, Item, Order, OrderPosition, Voucher, WaitingListEntry,
Item, Order, OrderPosition, Voucher, WaitingListEntry,
)
from pretix.control.signals import (
event_dashboard_widgets, user_dashboard_widgets,
@@ -138,7 +139,7 @@ def quota_widgets(sender, **kwargs):
status, left = q.availability()
widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
text=_('{quota} left').format(quota=q.name)),
text=_('{quota} left').format(quota=escape(q.name))),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.items.quotas.show', kwargs={
@@ -168,6 +169,27 @@ def shop_state_widget(sender, **kwargs):
}]
@receiver(signal=event_dashboard_widgets)
def checkin_widget(sender, **kwargs):
size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p')
checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False)
# if this setting is False, we check only items for admission
if not sender.settings.ticket_download_nonadm:
size_qs = size_qs.filter(item__admission=True)
checked_qs = checked_qs.filter(item__admission=True)
return [{
'content': NUM_WIDGET.format(num='{}/{}'.format(checked_qs.count(), size_qs.count()), text=_('Checked in')),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.orders.checkins', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug
})
}]
@receiver(signal=event_dashboard_widgets)
def welcome_wizard_widget(sender, **kwargs):
template = get_template('pretixcontrol/event/dashboard_widget_welcome.html')
@@ -207,11 +229,12 @@ def event_index(request, organizer, event):
for r, result in event_dashboard_widgets.send(sender=request.event):
widgets.extend(result)
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders')
qs = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not request.eventperm.can_view_orders:
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders'):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
if not request.eventperm.can_view_vouchers:
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers'):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
a_qs = request.event.requiredaction_set.filter(done=False)
@@ -221,7 +244,7 @@ def event_index(request, organizer, event):
ctx = {
'widgets': rearrange(widgets),
'logs': qs[:5],
'actions': a_qs[:5] if request.eventperm.can_change_orders else [],
'actions': a_qs[:5] if can_change_orders else [],
'has_domain': has_domain
}
@@ -242,11 +265,13 @@ def event_index(request, organizer, event):
def user_event_widgets(**kwargs):
user = kwargs.pop('user')
widgets = []
events = Event.objects.filter(permitted__id__exact=user.pk).select_related("organizer").order_by('-date_from')
events = user.get_events_with_any_permission().order_by('-date_from', 'name').select_related('organizer')
for event in events:
widgets.append({
'content': '<div class="event">{event}<span class="from">{df}</span><span class="to">{dt}</span></div>'.format(
event=event.name, df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
event=escape(event.name),
df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
dt=date_format(event.date_to, 'SHORT_DATE_FORMAT') if event.date_to else ''
),
'display_size': 'small',

View File

@@ -2,14 +2,12 @@ import re
from collections import OrderedDict
from datetime import timedelta
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.files import File
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms import modelformset_factory
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils import translation
@@ -22,14 +20,12 @@ from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin
from pytz import timezone
from pretix.base.forms import I18nModelForm
from pretix.base.models import (
CachedTicket, Event, EventPermission, Item, ItemVariation, LogEntry, Order,
RequiredAction, User, Voucher,
CachedTicket, Event, Item, ItemVariation, LogEntry, Order, RequiredAction,
Voucher,
)
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import (
event_live_issues, register_payment_providers, register_ticket_outputs,
)
@@ -50,7 +46,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
model = Event
form_class = EventUpdateForm
template_name = 'pretixcontrol/event/settings.html'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
@cached_property
def object(self) -> Event:
@@ -115,7 +111,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/plugins.html'
def get_object(self, queryset=None) -> Event:
@@ -178,7 +174,7 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/payment.html'
def get_object(self, queryset=None) -> Event:
@@ -264,7 +260,7 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
model = Event
permission = 'can_change_settings'
permission = 'can_change_event_settings'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
@@ -300,7 +296,7 @@ class InvoiceSettings(EventSettingsFormView):
model = Event
form_class = InvoiceSettingsForm
template_name = 'pretixcontrol/event/invoicing.html'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
def get_success_url(self) -> str:
if 'preview' in self.request.POST:
@@ -315,7 +311,7 @@ class InvoiceSettings(EventSettingsFormView):
class InvoicePreview(EventPermissionRequiredMixin, View):
permission = 'can_change_settings'
permission = 'can_change_event_settings'
def get(self, request, *args, **kwargs):
pdf = build_preview_invoice_pdf(request.event)
@@ -328,7 +324,7 @@ class DisplaySettings(EventSettingsFormView):
model = Event
form_class = DisplaySettingsForm
template_name = 'pretixcontrol/event/display.html'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
def get_success_url(self) -> str:
return reverse('control:event.settings.display', kwargs={
@@ -364,7 +360,7 @@ class MailSettings(EventSettingsFormView):
model = Event
form_class = MailSettingsForm
template_name = 'pretixcontrol/event/mail.html'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
def get_success_url(self) -> str:
return reverse('control:event.settings.mail', kwargs={
@@ -407,7 +403,7 @@ class MailSettings(EventSettingsFormView):
class MailSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_settings'
permission = 'can_change_event_settings'
# return the origin text if key is missing in dict
class SafeDict(dict):
@@ -440,7 +436,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
def items(self):
return {
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company',
'event', 'paymentinfo', 'url', 'invoice_name'],
'event', 'payment_info', 'url', 'invoice_name'],
'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'],
'mail_text_order_free': ['event', 'url', 'invoice_name', 'invoice_company'],
'mail_text_resend_link': ['event', 'url', 'invoice_name', 'invoice_company'],
@@ -471,7 +467,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
'code': '68CYU2H6ZTP3WLK5',
'invoice_name': _('John Doe'),
'invoice_company': _('Sample Corporation'),
'paymentinfo': _('Please transfer money to this bank account: 9999-9999-9999-9999')
'payment_info': _('Please transfer money to this bank account: 9999-9999-9999-9999')
}
def generate_order_url(self, code, secret):
@@ -513,7 +509,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_settings'
permission = 'can_change_event_settings'
@cached_property
def output(self):
@@ -545,7 +541,7 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
model = Event
form_class = TicketSettingsForm
template_name = 'pretixcontrol/event/tickets.html'
permission = 'can_change_settings'
permission = 'can_change_event_settings'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
@@ -637,140 +633,12 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
return providers
class EventPermissionForm(I18nModelForm):
class Meta:
model = EventPermission
fields = (
'can_change_settings', 'can_change_items', 'can_change_permissions', 'can_view_orders',
'can_change_orders', 'can_view_vouchers', 'can_change_vouchers'
)
class EventPermissionCreateForm(EventPermissionForm):
user = forms.EmailField(required=False, label=_('User'))
class EventPermissions(EventPermissionRequiredMixin, TemplateView):
model = Event
form_class = TicketSettingsForm
template_name = 'pretixcontrol/event/permissions.html'
permission = 'can_change_permissions'
@cached_property
def formset(self):
fs = modelformset_factory(
EventPermission,
form=EventPermissionForm,
can_delete=True, can_order=False, extra=0
)
return fs(data=self.request.POST if self.request.method == "POST" else None,
prefix="formset",
queryset=EventPermission.objects.filter(event=self.request.event))
@cached_property
def add_form(self):
return EventPermissionCreateForm(data=self.request.POST if self.request.method == "POST" else None,
prefix="add")
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['add_form'] = self.add_form
return ctx
def _send_invite(self, instance):
try:
mail(
instance.invite_email,
_('pretix account invitation'),
'pretixcontrol/email/invitation.txt',
{
'user': self,
'event': self.request.event.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.invite_token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
try:
self.add_form.instance.event = self.request.event
self.add_form.instance.event_id = self.request.event.id
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
except User.DoesNotExist:
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
if EventPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
event=self.request.event).exists():
messages.error(self.request, _('This user already has been invited for this event.'))
return self.get(*args, **kwargs)
self.add_form.save()
self._send_invite(self.add_form.instance)
self.request.event.log_action(
'pretix.event.permissions.invited', user=self.request.user, data=logdata
)
else:
if EventPermission.objects.filter(user=self.add_form.instance.user,
event=self.request.event).exists():
messages.error(self.request, _('This user already has permissions for this event.'))
return self.get(*args, **kwargs)
self.add_form.save()
logdata['user'] = self.add_form.instance.user_id
self.request.event.log_action(
'pretix.event.permissions.added', user=self.request.user, data=logdata
)
for form in self.formset.forms:
if form.has_changed():
changedata = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
changedata['user'] = form.instance.user_id
self.request.event.log_action(
'pretix.event.permissions.changed', user=self.request.user, data=changedata
)
if form.instance.user_id == self.request.user.pk:
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
return self.get(*args, **kwargs)
for form in self.formset.deleted_forms:
logdata = {
k: v for k, v in form.cleaned_data.items()
}
self.request.event.log_action(
'pretix.event.permissions.deleted', user=self.request.user, data=logdata
)
self.formset.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('Your changes could not be saved.'))
return self.get(*args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:event.settings.permissions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})
class EventLive(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_settings'
permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/live.html'
def get_context_data(self, **kwargs):
@@ -801,7 +669,7 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
issues.append(_('You need to configure at least one quota to sell anything.'))
responses = event_live_issues.send(self.request.event)
for receiver, response in responses:
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
if response:
issues.append(response)
@@ -840,9 +708,9 @@ class EventLog(EventPermissionRequiredMixin, ListView):
def get_queryset(self):
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not self.request.eventperm.can_view_orders:
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders'):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
if not self.request.eventperm.can_view_vouchers:
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers'):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
if self.request.GET.get('user') == 'yes':
@@ -856,7 +724,7 @@ class EventLog(EventPermissionRequiredMixin, ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['userlist'] = self.request.event.user_perms.select_related('user')
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
return ctx

View File

@@ -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('')

View File

@@ -4,7 +4,7 @@ from django.contrib import messages
from django.core.files import File
from django.core.urlresolvers import resolve, reverse
from django.db import transaction
from django.db.models import Count, F, Max, Q
from django.db.models import Count, F, Q
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
@@ -792,18 +792,6 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
if form.cleaned_data['copy_from']:
form.instance.description = form.cleaned_data['copy_from'].description
form.instance.active = form.cleaned_data['copy_from'].active
form.instance.available_from = form.cleaned_data['copy_from'].available_from
form.instance.available_until = form.cleaned_data['copy_from'].available_until
form.instance.require_voucher = form.cleaned_data['copy_from'].require_voucher
form.instance.hide_without_voucher = form.cleaned_data['copy_from'].hide_without_voucher
form.instance.allow_cancel = form.cleaned_data['copy_from'].allow_cancel
form.instance.min_per_order = form.cleaned_data['copy_from'].min_per_order
form.instance.max_per_order = form.cleaned_data['copy_from'].max_per_order
form.instance.position = (self.request.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
ret = super().form_valid(form)
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={

View File

@@ -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:

View File

@@ -674,7 +674,7 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
if not self.exporter.form.is_valid():
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(*args, **kwargs)
return self.get(request, *args, **kwargs)
cf = CachedFile()
cf.date = now()

View File

@@ -1,18 +1,22 @@
from django import forms
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms import modelformset_factory
from django.shortcuts import redirect
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from django.views.generic import (
CreateView, DeleteView, DetailView, ListView, UpdateView,
)
from pretix.base.forms import I18nModelForm
from pretix.base.models import Organizer, OrganizerPermission, User
from pretix.base.models import Organizer, Team, TeamInvite, User
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
from pretix.control.forms.organizer import (
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
)
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import nav_organizer
from pretix.helpers.urls import build_absolute_uri
@@ -28,25 +32,14 @@ class OrganizerList(ListView):
if self.request.user.is_superuser:
return Organizer.objects.all()
else:
return Organizer.objects.filter(
permitted__id__exact=self.request.user.pk
)
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
class OrganizerPermissionForm(I18nModelForm):
class Meta:
model = OrganizerPermission
fields = (
'can_create_events', 'can_change_permissions'
)
class OrganizerPermissionCreateForm(OrganizerPermissionForm):
class InviteForm(forms.Form):
user = forms.EmailField(required=False, label=_('User'))
class OrganizerDetailViewMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['nav_organizer'] = []
@@ -55,6 +48,7 @@ class OrganizerDetailViewMixin:
for recv, retv in nav_organizer.send(sender=self.request.organizer, request=self.request,
organizer=self.request.organizer):
ctx['nav_organizer'] += retv
ctx['nav_organizer'].sort(key=lambda n: n['label'])
return ctx
def get_object(self, queryset=None) -> Organizer:
@@ -82,141 +76,55 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
permission = 'can_change_permissions'
context_object_name = 'organizer'
@cached_property
def formset(self):
fs = modelformset_factory(
OrganizerPermission,
form=OrganizerPermissionForm,
can_delete=True, can_order=False, extra=0
)
return fs(
data=(
self.request.POST
if self.request.method == "POST" and 'formset-TOTAL_FORMS' in self.request.POST
else None
),
prefix="formset",
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer)
)
@cached_property
def add_form(self):
return OrganizerPermissionCreateForm(
data=(
self.request.POST
if self.request.method == "POST" and 'formset-TOTAL_FORMS' in self.request.POST
else None
),
prefix="add"
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['add_form'] = self.add_form
return ctx
def _send_invite(self, instance):
try:
mail(
instance.invite_email,
_('pretix account invitation'),
'pretixcontrol/email/invitation_organizer.txt',
{
'user': self,
'organizer': self.request.organizer.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.invite_token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
try:
self.add_form.instance.organizer = self.request.organizer
self.add_form.instance.organizer_id = self.request.organizer.id
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
except User.DoesNotExist:
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
if OrganizerPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
organizer=self.request.organizer).exists():
messages.error(self.request, _('This user already has been invited for this team.'))
return self.get(*args, **kwargs)
self.add_form.save()
self._send_invite(self.add_form.instance)
self.request.organizer.log_action(
'pretix.organizer.permissions.invited', user=self.request.user, data=logdata
)
else:
if OrganizerPermission.objects.filter(user=self.add_form.instance.user,
organizer=self.request.organizer).exists():
messages.error(self.request, _('This user already has permissions for this team.'))
return self.get(*args, **kwargs)
self.add_form.save()
logdata['user'] = self.add_form.instance.user_id
self.request.organizer.log_action(
'pretix.organizer.permissions.added', user=self.request.user, data=logdata
)
for form in self.formset.forms:
if form.has_changed():
changedata = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
changedata['user'] = form.instance.user_id
self.request.organizer.log_action(
'pretix.organizer.permissions.changed', user=self.request.user, data=changedata
)
if form.instance.user_id == self.request.user.pk:
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
return self.get(*args, **kwargs)
for form in self.formset.deleted_forms:
logdata = {
k: v for k, v in form.cleaned_data.items()
}
self.request.organizer.log_action(
'pretix.organizer.permissions.deleted', user=self.request.user, data=logdata
)
self.formset.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('Your changes could not be saved.'))
return self.get(*args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:organizer.teams', kwargs={
'organizer': self.request.organizer.slug,
})
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm
template_name = 'pretixcontrol/organizers/edit.html'
permission = None
permission = 'can_change_organizer_settings'
context_object_name = 'organizer'
def get_object(self, queryset=None) -> Organizer:
@cached_property
def object(self) -> Organizer:
return self.request.organizer
def get_object(self, queryset=None) -> Organizer:
return self.object
@cached_property
def sform(self):
return OrganizerSettingsForm(
obj=self.object,
prefix='settings',
data=self.request.POST if self.request.method == 'POST' else None,
files=self.request.FILES if self.request.method == 'POST' else None
)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform
return context
@transaction.atomic
def form_valid(self, form):
self.sform.save()
if self.sform.has_changed():
self.request.organizer.log_action(
'pretix.organizer.settings',
user=self.request.user,
data={
k: (self.sform.cleaned_data.get(k).name
if isinstance(self.sform.cleaned_data.get(k), File)
else self.sform.cleaned_data.get(k))
for k in self.sform.changed_data
}
)
if form.has_changed():
self.request.organizer.log_action(
'pretix.organizer.changed',
user=self.request.user,
data={k: form.cleaned_data.get(k) for k in form.changed_data}
)
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
@@ -231,6 +139,13 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
'organizer': self.request.organizer.slug,
})
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and self.sform.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
class OrganizerCreate(CreateView):
model = Organizer
@@ -243,14 +158,268 @@ class OrganizerCreate(CreateView):
raise PermissionDenied() # TODO
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('The new organizer has been created.'))
ret = super().form_valid(form)
OrganizerPermission.objects.create(
organizer=form.instance, user=self.request.user,
can_create_events=True
t = Team.objects.create(
organizer=form.instance, name=_('Administrators'),
all_events=True, can_create_events=True, can_change_teams=True,
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
)
t.members.add(self.request.user)
return ret
def get_success_url(self) -> str:
return reverse('control:organizers')
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Team
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'can_change_teams'
context_object_name = 'teams'
def get_queryset(self):
return self.request.organizer.teams.annotate(
memcount=Count('members', distinct=True),
eventcount=Count('limit_events', distinct=True),
invcount=Count('invites', distinct=True)
).all()
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Team
template_name = 'pretixcontrol/organizers/team_edit.html'
permission = 'can_change_teams'
form_class = TeamForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['organizer'] = self.request.organizer
return kwargs
def get_object(self, queryset=None):
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
def get_success_url(self):
return reverse('control:organizer.team', kwargs={
'organizer': self.request.organizer.slug,
'team': self.object.pk
})
def form_valid(self, form):
messages.success(self.request, _('The team has been created. You can now add members to the team.'))
form.instance.organizer = self.request.organizer
ret = super().form_valid(form)
form.instance.members.add(self.request.user)
form.instance.log_action('pretix.team.created', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
return ret
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Team
template_name = 'pretixcontrol/organizers/team_edit.html'
permission = 'can_change_teams'
context_object_name = 'team'
form_class = TeamForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['organizer'] = self.request.organizer
return kwargs
def get_object(self, queryset=None):
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
def get_success_url(self):
return reverse('control:organizer.team', kwargs={
'organizer': self.request.organizer.slug,
'team': self.object.pk
})
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.team.changed', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = Team
template_name = 'pretixcontrol/organizers/team_delete.html'
permission = 'can_change_teams'
context_object_name = 'team'
def get_object(self, queryset=None):
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
def get_success_url(self):
return reverse('control:organizer.teams', kwargs={
'organizer': self.request.organizer.slug,
})
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['possible'] = self.is_allowed()
return context
def is_allowed(self) -> bool:
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
can_change_teams=True, members__isnull=False
).exists()
@transaction.atomic
def delete(self, request, *args, **kwargs):
success_url = self.get_success_url()
self.object = self.get_object()
if self.is_allowed():
self.object.log_action('pretix.team.deleted', user=self.request.user)
self.object.delete()
messages.success(request, _('The selected team has been deleted.'))
return redirect(success_url)
else:
messages.error(request, _('The selected team cannot be deleted.'))
return redirect(success_url)
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/team_members.html'
context_object_name = 'team'
permission = 'can_change_teams'
model = Team
def get_object(self, queryset=None):
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
@cached_property
def add_form(self):
return InviteForm(data=self.request.POST if self.request.method == "POST" else None)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['add_form'] = self.add_form
return ctx
def _send_invite(self, instance):
try:
mail(
instance.email,
_('pretix account invitation'),
'pretixcontrol/email/invitation.txt',
{
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'remove-member' in request.POST:
try:
user = User.objects.get(pk=request.POST.get('remove-member'))
except User.DoesNotExist:
pass
else:
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
can_change_teams=True, members__isnull=False
).exists()
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
messages.error(self.request, _('You cannot remove the last member from this team as noone would '
'be left with the permission to change teams.'))
return redirect(self.get_success_url())
else:
self.object.members.remove(user)
self.object.log_action(
'pretix.team.member.removed', user=self.request.user, data={
'email': user.email,
'user': user.pk
}
)
messages.success(self.request, _('The member has been removed from the team.'))
return redirect(self.get_success_url())
elif 'remove-invite' in request.POST:
try:
invite = self.object.invites.get(pk=request.POST.get('remove-invite'))
except TeamInvite.DoesNotExist:
messages.error(self.request, _('Invalid invite selected.'))
return redirect(self.get_success_url())
else:
invite.delete()
self.object.log_action(
'pretix.team.invite.deleted', user=self.request.user, data={
'email': invite.email
}
)
messages.success(self.request, _('The invite has been revoked.'))
return redirect(self.get_success_url())
elif self.add_form.is_valid() and self.add_form.has_changed():
try:
user = User.objects.get(email=self.add_form.cleaned_data['user'])
except User.DoesNotExist:
if self.object.invites.filter(email=self.add_form.cleaned_data['user']).exists():
messages.error(self.request, _('This user already has been invited for this team.'))
return self.get(request, *args, **kwargs)
invite = self.object.invites.create(email=self.add_form.cleaned_data['user'])
self._send_invite(invite)
self.object.log_action(
'pretix.team.invite.created', user=self.request.user, data={
'email': self.add_form.cleaned_data['user']
}
)
messages.success(self.request, _('The new member has been invited to the team.'))
return redirect(self.get_success_url())
else:
if self.object.members.filter(pk=user.pk).exists():
messages.error(self.request, _('This user already has permissions for this team.'))
return self.get(request, *args, **kwargs)
self.object.members.add(user)
self.object.log_action(
'pretix.team.member.added', user=self.request.user,
data={
'email': user.email,
'user': user.pk,
}
)
messages.success(self.request, _('The new member has been added to the team.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('Your changes could not be saved.'))
return self.get(request, *args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:organizer.team', kwargs={
'organizer': self.request.organizer.slug,
'team': self.object.pk
})

View File

@@ -262,6 +262,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
ctx = super().get_context_data()
ctx['secret'] = base64.b32encode(self.device.bin_key).decode('utf-8')
ctx['secretGrouped'] = " ".join([ctx['secret'].lower()[(i * 4): (i + 1) * 4] for i in range(len(ctx['secret']) // 4)])
ctx['qrdata'] = 'otpauth://totp/{label}%3A%20{user}?issuer={label}&secret={secret}&digits={digits}'.format(
label=quote(settings.PRETIX_INSTANCE_NAME), user=quote(self.request.user.email),
secret=ctx['secret'],

View File

@@ -1,6 +1,6 @@
import csv
import io
from defusedcsv import csv
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import resolve, reverse

Some files were not shown because too many files have changed in this diff Show More