Compare commits

..

109 Commits

Author SHA1 Message Date
Raphael Michel
6b725a9db9 Bump version to 2.3.0 2018-12-06 10:16:55 +01:00
Raphael Michel
989ebbb444 Merge pull request #1115 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-12-05 17:13:35 +01:00
Raphael Michel
0a6efc1e0f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2890 of 2890 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2018-12-05 16:12:43 +00:00
Raphael Michel
d577a0d286 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2890 of 2890 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2018-12-05 16:11:04 +00:00
Raphael Michel
6b9b379ce2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-12-05 17:05:14 +01:00
Raphael Michel
13234b6fd5 Merge pull request #1108 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-12-05 17:04:36 +01:00
Raphael Michel
2fa0067663 Revert "Update po files"
This reverts commit 4e37fa5778.
2018-12-05 17:04:22 +01:00
Raphael Michel
4e37fa5778 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-12-05 17:02:53 +01:00
Alexander Schwartz
bfb74448b1 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (69 of 69 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de/

powered by weblate
2018-12-05 15:59:59 +00:00
arabestia
a255082b07 Translated on translate.pretix.eu (Spanish)
Currently translated at 97.3% (2797 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2018-12-05 15:59:59 +00:00
Matheus Nunes
14df35bd90 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 10.4% (300 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
bd0ba7baa5 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2874 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
9aa220b95b Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2874 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
3ed4be63fe Translated on translate.pretix.eu (German (informal))
Currently translated at 100,0% (69 of 69 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de_Informal/

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
23f4b0b62f Translated on translate.pretix.eu (German)
Currently translated at 100,0% (69 of 69 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de/

powered by weblate
2018-12-05 15:59:59 +00:00
arabestia
4b9acb64da Translated on translate.pretix.eu (Spanish)
Currently translated at 98.6% (68 of 69 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/es/

powered by weblate
2018-12-05 15:59:59 +00:00
oocf
ebba0ee0cb Translated on translate.pretix.eu (Spanish)
Currently translated at 97.3% (2796 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2018-12-05 15:59:59 +00:00
arabestia
335ce48d7e Translated on translate.pretix.eu (Spanish)
Currently translated at 97.3% (2796 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2018-12-05 15:59:59 +00:00
Alvaro Enrique Ruano
d9a0c8c523 Translated on translate.pretix.eu (Spanish)
Currently translated at 96.7% (2779 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2018-12-05 15:59:59 +00:00
Raphael Michel
a297bd1944 Show that some export parameters are optional 2018-12-05 16:51:08 +01:00
Raphael Michel
953ea26984 Add a custom text to explain usage of vouchers 2018-12-05 16:50:05 +01:00
Raphael Michel
e4f80f7660 Widget: Allow to pre-fill fields in the invoice address 2018-12-05 16:45:05 +01:00
Raphael Michel
128a185957 Improve crashed test handling 2018-12-05 16:16:50 +01:00
Raphael Michel
0bdd14b47a Re-add legacy import 2018-12-05 15:13:50 +01:00
Raphael Michel
3b84b181ad PDF editor: Move questions signal out of ticket provider 2018-12-05 14:45:07 +01:00
Raphael Michel
c9b0626324 Adjust tests to new is_public default 2018-12-04 15:41:57 +01:00
Raphael Michel
dc9a82cade Fix failing tests for previous commits 2018-12-04 15:02:16 +01:00
Raphael Michel
8266733e34 Clarify is_public and turn it on by default 2018-12-04 13:49:25 +01:00
Raphael Michel
246987955b Accept localized input for all fields with localized output 2018-12-02 19:12:21 +01:00
Raphael Michel
b93e7fcb60 Fix #1067 -- Allow to manually create partial payments 2018-12-02 18:32:16 +01:00
Raphael Michel
b1cebdbd99 Fix #582 -- Improve validation of organizer domains 2018-12-02 17:45:48 +01:00
Raphael Michel
d04047abd5 Fix #1105 -- Provide URL in order split log entry 2018-12-02 17:35:31 +01:00
Raphael Michel
efca46945a Fix #953 -- Render markdown in email text previews 2018-12-02 17:11:09 +01:00
Raphael Michel
0f9755e36f Add a warning message to products that are out of timeframe 2018-12-02 16:44:21 +01:00
Raphael Michel
478d8e4116 Add export to .xlsx for lists 2018-11-30 16:10:32 +01:00
Raphael Michel
81693e042c Introduce common base class for CSV exports 2018-11-30 15:56:29 +01:00
Raphael Michel
47b7d7b36c Add separate notification category for orders that require approval 2018-11-30 15:30:35 +01:00
Raphael Michel
ba15c34ce1 Fix #1106 -- Do not send reminders to orders placed in the last two hours 2018-11-30 15:16:12 +01:00
Raphael Michel
94f2ad9325 Highlight items that are unavailable by time 2018-11-29 10:13:43 +01:00
Raphael Michel
d8070ba8a3 Fix missing ticket attachments 2018-11-29 10:01:20 +01:00
Raphael Michel
b1019672b0 Fix file format in real expor 2018-11-27 15:52:57 +01:00
Raphael Michel
631307a4d5 Even with pdftk, use PyPDF to read page size 2018-11-27 09:22:53 +01:00
Raphael Michel
180a26ee1d Fix shredder test 2018-11-26 13:44:40 +01:00
Raphael Michel
7eab1982fe Add support for PDFTK 2018-11-26 13:43:06 +01:00
Raphael Michel
ca59237ebf Use regular asynctasks for order PDF generation 2018-11-26 13:21:25 +01:00
Raphael Michel
cc92210dc2 Retry crashed tests 2018-11-26 12:09:39 +01:00
Raphael Michel
6602afdd6c Use dedicated queue for notifications 2018-11-26 12:05:16 +01:00
Raphael Michel
c7a04bc08a Add cleanup for cached tickets 2018-11-26 12:04:25 +01:00
Raphael Michel
2cc5b7f4e8 Raise error 404 on invalid month 2018-11-26 09:20:48 +01:00
Raphael Michel
453f16af03 Merge pull request #1100 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-23 16:22:00 +01:00
Raphael Michel
0f3398ae13 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2874 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
f1b65c8695 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2874 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
2c4c89c8c2 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2874 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
4042b603b7 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2874 of 2874 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2018-11-23 15:17:00 +00:00
Alvaro Enrique Ruano
63b0288383 Translated on translate.pretix.eu (Spanish)
Currently translated at 97.5% (2792 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
7c01fee70b Fix incorrect return statement 2018-11-23 16:16:46 +01:00
Raphael Michel
8127c32ef5 More tolerant i18n deserializing 2018-11-23 16:15:23 +01:00
Raphael Michel
563decdfba Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-11-23 15:37:11 +01:00
Raphael Michel
a205b01d70 Add "paid" note on invoices if there is no open payment 2018-11-23 15:36:39 +01:00
Raphael Michel
b4290384e1 Add sales channels (#1103)
- [x] Data model
- [x] Enforce constraint
- [x] Filter order list
- [x] Set channel on created order
- [x] Products API
- [x] Order API
- [x] Tests
- [x] Filter reports
- [x] Resellers
- [ ] deploy plugins
  - [ ] posbackend
  - [ ] resellers
  - [ ] reports
- [x] Ticketlayouts
- [x] Support in pretixPOS
2018-11-23 15:35:09 +01:00
Raphael Michel
0f76779fb1 Fix involuntarily created invoices 2018-11-21 13:07:28 +01:00
Raphael Michel
f34c528cba Add passphrase to wordlist 2018-11-21 11:51:57 +01:00
Raphael Michel
cf01e04101 PayPal: Improve log display 2018-11-21 11:24:44 +01:00
Martin Gross
a3a63def55 Fix #369 -- Connect with PayPal (#1084)
* Connect with PayPal

* PayPal connect code-review fixes

* PayPal Connect: Global Env selection; Fix for payee-dict

* Fix missing PayPal Connect indicator for Endpoint

* Fix backwards compatibility
2018-11-21 11:14:33 +01:00
Raphael Michel
a3489eea04 PayPal: Properly detect pending sales 2018-11-21 11:14:03 +01:00
Raphael Michel
c6cb98c30a Add documentation on development and enterprise installs 2018-11-21 09:40:44 +01:00
Alvaro Enrique Ruano
332c58c82f Improve docker cache utilization on image construction (#1099)
To provide better use of Docker cache during image build by installing from PIP before creating the layer with the source code (the PIP installations are less probable to change than the rest of the source code, so this layer will not be recreated with all the source code changes)

This was previously discussed in #1094
2018-11-21 09:27:43 +01:00
Raphael Michel
beb0ded6dc Allow to pass user data to the widget (#1095)
- [x] Logic
- [x] Tests
- [x] Docs
- [x] find a way to integrate with tracking
2018-11-20 17:55:37 +01:00
Raphael Michel
b49b2035bd Cancel payments if the pending price of the order changes 2018-11-20 17:41:33 +01:00
Raphael Michel
106c8d373d Fix #1098 -- order search with dash in event slug 2018-11-20 14:33:41 +01:00
Raphael Michel
aee44a3284 Fix marking an overpaid order as paid manually 2018-11-20 10:39:48 +01:00
Raphael Michel
d4c1fcf838 Merge pull request #1087 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-20 10:39:26 +01:00
Alvaro Enrique Ruano
832f57c9d7 Translated on translate.pretix.eu (Spanish)
Currently translated at 97.4% (2790 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2018-11-20 09:36:17 +00:00
Mikkel Ricky
ac2a9b207d Translated on translate.pretix.eu (Danish)
Currently translated at 57.4% (1645 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2018-11-20 09:36:17 +00:00
Maarten van den Berg
f1e5d60a14 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2864 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2018-11-20 09:36:17 +00:00
Maarten van den Berg
7b1a1dc754 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.7% (2854 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2018-11-20 09:36:17 +00:00
Raphael Michel
c93f804992 Fix #1080 -- Deal with gaps in the invoice database (#1086) 2018-11-20 10:36:13 +01:00
Alvaro Enrique Ruano
1cba4b1d45 Dockerfile improvements for better readability (#1094) 2018-11-20 10:24:53 +01:00
Raphael Michel
22369a5559 Fix CI problems with SQLite (#1076)
* Try to re-enable tests on Py3.5

* Add newer SQLite3
2018-11-19 17:57:50 +01:00
Raphael Michel
a8223ad354 Fix missing return statement 2018-11-19 17:12:48 +01:00
Raphael Michel
c9d3cf7cac Fix exceptions in previous commit 2018-11-19 11:03:54 +01:00
Raphael Michel
bbdbc94f6e Redirect case-insensitive versions of event/organizer slugs 2018-11-19 10:22:40 +01:00
Raphael Michel
5c8d9c4dca Fix incorrect feedback on invite form 2018-11-16 14:13:44 +01:00
Raphael Michel
546ff6e42f Variations: Show a price range on the front page 2018-11-14 15:43:21 +01:00
Raphael Michel
7b7d45ce2e Fix Dutch date format 2018-11-14 15:43:15 +01:00
Raphael Michel
be3ca7c561 Sort cart positions by reasonable values 2018-11-14 15:36:50 +01:00
Raphael Michel
abdb6e2d52 Add "Event date" to PDF editors 2018-11-14 14:39:20 +01:00
Raphael Michel
138ddcdcd7 CSV-export improvements (include voucher, allow semicolon) 2018-11-14 10:02:28 +01:00
Raphael Michel
8ffc6550da Do not allow orders with unavailable items to be completed 2018-11-13 17:55:56 +01:00
Raphael Michel
0734715bab Only warn about bad-contrasat colors 2018-11-13 15:53:18 +01:00
Raphael Michel
7528bfb10b Fix tests for color saving 2018-11-13 14:15:52 +01:00
Raphael Michel
2798fb3468 Avoid setting name_parts to None 2018-11-13 12:59:12 +01:00
Raphael Michel
4e6f4716ec Allow to configure accent colors 2018-11-13 12:56:52 +01:00
Raphael Michel
e523a4e610 Allow to manually generate invoices like in c131ad8c 2018-11-12 13:08:12 +01:00
Raphael Michel
31cec76809 Generate invoice after expired order is extended 2018-11-12 13:08:12 +01:00
Raphael Michel
fdfd9f9275 Fix cart cleanup 2018-11-12 13:08:12 +01:00
oocf
b658c73c19 Removed permissions page from events settings (#1063)
* Removed old configuration for events settings

* Urls file to remove permissions file

* Removing not needed test

* Removing test in permissions
2018-11-12 12:23:40 +01:00
Raphael Michel
ebd3e6f31a Fix TypeError in typeahead 2018-11-12 12:17:49 +01:00
Raphael Michel
ccec114653 Merge pull request #1085 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-12 11:41:06 +01:00
Raphael Michel
f0716dc482 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2864 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2018-11-12 10:40:47 +00:00
Raphael Michel
513778b2c4 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2864 of 2864 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2018-11-12 10:34:26 +00:00
Raphael Michel
742e403ae2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-11-12 11:31:32 +01:00
Raphael Michel
09a9d610f8 Make navigation structure more approachable to new users (#1083)
* Move event selector to sidebar

* Unify navigation

* Fix confusing icons
2018-11-12 11:30:36 +01:00
Raphael Michel
b9534f23f5 Delete add-ons explicitly 2018-11-12 11:11:33 +01:00
Raphael Michel
b053f61001 Delete add-on positions explicitly 2018-11-12 11:11:33 +01:00
Felix Rindt
21042f2111 Fix #1071 -- Make payments and invoice address full width panels (#1072)
Solve #1071.

I'm not happy about how the invoice address panel is really wide now.
2018-11-12 10:38:22 +01:00
Raphael Michel
e953474138 Fix a few models.CASCADE 2018-11-11 16:23:37 +01:00
Tobias Kunze
0d438ad07c Remove outline when clicking on checkout progress (#1082) 2018-11-10 14:48:37 +01:00
Raphael Michel
e285b7cff0 Bump version to release 2.3.0.dev0 2018-11-09 16:45:43 +01:00
187 changed files with 26555 additions and 22077 deletions

View File

@@ -22,6 +22,8 @@ matrix:
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=plugins
- python: 3.6
@@ -36,6 +38,9 @@ addons:
- enchant
- myspell-de-de
- aspell-en
- sqlite3
sources:
- travis-ci/sqlite3
branches:
except:
- /^weblate-.*/

View File

@@ -1,10 +1,26 @@
FROM python:3.6
RUN apt-get update && \
apt-get install -y git libxml2-dev libxslt1-dev python-dev python-virtualenv locales \
libffi-dev build-essential python3-dev zlib1g-dev libssl-dev gettext libpq-dev \
default-libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
--no-install-recommends && \
apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
gettext \
git \
libffi-dev \
libjpeg-dev \
libmemcached-dev \
libpq-dev \
libssl-dev \
libxml2-dev \
libxslt1-dev \
locales \
nginx \
python-dev \
python-virtualenv \
python3-dev \
sudo \
supervisor \
zlib1g-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
dpkg-reconfigure locales && \
@@ -19,6 +35,22 @@ RUN apt-get update && \
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/requirements /pretix/src/requirements
COPY src/requirements.txt /pretix/src
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
pip3 install \
-r requirements.txt \
-r requirements/memcached.txt \
-r requirements/mysql.txt \
-r requirements/redis.txt \
gunicorn && \
rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord.conf /etc/supervisord.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
@@ -27,11 +59,8 @@ COPY src /pretix/src
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
pip3 install -U pip wheel setuptools && \
cd /pretix/src && \
rm -f pretix.cfg && \
pip3 install -r requirements.txt -r requirements/mysql.txt \
-r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \
sudo -u pretixuser make production

View File

@@ -295,5 +295,13 @@ various places like order codes, secrets in the ticket QR codes, etc. Example::
; Voucher code needs to be < 255 characters, default is 16
voucher_code=16
External tools
--------------
pretix can make use of some external tools if they are installed. Currently, they are all optional. Example::
[tools]
pdftk=/usr/bin/pdftk
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html

View File

@@ -0,0 +1,37 @@
.. highlight:: none
Installing a development version
================================
If you want to use a feature of pretix that is not yet contained in the last monthly release, you can also
install a development version with pretix.
.. warning:: When in production, we strongly recommend only installing released versions. Development versions might
be broken, incompatible to plugins, or in rare cases incompatible to upgrade later on.
Manual installation
-------------------
You can use ``pip`` to update pretix directly to the development branch. Then, upgrade as usual::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix&subdirectory=src"
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Docker installation
-------------------
To use the latest development version with Docker, first pull it from Docker Hub::
$ docker pull pretix/standalone:latest
Then change your ``/etc/systemd/system/pretix.service`` file to use the ``:latest`` tag instead of ``:stable`` as well
and upgrade as usual::
$ systemctl restart pretix.service
$ docker exec -it pretix.service pretix upgrade

View File

@@ -0,0 +1,84 @@
.. highlight:: none
Installing pretix Enterprise plugins
====================================
If you want to use a feature of pretix that is part of our commercial offering pretix Enterprise, you need to follow
some extra steps. Installation works similar to normal pretix plugins, but involves a few extra steps.
Buying the license
------------------
To obtain a license, please get in touch at sales@pretix.eu. Please let us know how many tickets you roughly intend
to sell per year and how many servers you want to use the plugin on. We recommend having a look at our `price list`_
first.
Manual installation
-------------------
First, generate an SSH key for the system user that you install pretix as. In our tutorial, that would be the user
``pretix``. Choose an empty passphrase::
# su pretix
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/var/pretix/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /var/pretix/.ssh/id_rsa.
Your public key has been saved in /var/pretix/.ssh/id_rsa.pub.
Next, send the content of the *public* key to your sales representative at pretix::
$ cat /var/pretix/.ssh/id_rsa.pub
ssh-rsa AAAAB3N...744HZawHlD pretix@foo
After we configured your key in our system, you can install the plugin directly using ``pip`` from the URL we told
you, for example::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack"
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
# systemctl restart pretix-web pretix-worker
Docker installation
-------------------
To install a plugin, you need to build your own docker image. To do so, create a new directory to work in. As a first
step, generate a new SSH key in that directory to use for authentication with us::
$ cd /home/me/mypretixdocker
$ ssh-keygen -N "" -f id_pretix_enterprise
Next, send the content of the *public* key to your sales representative at pretix::
$ cat id_pretix_enterprise.pub
ssh-rsa AAAAB3N...744HZawHlD pretix@foo
After we configured your key in our system, you can add a ``Dockerfile`` in your directory that includes the newly
generated key and installs the plugin from the URL we told you::
FROM pretix/standalone:stable
USER root
COPY id_pretix_enterprise /root/.ssh/id_rsa
COPY id_pretix_enterprise.pub /root/.ssh/id_rsa.pub
RUN chmod -R 0600 /root/.ssh && \
mkdir -p /etc/ssh && \
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
pip3 install -Ue "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
cd /pretix/src && \
sudo -u pretixuser make production
USER pretixuser
Then, build the image for docker::
$ docker build -t mypretix
You can now use that image ``mypretix`` instead of ``pretix/standalone:stable`` in your ``/etc/systemd/system/pretix.service``
service file. Be sure to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an
update to a new version of pretix.
.. _price list: https://pretix.eu/about/en/pricing

View File

@@ -10,3 +10,5 @@ for your needs.
general
docker_smallscale
manual_smallscale
dev_version
enterprise

View File

@@ -37,6 +37,8 @@ admission boolean ``True`` for it
position integer An integer, used for sorting
picture string A product picture to be displayed in the shop
(read-only).
sales_channels list of strings Sales channels this product is available on, such as
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_until datetime The last date time at which this item can be bought
@@ -105,6 +107,10 @@ addons list of objects Definition of a
The field ``require_approval`` has been added.
.. versionchanged:: 2.3
The ``sales_channels`` attribute has been added.
Notes
-----
Please note that an item either always has variations or never has. Once created with variations the item can never
@@ -147,6 +153,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -232,6 +239,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -298,6 +306,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -351,6 +360,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -436,6 +446,7 @@ Endpoints
"id": 1,
"name": {"en": "Ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "25.00",
"original_price": null,
"category": null,

View File

@@ -30,6 +30,8 @@ status string Order status, o
secret string The secret contained in the link sent to the customer
email string The customer email address
locale string The locale used for communication with this customer
sales_channel string Channel this sale was created through, such as
``"web"``.
datetime datetime Time of order creation
expires datetime The order will expire, if it is still pending by this time
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
@@ -121,6 +123,10 @@ last_modified datetime Last modificati
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
.. versionchanged:: 2.3
The ``sales_channel`` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -265,6 +271,7 @@ List of all orders
"secret": "k24fiuwvu8kxz3y1",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z",
"expires": "2017-12-10T10:00:00Z",
"last_modified": "2017-12-01T10:00:00Z",
@@ -401,6 +408,7 @@ Fetching individual orders
"secret": "k24fiuwvu8kxz3y1",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z",
"expires": "2017-12-10T10:00:00Z",
"last_modified": "2017-12-01T10:00:00Z",
@@ -561,6 +569,8 @@ Creating orders
* does not validate if products are only to be sold in a specific time frame
* does not validate if products are only to be sold on other sales channels
* does not validate if the event's ticket sales are already over or haven't started
* does not validate the number of items per order or the number of times an item can be included in an order
@@ -597,6 +607,7 @@ Creating orders
creation.
* ``email``
* ``locale``
* ``sales_channel``
* ``payment_provider`` The identifier of the payment provider set for this order. This needs to be an existing
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
orders you create as paid.
@@ -661,6 +672,7 @@ Creating orders
{
"email": "dummy@example.org",
"locale": "en",
"sales_channel": "web",
"fees": [
{
"fee_type": "payment",

View File

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

View File

@@ -20,6 +20,7 @@ default boolean ``true`` if thi
layout object Layout specification for libpretixprint
background URL Background PDF file
item_assignments list of objects Products this layout is assigned to
├ sales_channel string Sales channel (defaults to ``web``).
└ item integer Item ID
===================================== ========================== =======================================================
@@ -27,6 +28,10 @@ item_assignments list of objects Products this l
This resource has been added.
.. versionchanged:: 2.3
The ``item_assignments.sales_channel`` field has been added.
Endpoints
---------

View File

@@ -1,5 +1,6 @@
addon
addons
Analytics
anonymize
api
auditability
@@ -65,6 +66,7 @@ ons
optimizations
overpayment
param
passphrase
percental
positionid
pre

View File

@@ -149,8 +149,101 @@ Just as the widget, the button supports the optional attributes ``voucher`` and
You can style the button using the ``pretix-button`` CSS class.
.. versionchanged:: 1.13
Dynamically loading the widget
------------------------------
The pretix Button has been added in version 1.13.
If you need to control the way or timing the widget loads, for example because you want to modify user data (see
below) dynamically via JavaScript, you can register a listener that we will call before creating the widget::
<script type="text/javascript">
window.pretixWidgetCallback = function () {
// Will be run before we create the widget.
}
</script>
If you want, you can suppress us loading the widget and/or modify the user data passed to the widget::
<script type="text/javascript">
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.PretixWidget.widget_data["email"] = "test@example.org";
}
</script>
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
Passing user data to the widget
-------------------------------
If you display the widget in a restricted area of your website and you want to pre-fill fields in the checkout process
with known user data to save your users some typing and increase conversions, you can pass additional data attributes
with that information::
<pretix-widget event="https://pretix.eu/demo/democon/"
data-attendee-name-given-name="John"
data-attendee-name-family-name="Doe"
data-invoice-address-name-given-name="John"
data-invoice-address-name-family-name="Doe"
data-email="test@example.org"
data-question-L9G8NG9M="Foobar">
</pretix-widget>
This works for the pretix Button as well. Currently, the following attributes are understood by pretix itself:
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
* ``data-question-IDENTIFIER`` will pre-fill the answer for the question with the given identifier. You can view and set
identifiers in the *Questions* section of the backend.
* Depending on the person name scheme configured in your event settings, you can pass one or more of
``data-attendee-name-full-name``, ``data-attendee-name-given-name``, ``data-attendee-name-family-name``,
``data-attendee-name-middle-name``, ``data-attendee-name-title``, ``data-attendee-name-calling-name``,
``data-attendee-name-latin-transcription``. If you don't know or don't care, you can also just pass a string as
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
country code.
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
made through this widget will be counted towards this campaign.
* If you use the tracking plugin, you can pass a Google Analytics User ID to enable cross-domain tracking. This will
require you to dynamically load the widget, like this::
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXXX-1', 'auto');
ga('send', 'pageview');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if(window.ga && ga.create) {
ga(function(tracker) {
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
window.PretixWidget.buildWidgets()
});
} else { // Tracking is probably blocked
window.PretixWidget.buildWidgets()
}
});
};
</script>
.. versionchanged:: 2.3
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
fully if you configured a redis server.
.. _Let's Encrypt: https://letsencrypt.org/

View File

@@ -1 +1 @@
__version__ = "2.2.0"
__version__ = "2.3.0"

View File

@@ -15,13 +15,20 @@ class I18nField(Field):
super().__init__(**kwargs)
def to_representation(self, value):
if value is None or value.data is None:
if hasattr(value, 'data'):
if isinstance(value.data, dict):
return value.data
elif value.data is None:
return None
else:
return {
settings.LANGUAGE_CODE: str(value.data)
}
elif value is None:
return None
if isinstance(value.data, dict):
return value.data
else:
return {
settings.LANGUAGE_CODE: str(value.data)
settings.LANGUAGE_CODE: str(value)
}
def to_internal_value(self, data):

View File

@@ -74,7 +74,7 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'description',
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',

View File

@@ -11,6 +11,7 @@ from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
@@ -232,7 +233,7 @@ class OrderSerializer(I18nAwareModelSerializer):
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval')
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -412,7 +413,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
def validate_payment_provider(self, pp):
@@ -420,6 +421,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_sales_channel(self, channel):
if channel not in get_all_sales_channels():
raise ValidationError('Unknown sales channel.')
return channel
def validate_code(self, code):
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
raise ValidationError(

View File

@@ -25,8 +25,8 @@ from pretix.api.serializers.order import (
OrderRefundSerializer, OrderSerializer,
)
from pretix.base.models import (
Device, Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
TeamAPIToken,
CachedCombinedTicket, CachedTicket, Device, Invoice, Order, OrderPayment,
OrderPosition, OrderRefund, Quota, TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
@@ -38,9 +38,7 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.services.tickets import generate
from pretix.base.signals import order_placed, register_ticket_outputs
@@ -130,9 +128,11 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
if order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
ct = get_cachedticket_for_order(order, provider.identifier)
if not ct.file:
ct = CachedCombinedTicket.objects.filter(
order=order, provider=provider.identifier, file__isnull=False
).last()
if not ct or not ct.file:
generate.apply_async(args=('order', order.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
@@ -447,9 +447,11 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
if not pos.item.admission and not request.event.settings.ticket_download_nonadm:
raise PermissionDenied("Downloads are not enabled for non-admission products.")
ct = get_cachedticket_for_position(pos, provider.identifier)
if not ct.file:
ct = CachedTicket.objects.filter(
order_position=pos, provider=provider.identifier, file__isnull=False
).last()
if not ct or not ct.file:
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)

View File

@@ -102,6 +102,7 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
return d
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
@@ -111,6 +112,10 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.placed',
_('New order placed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.placed.required_approval',
_('New order requires approval'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.paid',
_('Order marked as paid'),

View File

@@ -0,0 +1,66 @@
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.signals import register_sales_channels
logger = logging.getLogger(__name__)
_ALL_CHANNELS = None
class SalesChannel:
def __repr__(self):
return '<SalesChannel: {}>'.format(self.identifier)
@property
def identifier(self) -> str:
"""
The internal identifier of this sales channel.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this sales channel.
"""
raise NotImplementedError() # NOQA
@property
def icon(self) -> str:
"""
The name of a Font Awesome icon to represent this channel
"""
return "circle"
def get_all_sales_channels():
global _ALL_CHANNELS
if _ALL_CHANNELS:
return _ALL_CHANNELS
types = OrderedDict()
for recv, ret in register_sales_channels.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
else:
types[ret.identifier] = ret
_ALL_CHANNELS = types
return types
class WebshopSalesChannel(SalesChannel):
identifier = "web"
verbose_name = _('Online shop')
icon = "globe"
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
def base_sales_channels(sender, **kwargs):
return (
WebshopSalesChannel(),
)

View File

@@ -1,5 +1,14 @@
import io
import tempfile
from collections import OrderedDict
from typing import Tuple
from defusedcsv import csv
from django import forms
from django.utils.translation import ugettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
class BaseExporter:
"""
@@ -55,7 +64,7 @@ class BaseExporter:
"""
return {}
def render(self, form_data: dict) -> Tuple[str, str, str]:
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
"""
Render the exported file and return a tuple consisting of a filename, a file type
and file content.
@@ -69,3 +78,68 @@ class BaseExporter:
tasks.
"""
raise NotImplementedError() # NOQA
class ListExporter(BaseExporter):
@property
def export_form_fields(self) -> dict:
ff = OrderedDict(
[
('_format',
forms.ChoiceField(
label=_('Export format'),
choices=(
('xlsx', _('Excel (.xlsx)')),
('default', _('CSV (with commas)')),
('excel', _('CSV (Excel-style)')),
('semicolon', _('CSV (with semicolons)')),
),
)),
]
)
ff.update(self.additional_form_fields)
return ff
@property
def additional_form_fields(self) -> dict:
return {}
def iterate_list(self, form_data):
raise NotImplementedError() # noqa
def get_filename(self):
return 'export.csv'
def _render_csv(self, form_data, **kwargs):
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_list(form_data):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data):
wb = Workbook()
ws = wb.get_active_sheet()
try:
ws.title = str(self.verbose_name)
except:
pass
for i, line in enumerate(self.iterate_list(form_data)):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
if form_data.get('_format') == 'xlsx':
return self._render_xlsx(form_data)
elif form_data.get('_format') == 'default':
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
elif form_data.get('_format') == 'csv-excel':
return self._render_csv(form_data, dialect='excel')
elif form_data.get('_format') == 'semicolon':
return self._render_csv(form_data, dialect='excel', delimiter=';')

View File

@@ -1,9 +1,7 @@
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 DateTimeField, Max, OuterRef, Subquery, Sum
from django.dispatch import receiver
@@ -14,16 +12,16 @@ from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import BaseExporter
from ..exporter import ListExporter
from ..signals import register_data_exporters
class OrderListExporter(BaseExporter):
identifier = 'orderlistcsv'
verbose_name = ugettext_lazy('List of orders (CSV)')
class OrderListExporter(ListExporter):
identifier = 'orderlist'
verbose_name = ugettext_lazy('List of orders')
@property
def export_form_fields(self):
def additional_form_fields(self):
return OrderedDict(
[
('paid_only',
@@ -51,10 +49,8 @@ class OrderListExporter(BaseExporter):
tax_rates = sorted(tax_rates)
return tax_rates
def render(self, form_data: dict):
output = io.StringIO()
def iterate_list(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
@@ -95,7 +91,7 @@ class OrderListExporter(BaseExporter):
headers.append(_('Invoice numbers'))
writer.writerow(headers)
yield headers
full_fee_sum_cache = {
o['order__id']: o['grosssum'] for o in
@@ -162,17 +158,18 @@ class OrderListExporter(BaseExporter):
]
row.append(', '.join([i.number for i in order.invoices.all()]))
writer.writerow(row)
yield row
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
def get_filename(self):
return '{}_orders'.format(self.event.slug)
class PaymentListExporter(BaseExporter):
identifier = 'paymentlistcsv'
verbose_name = ugettext_lazy('List of payments and refunds (CSV)')
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = ugettext_lazy('List of payments and refunds')
@property
def export_form_fields(self):
def additional_form_fields(self):
return OrderedDict(
[
('successful_only',
@@ -184,10 +181,8 @@ class PaymentListExporter(BaseExporter):
]
)
def render(self, form_data: dict):
output = io.StringIO()
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
provider_names = {
k: v.verbose_name
@@ -215,7 +210,7 @@ class PaymentListExporter(BaseExporter):
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Amount'), _('Payment method')
]
writer.writerow(headers)
yield headers
for obj in objs:
if isinstance(obj, OrderPayment) and obj.payment_date:
@@ -233,24 +228,22 @@ class PaymentListExporter(BaseExporter):
localize(obj.amount * (-1 if isinstance(obj, OrderRefund) else 1)),
provider_names.get(obj.provider, obj.provider)
]
writer.writerow(row)
yield row
return '{}_payments.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
def get_filename(self):
return '{}_payments'.format(self.event.slug)
class QuotaListExporter(BaseExporter):
identifier = 'quotalistcsv'
verbose_name = ugettext_lazy('Quota availabilities (CSV)')
def render(self, form_data: dict):
output = io.StringIO()
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
class QuotaListExporter(ListExporter):
identifier = 'quotalist'
verbose_name = ugettext_lazy('Quota availabilities')
def iterate_list(self, form_data):
headers = [
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
_('Current user\'s carts'), _('Waiting list'), _('Current availability')
]
writer.writerow(headers)
yield headers
for quota in self.event.quotas.all():
avail = quota.availability()
@@ -264,9 +257,10 @@ class QuotaListExporter(BaseExporter):
quota.count_waiting_list_pending(),
_('Infinite') if avail[1] is None else avail[1]
]
writer.writerow(row)
yield row
return '{}_quotas.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
def get_filename(self):
return '{}_quotas'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")

View File

@@ -0,0 +1,29 @@
# Generated by Django 2.1.1 on 2018-11-21 12:24
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0102_auto_20181017_0024'),
]
operations = [
migrations.AddField(
model_name='item',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=['web'], verbose_name='Sales channels'),
preserve_default=False,
),
migrations.AddField(
model_name='order',
name='sales_channel',
field=models.CharField(default='web', max_length=190),
preserve_default=False,
),
]

View File

@@ -227,10 +227,9 @@ class Event(EventMixin, LoggedModel):
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 "
"or an organization profile."))
is_public = models.BooleanField(default=True,
verbose_name=_("Show in lists"),
help_text=_("If selected, this event will show up publicly on the list of events for your organizer account."))
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),

View File

@@ -0,0 +1,94 @@
from django.core import exceptions
from django.db.models import TextField, lookups as builtin_lookups
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
DELIMITER = "\x1F"
class MultiStringField(TextField):
default_error_messages = {
'delimiter_found': _('No value can contain the delimiter character.')
}
def __init__(self, verbose_name=None, name=None, **kwargs):
super().__init__(verbose_name, name, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
return name, path, args, kwargs
def to_python(self, value):
if isinstance(value, (list, tuple)):
return value
elif value:
return [v for v in value.split(DELIMITER) if v]
else:
return []
def get_prep_value(self, value):
if isinstance(value, (list, tuple)):
return DELIMITER + DELIMITER.join(value) + DELIMITER
elif value is None:
return ""
raise TypeError("Invalid data type passed.")
def get_prep_lookup(self, lookup_type, value): # NOQA
raise TypeError('Lookups on multi strings are currently not supported.')
def from_db_value(self, value, expression, connection, context):
if value:
return [v for v in value.split(DELIMITER) if v]
else:
return []
def validate(self, value, model_instance):
super().validate(value, model_instance)
for l in value:
if DELIMITER in l:
raise exceptions.ValidationError(
self.error_messages['delimiter_found'],
code='delimiter_found',
)
def get_lookup(self, lookup_name):
if lookup_name == 'contains':
return MultiStringContains
elif lookup_name == 'icontains':
return MultiStringIContains
raise NotImplementedError(
"Lookup '{}' doesn't work with MultiStringField".format(lookup_name),
)
class MultiStringContains(builtin_lookups.Contains):
def process_rhs(self, qn, connection):
sql, params = super().process_rhs(qn, connection)
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
return sql, params
class MultiStringIContains(builtin_lookups.IContains):
def process_rhs(self, qn, connection):
sql, params = super().process_rhs(qn, connection)
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
return sql, params
class MultiStringSerializer(serializers.Field):
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
super().__init__(**kwargs)
def to_representation(self, value):
return value
def to_internal_value(self, data):
if isinstance(data, list):
return data
else:
raise ValidationError('Invalid data type.')
serializers.ModelSerializer.serializer_field_mapping[MultiStringField] = MultiStringSerializer

View File

@@ -2,6 +2,8 @@ import string
from decimal import Decimal
from django.db import DatabaseError, models, transaction
from django.db.models import Max
from django.db.models.functions import Cast
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -124,8 +126,12 @@ class Invoice(models.Model):
numeric_invoices = Invoice.objects.filter(
event__organizer=self.event.organizer,
prefix=self.prefix,
).exclude(invoice_no__contains='-')
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
).exclude(invoice_no__contains='-').annotate(
numeric_number=Cast('invoice_no', models.IntegerField())
).aggregate(
max=Max('numeric_number')
)['max'] or 0
return self._to_numeric_invoice_number(numeric_invoices + 1)
def _get_invoice_number_from_order(self):
return '{order}-{count}'.format(
@@ -183,7 +189,7 @@ class Invoice(models.Model):
class Meta:
unique_together = ('organizer', 'prefix', 'invoice_no')
ordering = ('invoice_no',)
ordering = ('date', 'invoice_no',)
class InvoiceLine(models.Model):

View File

@@ -17,6 +17,7 @@ from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.tax import TaxedPrice
@@ -195,6 +196,8 @@ class Item(LoggedModel):
:type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
:type require_approval: bool
:param sales_channels: Sales channels this item is available on.
:type sales_channels: bool
"""
event = models.ForeignKey(
@@ -329,6 +332,10 @@ class Item(LoggedModel):
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=['web']
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.
@@ -357,17 +364,21 @@ class Item(LoggedModel):
rate=Decimal('0.00'), name='')
return self.tax_rule.tax(price, base_price_is=base_price_is)
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
return False
return True
def is_available(self, now_dt: datetime=None) -> bool:
"""
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or now()
if not self.active:
return False
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
@@ -750,7 +761,7 @@ class Question(LoggedModel):
@staticmethod
def _clean_identifier(event, code, instance=None):
qs = Question.objects.filter(event=event, identifier=code)
qs = Question.objects.filter(event=event, identifier__iexact=code)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists():

View File

@@ -94,6 +94,8 @@ class Order(LockModel, LoggedModel):
:type require_approval: bool
:param meta_info: Additional meta information on the order, JSON-encoded.
:type meta_info: str
:param sales_channel: Identifier of the sales channel this order was created through.
:type sales_channel: str
"""
STATUS_PENDING = "n"
@@ -174,6 +176,7 @@ class Order(LockModel, LoggedModel):
require_approval = models.BooleanField(
default=False
)
sales_channel = models.CharField(max_length=190, default="web")
class Meta:
verbose_name = _("Order")
@@ -555,7 +558,8 @@ class Order(LockModel, LoggedModel):
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else []
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
}
)
@@ -715,7 +719,7 @@ class AbstractPosition(models.Model):
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
on_delete=models.PROTECT,
verbose_name=pgettext_lazy("subevent", "Date"),
)
item = models.ForeignKey(
@@ -748,10 +752,10 @@ class AbstractPosition(models.Model):
help_text=_("Empty, if this product is not an admission ticket")
)
voucher = models.ForeignKey(
'Voucher', null=True, blank=True, on_delete=models.CASCADE
'Voucher', null=True, blank=True, on_delete=models.PROTECT
)
addon_to = models.ForeignKey(
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='addons'
'self', null=True, blank=True, on_delete=models.PROTECT, related_name='addons'
)
meta_info = models.TextField(
verbose_name=_("Meta information"),
@@ -1449,6 +1453,7 @@ class OrderPosition(AbstractPosition):
# Delete afterwards. Deleting in between might cause deletion of things related to add-ons
# due to the deletion cascade.
for cartpos in cp:
cartpos.addons.all().delete()
cartpos.delete()
return ops
@@ -1600,6 +1605,7 @@ class InvoiceAddress(models.Model):
self.name_cached = self.name
else:
self.name_cached = ""
self.name_parts = {}
super().save(**kwargs)
@property

View File

@@ -193,6 +193,12 @@ def register_default_notification_types(sender, **kwargs):
_('New order placed'),
_('A new order has been placed: {order.code}'),
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.placed.require_approval',
_('New order requires approval'),
_('A new order has been placed that requires approval: {order.code}'),
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.paid',

View File

@@ -1,12 +1,18 @@
import copy
import logging
import os
import re
import subprocess
import tempfile
import uuid
from collections import OrderedDict
from functools import partial
from io import BytesIO
import bleach
from django.conf import settings
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from PyPDF2 import PdfFileReader
@@ -25,7 +31,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition
from pretix.base.models import Order, OrderPosition, QuestionAnswer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -118,6 +124,14 @@ DEFAULT_VARIABLES = OrderedDict((
"SHORT_DATETIME_FORMAT"
) if ev.date_to else ""
}),
("event_end_date", {
"label": _("Event end date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT"
) if ev.date_to else ""
}),
("event_end_time", {
"label": _("Event end time"),
"editor_sample": _("22:00"),
@@ -181,6 +195,30 @@ DEFAULT_VARIABLES = OrderedDict((
))
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
def variables_from_questions(sender, *args, **kwargs):
def get_answer(op, order, event, question_id):
try:
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
a = [a for a in op.answers.all() if a.question_id == question_id][0]
else:
a = op.answers.get(question_id=question_id)
return str(a).replace("\n", "<br/>\n")
except QuestionAnswer.DoesNotExist:
return ""
except IndexError:
return ""
d = {}
for q in sender.questions.all():
d['question_{}'.format(q.pk)] = {
'label': _('Question: {question}').format(question=q.question),
'editor_sample': _('<Answer: {question}>').format(question=q.question),
'evaluate': partial(get_answer, question_id=q.pk)
}
return d
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
@@ -215,8 +253,10 @@ class Renderer:
self.background_file = background_file
self.variables = get_variables(event)
if self.background_file:
self.bg_pdf = PdfFileReader(BytesIO(self.background_file.read()), strict=False)
self.bg_bytes = self.background_file.read()
self.bg_pdf = PdfFileReader(BytesIO(self.bg_bytes), strict=False)
else:
self.bg_bytes = None
self.bg_pdf = None
@classmethod
@@ -329,24 +369,45 @@ class Renderer:
self._draw_textarea(canvas, op, order, o)
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
canvas.setPageSize((self.bg_pdf.getPage(0).mediaBox[2], self.bg_pdf.getPage(0).mediaBox[3]))
canvas.showPage()
def render_background(self, buffer, title=_('Ticket')):
from PyPDF2 import PdfFileWriter, PdfFileReader
buffer.seek(0)
new_pdf = PdfFileReader(buffer)
output = PdfFileWriter()
if settings.PDFTK:
buffer.seek(0)
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, 'back.pdf'), 'wb') as f:
f.write(self.bg_bytes)
with open(os.path.join(d, 'front.pdf'), 'wb') as f:
f.write(buffer.read())
subprocess.run([
settings.PDFTK,
os.path.join(d, 'front.pdf'),
'background',
os.path.join(d, 'back.pdf'),
'output',
os.path.join(d, 'out.pdf'),
'compress'
], check=True)
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from PyPDF2 import PdfFileWriter, PdfFileReader
buffer.seek(0)
new_pdf = PdfFileReader(buffer)
output = PdfFileWriter()
for page in new_pdf.pages:
bg_page = copy.copy(self.bg_pdf.getPage(0))
bg_page.mergePage(page)
output.addPage(bg_page)
for page in new_pdf.pages:
bg_page = copy.copy(self.bg_pdf.getPage(0))
bg_page.mergePage(page)
output.addPage(bg_page)
output.addMetadata({
'/Title': str(title),
'/Creator': 'pretix',
})
outbuffer = BytesIO()
output.write(outbuffer)
outbuffer.seek(0)
return outbuffer
output.addMetadata({
'/Title': str(title),
'/Creator': 'pretix',
})
outbuffer = BytesIO()
output.write(outbuffer)
outbuffer.seek(0)
return outbuffer

View File

@@ -4,6 +4,7 @@ from decimal import Decimal
from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.dispatch import receiver
@@ -17,9 +18,11 @@ from pretix.base.models import (
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.rich_text import rich_text
from pretix.celery_app import app
from pretix.presale.signals import (
@@ -99,7 +102,8 @@ class CartManager:
AddOperation: 30
}
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None):
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
sales_channel='web'):
self.event = event
self.cart_id = cart_id
self.now_dt = now()
@@ -111,6 +115,8 @@ class CartManager:
self._variations_cache = {}
self._expiry = None
self.invoice_address = invoice_address
self._widget_data = widget_data or {}
self._sales_channel = sales_channel
@property
def positions(self):
@@ -188,6 +194,9 @@ class CartManager:
if not op.item.is_available() or (op.variation and not op.variation.active):
raise CartError(error_messages['unavailable'])
if self._sales_channel not in op.item.sales_channels:
raise CartError(error_messages['unavailable'])
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
raise CartError(error_messages['voucher_invalid_item'])
@@ -605,12 +614,37 @@ class CartManager:
if isinstance(op, self.AddOperation):
for k in range(available_count):
new_cart_positions.append(CartPosition(
cp = CartPosition(
event=self.event, item=op.item, variation=op.variation,
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
subevent=op.subevent, includes_tax=op.includes_tax
))
)
if self.event.settings.attendee_names_asked:
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
if 'attendee-name' in self._widget_data:
cp.attendee_name_parts = {'_legacy': self._widget_data['attendee-name']}
if any('attendee-name-{}'.format(k.replace('_', '-')) in self._widget_data for k, l, w
in scheme['fields']):
cp.attendee_name_parts = {
k: self._widget_data.get('attendee-name-{}'.format(k.replace('_', '-')), '')
for k, l, w in scheme['fields']
}
if self.event.settings.attendee_emails_asked and 'email' in self._widget_data:
cp.attendee_email = self._widget_data.get('email')
cp._answers = {}
for k, v in self._widget_data.items():
if not k.startswith('question-'):
continue
q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first()
if q:
try:
cp._answers[q] = q.clean_answer(v)
except ValidationError:
pass
new_cart_positions.append(cp)
elif isinstance(op, self.ExtendOperation):
if available_count == 1:
op.position.expires = self._expiry
@@ -621,7 +655,11 @@ class CartManager:
else:
raise AssertionError("ExtendOperation cannot affect more than one item")
CartPosition.objects.bulk_create(new_cart_positions)
for p in new_cart_positions:
if p._answers:
p.save()
_save_answers(p, {}, p._answers)
CartPosition.objects.bulk_create([p for p in new_cart_positions if not p._answers])
return err
def commit(self):
@@ -702,7 +740,7 @@ def get_fees(event, request, total, invoice_address, provider):
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None) -> None:
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
"""
Adds a list of items to a user's cart.
:param event: The event ID in question
@@ -722,7 +760,8 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia)
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
sales_channel=sales_channel)
cm.add_new_items(items)
cm.commit()
except LockTimeoutException:
@@ -774,7 +813,7 @@ def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None) -> None:
invoice_address: int=None, sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
@@ -792,7 +831,7 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
pass
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia)
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
cm.set_addons(addons)
cm.commit()
except LockTimeoutException:

View File

@@ -3,13 +3,17 @@ from datetime import timedelta
from django.dispatch import receiver
from django.utils.timezone import now
from pretix.base.models import CachedCombinedTicket, CachedTicket
from ..models import CachedFile, CartPosition, InvoiceAddress
from ..signals import periodic_task
@receiver(signal=periodic_task)
def clean_cart_positions(sender, **kwargs):
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14)):
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=False):
cp.delete()
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
cp.delete()
for ia in InvoiceAddress.objects.filter(order__isnull=True, last_modified__lt=now() - timedelta(days=14)):
ia.delete()
@@ -19,3 +23,15 @@ def clean_cart_positions(sender, **kwargs):
def clean_cached_files(sender, **kwargs):
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
cf.delete()
@receiver(signal=periodic_task)
def clean_cached_tickets(sender, **kwargs):
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
cf.delete()
for cf in CachedCombinedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
cf.delete()
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(minutes=30), file__isnull=True):
cf.delete()
for cf in CachedCombinedTicket.objects.filter(created__lte=now() - timedelta(minutes=30), file__isnull=True):
cf.delete()

View File

@@ -53,6 +53,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if open_payment and open_payment.payment_provider:
payment = open_payment.payment_provider.render_invoice_text(invoice.order)
elif invoice.order.status == Order.STATUS_PAID:
payment = pgettext('invoice', 'The payment for this invoice has already been received.')
else:
payment = ""

View File

@@ -91,34 +91,33 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
"""
if new_date < now():
raise OrderError(_('The new expiry date needs to be in the future.'))
if order.status == Order.STATUS_PENDING:
def change(was_expired=True):
order.expires = new_date
order.save(update_fields=['expires'])
if was_expired:
order.status = Order.STATUS_PENDING
order.save(update_fields=['expires'] + (['status'] if was_expired else []))
order.log_action(
'pretix.event.order.expirychanged',
user=user,
auth=auth,
data={
'expires': order.expires,
'state_change': False
'state_change': was_expired
}
)
if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices:
generate_invoice(order)
if order.status == Order.STATUS_PENDING:
change(was_expired=False)
else:
with order.event.lock() as now_dt:
is_available = order._is_still_available(now_dt, count_waitinglist=False)
if is_available is True or force is True:
order.expires = new_date
order.status = Order.STATUS_PENDING
order.save(update_fields=['expires', 'status'])
order.log_action(
'pretix.event.order.expirychanged',
user=user,
auth=auth,
data={
'expires': order.expires,
'state_change': True
}
)
change(was_expired=True)
else:
raise OrderError(is_available)
@@ -379,7 +378,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
products_seen = Counter()
for i, cp in enumerate(positions):
if not cp.item.active or (cp.variation and not cp.variation.active):
if not cp.item.is_available() or (cp.variation and not cp.variation.active):
err = err or error_messages['unavailable']
cp.delete()
continue
@@ -499,7 +498,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None):
meta_info: dict=None, sales_channel: str='web'):
fees, pf = _get_fees(positions, payment_provider, address, meta_info, event)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
@@ -512,7 +511,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
locale=locale,
total=total,
meta_info=json.dumps(meta_info or {}),
require_approval=any(p.item.require_approval for p in positions)
require_approval=any(p.item.require_approval for p in positions),
sales_channel=sales_channel
)
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
order.save()
@@ -542,6 +542,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
OrderPosition.transform_cart_positions(positions, order)
order.log_action('pretix.event.order.placed')
if order.require_approval:
order.log_action('pretix.event.order.placed.require_approval')
if meta_info:
for msg in meta_info.get('confirm_messages', []):
order.log_action('pretix.event.order.consent', data={'msg': msg})
@@ -551,7 +553,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None):
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
event = Event.objects.get(id=event)
if payment_provider:
@@ -580,7 +582,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions, address=addr)
order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info)
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -661,7 +663,9 @@ def send_expiry_warnings(sender, **kwargs):
eventcache = {}
today = now().replace(hour=0, minute=0, second=0)
for o in Order.objects.filter(expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING).only('pk'):
for o in Order.objects.filter(
expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING, datetime__lte=now() - timedelta(hours=2)
).only('pk'):
with transaction.atomic():
o = Order.objects.select_related('event').select_for_update().get(pk=o.pk)
if o.status != Order.STATUS_PENDING or o.expiry_reminder_sent:
@@ -723,7 +727,7 @@ def send_download_reminders(sender, **kwargs):
if now() < reminder_date:
continue
for o in e.orders.filter(status=Order.STATUS_PAID, download_reminder_sent=False).only('pk'):
for o in e.orders.filter(status=Order.STATUS_PAID, download_reminder_sent=False, datetime__lte=now() - timedelta(hours=2)).only('pk'):
with transaction.atomic():
o = Order.objects.select_related('event').select_for_update().get(pk=o.pk)
if o.download_reminder_sent:
@@ -933,6 +937,21 @@ class OrderChangeManager:
if self.order.pending_sum <= Decimal('0.00'):
self.order.status = Order.STATUS_PAID
self.order.save()
elif self.open_payment:
self.open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED
self.open_payment.save()
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
elif self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and self._totaldiff > 0:
if self.open_payment:
self.open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED
self.open_payment.save()
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
def _check_paid_to_free(self):
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)):
@@ -1024,6 +1043,7 @@ class OrderChangeManager:
'addon_to': opa.addon_to_id,
'old_price': opa.price,
})
opa.delete()
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
@@ -1303,11 +1323,13 @@ class OrderChangeManager:
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None):
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web'):
with language(locale):
try:
try:
return _perform_order(event, payment_provider, positions, email, locale, address, meta_info)
return _perform_order(event, payment_provider, positions, email, locale, address, meta_info,
sales_channel)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -1,6 +1,5 @@
import logging
import os
from datetime import timedelta
from django.core.files.base import ContentFile
from django.utils.timezone import now
@@ -20,54 +19,48 @@ from pretix.helpers.database import rolledback_transaction
logger = logging.getLogger(__name__)
@app.task(base=ProfiledTask)
def generate(order_position: str, provider: str):
def generate_orderposition(order_position: int, provider: str):
order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position)
try:
ct = CachedTicket.objects.get(order_position=order_position, provider=provider)
except CachedTicket.MultipleObjectsReturned:
CachedTicket.objects.filter(order_position=order_position, provider=provider).delete()
ct = CachedTicket.objects.create(order_position=order_position, provider=provider, extension='',
type='', file=None)
except CachedTicket.DoesNotExist:
ct = CachedTicket.objects.create(order_position=order_position, provider=provider, extension='',
type='', file=None)
with language(order_position.order.locale):
responses = register_ticket_outputs.send(order_position.order.event)
for receiver, response in responses:
prov = response(order_position.order.event)
if prov.identifier == provider:
filename, ct.type, data = prov.generate(order_position)
filename, ttype, data = prov.generate(order_position)
path, ext = os.path.splitext(filename)
ct.extension = ext
ct.save()
for ct in CachedTicket.objects.filter(order_position=order_position, provider=provider):
ct.delete()
ct = CachedTicket.objects.create(order_position=order_position, provider=provider,
extension=ext, type=ttype, file=None)
ct.file.save(filename, ContentFile(data))
return ct.pk
@app.task(base=ProfiledTask)
def generate_order(order: int, provider: str):
order = Order.objects.select_related('event').get(id=order)
try:
ct = CachedCombinedTicket.objects.get(order=order, provider=provider)
except CachedCombinedTicket.MultipleObjectsReturned:
CachedCombinedTicket.objects.filter(order=order, provider=provider).delete()
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension='',
type='', file=None)
except CachedCombinedTicket.DoesNotExist:
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension='',
type='', file=None)
with language(order.locale):
responses = register_ticket_outputs.send(order.event)
for receiver, response in responses:
prov = response(order.event)
if prov.identifier == provider:
filename, ct.type, data = prov.generate_order(order)
filename, ttype, data = prov.generate_order(order)
path, ext = os.path.splitext(filename)
ct.extension = ext
ct.save()
for ct in CachedCombinedTicket.objects.filter(order=order, provider=provider):
ct.delete()
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension=ext,
type=ttype, file=None)
ct.file.save(filename, ContentFile(data))
return ct.pk
@app.task(base=ProfiledTask)
def generate(model: str, pk: int, provider: str):
if model == 'order':
return generate_order(pk, provider)
elif model == 'orderposition':
return generate_orderposition(pk, provider)
class DummyRollbackException(Exception):
@@ -103,56 +96,6 @@ def preview(event: int, provider: str):
return prov.generate(p)
def get_cachedticket_for_position(pos, identifier, generate_async=True):
apply_method = 'apply_async' if generate_async else 'apply'
try:
ct = CachedTicket.objects.filter(
order_position=pos, provider=identifier
).last()
except CachedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedTicket.objects.create(
order_position=pos, provider=identifier,
extension='', type='', file=None)
getattr(generate, apply_method)(args=(pos.id, identifier))
if not generate_async:
ct.refresh_from_db()
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
getattr(generate, apply_method)(args=(pos.id, identifier))
if not generate_async:
ct.refresh_from_db()
return ct
def get_cachedticket_for_order(order, identifier, generate_async=True):
apply_method = 'apply_async' if generate_async else 'apply'
try:
ct = CachedCombinedTicket.objects.filter(
order=order, provider=identifier
).last()
except CachedCombinedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedCombinedTicket.objects.create(
order=order, provider=identifier,
extension='', type='', file=None)
getattr(generate_order, apply_method)(args=(order.id, identifier))
if not generate_async:
ct.refresh_from_db()
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
getattr(generate_order, apply_method)(args=(order.id, identifier))
if not generate_async:
ct.refresh_from_db()
return ct
def get_tickets_for_order(order):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
@@ -174,7 +117,12 @@ def get_tickets_for_order(order):
if p.multi_download_enabled:
try:
ct = get_cachedticket_for_order(order, p.identifier, generate_async=False)
ct = CachedCombinedTicket.objects.filter(
order=order, provider=p.identifier, file__isnull=False
).last()
if not ct or not ct.file:
retval = generate.apply(args=('order', order.pk, p.identifier))
ct = CachedCombinedTicket.objects.get(pk=retval.get())
tickets.append((
"{}-{}-{}{}".format(
order.event.slug.upper(), order.code, ct.provider, ct.extension,
@@ -190,7 +138,12 @@ def get_tickets_for_order(order):
if not pos.item.admission and not order.event.settings.ticket_download_nonadm:
continue
try:
ct = get_cachedticket_for_position(pos, p.identifier, generate_async=False)
ct = CachedTicket.objects.filter(
order_position=pos, provider=p.identifier, file__isnull=False
).last()
if not ct or not ct.file:
retval = generate.apply(args=('orderposition', pos.pk, p.identifier))
ct = CachedTicket.objects.get(pk=retval.get())
tickets.append((
"{}-{}-{}-{}{}".format(
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,

View File

@@ -483,6 +483,14 @@ Your {event} team"""))
'default': '#8E44B3',
'type': str
},
'theme_color_success': {
'default': '#50A167',
'type': str
},
'theme_color_danger': {
'default': '#D36060',
'type': str
},
'primary_font': {
'default': 'Open Sans',
'type': str
@@ -515,6 +523,10 @@ Your {event} team"""))
'default': '',
'type': LazyI18nString
},
'voucher_explanation_text': {
'default': '',
'type': LazyI18nString
},
'organizer_info_text': {
'default': '',
'type': LazyI18nString

View File

@@ -178,6 +178,15 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
notification settings!
"""
register_sales_channels = django.dispatch.Signal(
providing_args=[]
)
"""
This signal is sent out to get all known sales channels types. Receivers should return an
instance of a subclass of pretix.base.channels.SalesChannel or a list of such
instances.
"""
register_data_exporters = EventPluginSignal(
providing_args=[]
)

View File

@@ -8,11 +8,14 @@ from django.utils.translation import get_language
from pretix.base.models.auth import StaffSession
from pretix.base.settings import GlobalSettingsObject
from pretix.control.navigation import (
get_event_navigation, get_global_navigation, get_organizer_navigation,
)
from ..helpers.i18n import (
get_javascript_format, get_javascript_output_format, get_moment_locale,
)
from .signals import html_head, nav_event, nav_global, nav_topbar
from .signals import html_head, nav_topbar
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -40,10 +43,9 @@ def contextprocessor(request):
ctx['html_head'] = "".join(_html_head)
_js_payment_weekdays_disabled = '[]'
_nav_event = []
if getattr(request, 'event', None) and hasattr(request, 'organizer') and request.user.is_authenticated:
for receiver, response in nav_event.send(request.event, request=request):
_nav_event += response
ctx['nav_items'] = get_event_navigation(request)
if request.event.settings.get('payment_term_weekdays'):
_js_payment_weekdays_disabled = '[0,6]'
@@ -65,17 +67,13 @@ def contextprocessor(request):
if request.GET.get('subevent', ''):
# Do not use .get() for lazy evaluation
ctx['selected_subevents'] = request.event.subevents.filter(pk=request.GET.get('subevent'))
elif getattr(request, 'organizer', None) and request.user.is_authenticated:
ctx['nav_items'] = get_organizer_navigation(request)
elif request.user.is_authenticated:
ctx['nav_items'] = get_global_navigation(request)
ctx['nav_event'] = _nav_event
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
_nav_global = []
if not hasattr(request, 'event') and request.user.is_authenticated:
for receiver, response in nav_global.send(request, request=request):
_nav_global += response
ctx['nav_global'] = sorted(_nav_global, key=lambda n: n['label'])
_nav_topbar = []
if request.user.is_authenticated:
for receiver, response in nav_topbar.send(request, request=request):

View File

@@ -938,7 +938,27 @@ class DisplaySettingsForm(SettingsForm):
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_success = forms.CharField(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
required=False,
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'})
)
theme_color_danger = forms.CharField(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a dark shade of red."),
required=False,
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'})
)
@@ -961,6 +981,14 @@ class DisplaySettingsForm(SettingsForm):
required=False,
widget=I18nTextarea
)
voucher_explanation_text = I18nFormField(
label=_("Voucher explanation"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
"how to obtain a voucher code.")
)
show_variations_expanded = forms.BooleanField(
label=_("Show variations of a product expanded by default"),
required=False

View File

@@ -116,8 +116,8 @@ class OrderFilterForm(FilterForm):
u = fdata.get('query')
if "-" in u:
code = (Q(event__slug__icontains=u.split("-")[0])
& Q(code__icontains=Order.normalize_code(u.split("-")[1])))
code = (Q(event__slug__icontains=u.rsplit("-", 1)[0])
& Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1])))
else:
code = Q(code__icontains=Order.normalize_code(u))

View File

@@ -8,6 +8,7 @@ from django.utils.translation import (
)
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
@@ -226,6 +227,10 @@ class ItemCreateForm(I18nModelForm):
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
self.instance.free_price = self.cleaned_data['copy_from'].free_price
self.instance.original_price = self.cleaned_data['copy_from'].original_price
self.instance.sales_channels = self.cleaned_data['copy_from'].sales_channels
else:
# Add to all sales channels by default
self.instance.sales_channels = [k for k in get_all_sales_channels().keys()]
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)
@@ -302,6 +307,13 @@ class ItemUpdateForm(I18nModelForm):
'over 65. This ticket includes access to all parts of the event, except the VIP '
'area.'
)
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
change_decimal_field(self.fields['default_price'], self.event.currency)
class Meta:
@@ -312,6 +324,7 @@ class ItemUpdateForm(I18nModelForm):
'name',
'internal_name',
'active',
'sales_channels',
'admission',
'description',
'picture',

View File

@@ -49,7 +49,7 @@ class ExtendForm(I18nModelForm):
return data
class MarkPaidForm(forms.Form):
class ConfirmPaymentForm(forms.Form):
force = forms.BooleanField(
label=_('Overbook quota and ignore late payment'),
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
@@ -75,6 +75,20 @@ class MarkPaidForm(forms.Form):
del self.fields['force']
class MarkPaidForm(ConfirmPaymentForm):
amount = forms.DecimalField(
required=True,
max_digits=10, decimal_places=2,
localize=True,
label=_('Payment amount'),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
change_decimal_field(self.fields['amount'], self.instance.event.currency)
self.fields['amount'].initial = max(0, self.instance.pending_sum)
class ExporterForm(forms.Form):
def clean(self):
data = super().clean()

View File

@@ -1,3 +1,5 @@
from urllib.parse import urlparse
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -80,6 +82,19 @@ class OrganizerUpdateForm(OrganizerForm):
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
def clean_domain(self):
d = self.cleaned_data['domain']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk).exists():
raise ValidationError(
_('This domain is already in use for a different organizer.')
)
return d
def clean_slug(self):
if self.change_slug:
return self.cleaned_data['slug']
@@ -178,7 +193,28 @@ class OrganizerDisplaySettingsForm(SettingsForm):
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_success = forms.CharField(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
required=False,
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'})
)
theme_color_danger = forms.CharField(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a shade of red."),
required=False,
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'})
)

View File

@@ -2,6 +2,7 @@ from datetime import timedelta
from django import forms
from django.forms import formset_factory
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -276,13 +277,13 @@ class RRuleForm(forms.Form):
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
@@ -291,18 +292,7 @@ class RRuleForm(forms.Form):
)
yearly_bymonth = forms.ChoiceField(
choices=[
('1', _('January')),
('2', _('February')),
('3', _('March')),
('4', _('April')),
('5', _('May')),
('6', _('June')),
('7', _('July')),
('8', _('August')),
('9', _('September')),
('10', _('October')),
('11', _('November')),
('12', _('December')),
(str(i), MONTHS[i]) for i in range(1, 13)
],
required=False
)
@@ -326,13 +316,13 @@ class RRuleForm(forms.Form):
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
@@ -342,13 +332,13 @@ class RRuleForm(forms.Form):
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
],
required=False,
widget=forms.CheckboxSelectMultiple

View File

@@ -6,7 +6,10 @@ import bleach
import dateutil.parser
import pytz
from django.dispatch import receiver
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.strings import LazyI18nString
@@ -91,12 +94,17 @@ def _display_order_changed(event: Event, logentry: LogEntry):
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
old_item=old_item,
url = reverse('control:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'code': data['new_order']
})
return mark_safe(escape(text) + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
old_item=escape(old_item),
posid=data.get('positionid', '?'),
order=data['new_order'],
order='<a href="{}">{}</a>'.format(url, data['new_order']),
old_price=money_filter(Decimal(data['old_price']), event.currency),
)
))
elif logentry.action_type == 'pretix.event.order.changed.split_from':
return _('This order has been created by splitting the order {order}').format(
order=data['original_order'],
@@ -165,6 +173,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.canceled': _('The order has been canceled.'),
'pretix.event.order.placed': _('The order has been created.'),
'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '

View File

@@ -0,0 +1,471 @@
from django.http import HttpRequest
from django.urls import reverse
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.control.signals import (
nav_event, nav_event_settings, nav_global, nav_organizer,
)
def get_event_navigation(request: HttpRequest):
url = request.resolver_match
if not url:
return []
nav = [
{
'label': _('Dashboard'),
'url': reverse('control:event.index', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': (url.url_name == 'event.index'),
'icon': 'dashboard',
}
]
if 'can_change_event_settings' in request.eventpermset:
event_settings = [
{
'label': _('General'),
'url': reverse('control:event.settings', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings',
},
{
'label': _('Payment'),
'url': reverse('control:event.settings.payment', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.payment',
},
{
'label': _('Plugins'),
'url': reverse('control:event.settings.plugins', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.plugins',
},
{
'label': _('Display'),
'url': reverse('control:event.settings.display', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.display',
},
{
'label': _('Tickets'),
'url': reverse('control:event.settings.tickets', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.tickets',
},
{
'label': _('E-mail'),
'url': reverse('control:event.settings.mail', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.mail',
},
{
'label': _('Tax rules'),
'url': reverse('control:event.settings.tax', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.tax',
},
{
'label': _('Invoicing'),
'url': reverse('control:event.settings.invoice', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.invoice',
},
{
'label': _('Widget'),
'url': reverse('control:event.settings.widget', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.widget',
},
]
event_settings += sorted(
sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []),
key=lambda r: r['label']
)
nav.append({
'label': _('Settings'),
'url': reverse('control:event.settings', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False,
'icon': 'wrench',
'children': event_settings
})
if request.event.has_subevents:
nav.append({
'label': pgettext_lazy('subevent', 'Dates'),
'url': reverse('control:event.subevents', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': ('event.subevent' in url.url_name),
'icon': 'calendar',
})
if 'can_change_items' in request.eventpermset:
nav.append({
'label': _('Products'),
'url': reverse('control:event.items', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False,
'icon': 'ticket',
'children': [
{
'label': _('Products'),
'url': reverse('control:event.items', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name in (
'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name,
},
{
'label': _('Quotas'),
'url': reverse('control:event.items.quotas', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.quota' in url.url_name,
},
{
'label': _('Categories'),
'url': reverse('control:event.items.categories', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.categories' in url.url_name,
},
{
'label': _('Questions'),
'url': reverse('control:event.items.questions', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.questions' in url.url_name,
},
]
})
if 'can_view_orders' in request.eventpermset:
nav.append({
'label': _('Orders'),
'url': reverse('control:event.orders', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False,
'icon': 'shopping-cart',
'children': [
{
'label': _('All orders'),
'url': reverse('control:event.orders', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name in ('event.orders', 'event.order') or "event.order." in url.url_name,
},
{
'label': _('Overview'),
'url': reverse('control:event.orders.overview', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.overview' in url.url_name,
},
{
'label': _('Refunds'),
'url': reverse('control:event.orders.refunds', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.refunds' in url.url_name,
},
{
'label': _('Export'),
'url': reverse('control:event.orders.export', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.export' in url.url_name,
},
{
'label': _('Waiting list'),
'url': reverse('control:event.orders.waitinglist', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.waitinglist' in url.url_name,
},
]
})
if 'can_view_vouchers' in request.eventpermset:
nav.append({
'label': _('Vouchers'),
'url': reverse('control:event.vouchers', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False,
'icon': 'tags',
'children': [
{
'label': _('All vouchers'),
'url': reverse('control:event.vouchers', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name != 'event.vouchers.tags' and "event.vouchers" in url.url_name,
},
{
'label': _('Tags'),
'url': reverse('control:event.vouchers.tags', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.vouchers.tags' in url.url_name,
},
]
})
if 'can_view_orders' in request.eventpermset:
nav.append({
'label': pgettext_lazy('navigation', 'Check-in'),
'url': reverse('control:event.orders.checkinlists', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False,
'icon': 'check-square-o',
'children': [
{
'label': _('Check-in lists'),
'url': reverse('control:event.orders.checkinlists', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.checkin' in url.url_name,
},
]
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_event.send(request.event, request=request)), []),
key=lambda r: r['label']
))
return nav
def get_global_navigation(request):
url = request.resolver_match
if not url:
return []
has_staff_session = request.user.has_active_staff_session(request.session.session_key)
nav = [
{
'label': _('Dashboard'),
'url': reverse('control:index'),
'active': (url.url_name == 'index'),
'icon': 'dashboard',
},
{
'label': _('Events'),
'url': reverse('control:events'),
'active': 'events' in url.url_name,
'icon': 'calendar',
},
{
'label': _('Organizers'),
'url': reverse('control:organizers'),
'active': 'organizers' in url.url_name,
'icon': 'group',
},
{
'label': _('Order search'),
'url': reverse('control:search.orders'),
'active': 'search.orders' in url.url_name,
'icon': 'search',
},
{
'label': _('User settings'),
'url': reverse('control:user.settings'),
'active': False,
'icon': 'user',
'children': [
{
'label': _('General'),
'url': reverse('control:user.settings'),
'active': 'user.settings' == url.url_name,
},
{
'label': _('Notifications'),
'url': reverse('control:user.settings.notifications'),
'active': 'user.settings.notifications' == url.url_name,
},
{
'label': _('2FA'),
'url': reverse('control:user.settings.2fa'),
'active': 'user.settings.2fa' in url.url_name,
},
{
'label': _('Authorized apps'),
'url': reverse('control:user.settings.oauth.list'),
'active': 'user.settings.oauth' in url.url_name,
},
{
'label': _('Account history'),
'url': reverse('control:user.settings.history'),
'active': 'user.settings.history' in url.url_name,
},
]
},
]
if has_staff_session:
nav.append({
'label': _('Users'),
'url': reverse('control:users'),
'active': False,
'icon': 'user',
'children': [
{
'label': _('All users'),
'url': reverse('control:users'),
'active': ('users' in url.url_name),
},
{
'label': _('Admin sessions'),
'url': reverse('control:user.sudo.list'),
'active': ('sudo' in url.url_name),
},
]
})
nav.append({
'label': _('Global settings'),
'url': reverse('control:global.settings'),
'active': False,
'icon': 'wrench',
'children': [
{
'label': _('Settings'),
'url': reverse('control:global.settings'),
'active': (url.url_name == 'global.settings'),
},
{
'label': _('Update check'),
'url': reverse('control:global.update'),
'active': (url.url_name == 'global.update'),
},
]
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
key=lambda r: r['label']
))
return nav
def get_organizer_navigation(request):
url = request.resolver_match
if not url:
return []
nav = [
{
'label': _('Events'),
'url': reverse('control:organizer', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name == 'organizer',
'icon': 'calendar',
},
]
if 'can_change_organizer_settings' in request.orgapermset:
nav.append({
'label': _('Settings'),
'url': reverse('control:organizer.edit', kwargs={
'organizer': request.organizer.slug
}),
'icon': 'wrench',
'children': [
{
'label': _('General'),
'url': reverse('control:organizer.edit', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name == 'organizer.edit',
},
{
'label': _('Display'),
'url': reverse('control:organizer.display', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name == 'organizer.display',
},
]
})
if 'can_change_teams' in request.orgapermset:
nav.append({
'label': _('Teams'),
'url': reverse('control:organizer.teams', kwargs={
'organizer': request.organizer.slug
}),
'active': 'organizer.team' in url.url_name,
'icon': 'group',
})
if 'can_change_organizer_settings' in request.orgapermset:
nav.append({
'label': _('Devices'),
'url': reverse('control:organizer.devices', kwargs={
'organizer': request.organizer.slug
}),
'active': 'organizer.device' in url.url_name,
'icon': 'tablet',
})
nav.append({
'label': _('Webhooks'),
'url': reverse('control:organizer.webhooks', kwargs={
'organizer': request.organizer.slug
}),
'active': 'organizer.webhook' in url.url_name,
'icon': 'bolt',
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
[]),
key=lambda r: r['label']
))
return nav
def merge_in(nav, newnav):
for item in newnav:
if 'parent' in item:
parents = [n for n in nav if n['url'] == item['parent']]
if parents:
parents[0]['children'].append(item)
else:
nav.append(item)

View File

@@ -46,7 +46,6 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.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>
@@ -82,10 +81,6 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<button type="button" class="navbar-toggle navbar-events"
data-toggle="collapse" data-target=".navbar-events-collapse">
<i class="fa fa-calendar"></i><span class="caret"></span>
</button>
{% if request.event %}
{% if has_domain and not request.event.live %}
<form action="{% eventurl request.event "presale:event.auth" %}" method="post"
@@ -108,24 +103,6 @@
</a>
</div>
<ul class="nav navbar-nav navbar-top-links navbar-left hidden-xs">
<li class="dropdown">
<a href="#" class="dropdown-toggle event-dropdown-toggle" data-toggle="dropdown"><i class="fa fa-calendar"></i>
<div class="event-indicator">
<span class="event-name">{{ request.event }}</span>
<span class="event-daterange">{{ request.event.get_date_range_display }}</span>
</div>
<span class="caret"></span></a>
<ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead
data-source="{% url "control:events.typeahead" %}">
<li class="query-holder">
<div class="form-box">
<input type="text" class="form-control"
placeholder="{% trans "Search for events" %}"
data-typeahead-query>
</div>
</li>
</ul>
</li>
{% if request.event %}
<li>
{% if has_domain and not request.event.live %}
@@ -209,71 +186,56 @@
</li>
</ul>
<div class="navbar-default sidebar" role="navigation">
<div class="sidebar-nav navbar-events-collapse navbar-collapse hidden-sm hidden-md hidden-lg mobile-event-dropdown">
<ul class="nav" data-event-typeahead data-source="{% url "control:events.typeahead" %}">
<li class="query-holder">
<div class="form-box">
<input type="text" class="form-control"
placeholder="{% trans "Search for events" %}"
data-typeahead-query>
</div>
</li>
</ul>
</div>
<div class="sidebar-nav navbar-nav-collapse navbar-collapse">
<div class="dropdown context-selector">
{% if request.event %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-calendar fa-stack-1x fa-inverse"></i>
</span>
<div class="context-indicator">
<span class="context-name">{{ request.event }}</span>
<span class="context-meta">{{ request.event.get_date_range_display }}</span>
</div>
<span class="caret"></span></a>
{% elif request.organizer %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-group fa-stack-1x fa-inverse"></i>
</span>
<div class="context-indicator">
<span class="context-name">{{ request.organizer }}</span>
<span class="context-meta">{% trans "Organizer account" %}</span>
</div>
<span class="caret"></span></a>
{% else %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-user fa-stack-1x fa-inverse"></i>
</span>
<div class="context-indicator">
<span class="context-name">{{ request.user }}</span>
</div>
<span class="caret"></span></a>
{% endif %}
<ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead
data-source="{% url "control:nav.typeahead" %}">
<li class="query-holder">
<div class="form-box">
<input type="text" class="form-control" id="event-dropdown-field"
placeholder="{% trans "Search for events" %}"
data-typeahead-query>
</div>
</li>
</ul>
</div>
<ul class="nav" id="side-menu">
{% block nav %}
<li>
<a href="{% url 'control:index' %}" {% if url_name == "index" %}class="active"{% endif %}>
<i class="fa fa-dashboard fa-fw"></i>
{% trans "Dashboard" %}
</a>
</li>
{% if staff_session %}
<li>
<a href="{% url 'control:global.settings' %}"
{% if "global.settings" in url_name %}class="active"{% endif %}>
<i class="fa fa-wrench fa-fw"></i>
{% trans "Global settings" %}
</a>
</li>
{% endif %}
<li>
<a href="{% url 'control:events' %}" {% if "events" in url_name %}class="active"{% endif %}>
<i class="fa fa-calendar fa-fw"></i>
{% trans "Events" %}
</a>
</li>
<li>
<a href="{% url 'control:organizers' %}" {% if "organizer" in url_name %}class="active"{% endif %}>
<i class="fa fa-users fa-fw"></i>
{% trans "Organizers" %}
</a>
</li>
<li>
<a href="{% url 'control:search.orders' %}"
{% if url_name == "search.orders" %}class="active"{% endif %}>
<i class="fa fa-search fa-fw"></i>
{% trans "Order search" %}
</a>
</li>
{% if staff_session %}
<li>
<a href="{% url 'control:users' %}"
{% if "users" in url_name %}class="active"{% endif %}>
<i class="fa fa-user fa-fw"></i>
{% trans "Users" %}
</a>
</li>
<li>
<a href="{% url 'control:user.sudo.list' %}"
{% if "sudo" in url_name %}class="active"{% endif %}>
<i class="fa fa-id-card fa-fw"></i>
{% trans "Admin sessions" %}
</a>
</li>
{% endif %}
{% for nav in nav_global %}
{% for nav in nav_items %}
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="has-children"{% endif %}>

View File

@@ -8,7 +8,7 @@
<input type="text" class="form-control" id="dashboard_query"
placeholder="{% trans "Go to event" %}"
data-typeahead-query autofocus>
<ul data-event-typeahead data-source="{% url "control:events.typeahead" %}" data-typeahead-field="#dashboard_query"
<ul data-event-typeahead data-source="{% url "control:nav.typeahead" %}" data-typeahead-field="#dashboard_query"
class="event-dropdown dropdown-menu">
</ul>
</div>

View File

@@ -2,158 +2,3 @@
{% load i18n %}
{% load static %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block nav %}
<li>
<a href="{% url 'control:event.index' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.index" %}class="active"{% endif %}>
<i class="fa fa-dashboard fa-fw"></i>
{% trans "Event dashboard" %}
</a>
</li>
{% if 'can_change_event_settings' in request.eventpermset %}
<li>
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if is_event_settings or "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
<i class="fa fa-wrench fa-fw"></i>
{% trans "Settings" %}
</a>
</li>
{% if request.event.has_subevents %}
<li>
<a href="{% url 'control:event.subevents' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.subevent" in url_name %}class="active"{% endif %}>
<i class="fa fa-calendar fa-fw"></i>
{% trans "Dates" context "subevent" %}
</a>
</li>
{% endif %}
{% endif %}
{% 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">
<i class="fa fa-ticket fa-fw"></i>
{% trans "Products" %}
</a>
<a href="#" class="arrow">
<span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li>
<a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.items" == url_name or "event.item." in url_name or "event.items.add" == url_name or url_name == "event.item" %}class="active"{% endif %}>
{% trans "Products" %}</a>
</li>
<li>
<a href="{% url 'control:event.items.quotas' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.items.quotas" in url_name %}class="active"{% endif %}>
{% trans "Quotas" %}
</a>
</li>
<li>
<a href="{% url 'control:event.items.categories' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.items.categories" in url_name %}class="active"{% endif %}>
{% trans "Categories" %}
</a>
</li>
<li>
<a href="{% url 'control:event.items.questions' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.items.questions" in url_name %}class="active"{% endif %}>
{% trans "Questions" %}
</a>
</li>
</ul>
</li>
{% endif %}
{% 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">
<i class="fa fa-shopping-cart fa-fw"></i>
{% trans "Orders" %}
</a>
<a href="#" class="arrow">
<span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li>
<a href="{% url 'control:event.orders' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders" or "event.order." in url_name or url_name == "event.order" %}class="active"{% endif %}>
{% trans "All orders" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.overview' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.overview" %}class="active"{% endif %}>
{% trans "Overview" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.refunds' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.refunds" %}class="active"{% endif %}>
{% trans "Refunds" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.export' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.export" %}class="active"{% endif %}>
{% trans "Export" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.waitinglist' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.waitinglist" %}class="active"{% endif %}>
{% trans "Waiting list" %}
</a>
</li>
</ul>
</li>
{% endif %}
{% 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.voucher" %}class="active"{% endif %}>
<i class="fa fa-tags fa-fw"></i>
{% trans "Vouchers" %}
</a>
</li>
{% endif %}
{% if 'can_view_orders' in request.eventpermset %}
<li>
<a href="{% url 'control:event.orders.checkinlists' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.orders.checkin" in url_name %}class="active"{% endif %}>
<i class="fa fa-check-square-o fa-fw"></i>
{% trans "Check-in lists" %}
</a>
</li>
{% endif %}
{% for nav in nav_event %}
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="has-children"{% endif %}>
{% 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 }}
</a>
{% if nav.children %}
<a href="#" class="arrow">
<span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
{% for item in nav.children %}
<li>
<a href="{{ item.url }}"
{% if item.active %}class="active"{% endif %}>
{{ item.label }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% endblock %}

View File

@@ -3,6 +3,7 @@
{% load bootstrap3 %}
{% load hierarkey_form %}
{% block inside %}
<h1>{% trans "Display settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
@@ -10,6 +11,7 @@
<legend>{% trans "Event page" %}</legend>
{% bootstrap_field form.logo_image layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.voucher_explanation_text layout="control" %}
{% bootstrap_field form.show_variations_expanded layout="control" %}
{% bootstrap_field form.meta_noindex layout="control" %}
{% if form.frontpage_subevent_ordering %}
@@ -19,8 +21,10 @@
<fieldset>
<legend>{% trans "Shop design" %}</legend>
{% url "control:organizer.display" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "primary_color" "primary_font" %}
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" %}
{% bootstrap_field form.primary_color layout="control" %}
{% bootstrap_field form.theme_color_success layout="control" %}
{% bootstrap_field form.theme_color_danger layout="control" %}
{% bootstrap_field form.primary_font layout="control" %}
{% endpropagated %}
</fieldset>

View File

@@ -2,11 +2,12 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Invoice settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Invoice settings" %}</legend>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}

View File

@@ -3,12 +3,13 @@
{% load bootstrap3 %}
{% load static %}
{% block inside %}
<h1>{% trans "E-mail settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "E-mail settings" %}</legend>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}

View File

@@ -36,7 +36,7 @@
</div>
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
{% for l in request.event.settings.locales %}
<pre lang="{{ l }}" for="{{ item }}" class="mail-preview"></pre>
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endfor %}
</div>
</div>

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Payment settings" %}</h1>
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>

View File

@@ -1,19 +0,0 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block inside %}
<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>
{% endblock %}

View File

@@ -2,10 +2,10 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Installed plugins" %}</h1>
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
<legend>{% trans "Installed plugins" %}</legend>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "General settings" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}

View File

@@ -27,68 +27,6 @@
</div>
{% endif %}
<h1>{% trans "Settings" %}</h1>
<ul class="nav nav-pills">
{% 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" %}
</a>
</li>
<li {% if "event.settings.payment" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.payment' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Payment" %}
</a>
</li>
<li {% if "event.settings.plugins" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.plugins' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Plugins" %}
</a>
</li>
<li {% if "event.settings.display" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.display' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Display" %}
</a>
</li>
<li {% if "event.settings.tickets" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.tickets' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Tickets" %}
</a>
</li>
<li {% if "event.settings.mail" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.mail' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "E-mail" %}
</a>
</li>
<li {% if "event.settings.tax" in url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.tax' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Tax rules" %}
</a>
</li>
<li {% if "event.settings.invoice" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.invoice' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Invoicing" %}
</a>
</li>
<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" %}
</a>
</li>
<li {% if "event.settings.widget" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.widget' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Widget" %}
</a>
</li>
{% endif %}
{% for nav in nav_event_settings %}
<li {% if nav.active %}class="active"{% endif %}>
<a href="{{ nav.url }}">
{{ nav.label }}
</a>
</li>
{% endfor %}
</ul>
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% load bootstrap3 %}
{% block title %}{% trans "Delete tax rule" %}{% endblock %}
{% block inside %}
<legend>{% trans "Delete tax rule" %}</legend>
<h1>{% trans "Delete tax rule" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if possible %}

View File

@@ -11,9 +11,9 @@
{% endblock %}
{% block inside %}
{% if rule %}
<legend>{% blocktrans with name=rule.name %}Tax rule: {{ name }}{% endblocktrans %}</legend>
<h1>{% blocktrans with name=rule.name %}Tax rule: {{ name }}{% endblocktrans %}</h1>
{% else %}
<legend>{% trans "Tax rule" %}</legend>
<h1>{% trans "Tax rule" %}</h1>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}

View File

@@ -2,7 +2,7 @@
{% load i18n %}
{% block title %}{% trans "Tax rules" %}{% endblock %}
{% block inside %}
<legend>{% trans "Tax rules" %}</legend>
<h1>{% trans "Tax rules" %}</h1>
{% if taxrules|length == 0 %}
<div class="empty-collection">
<p>

View File

@@ -4,8 +4,8 @@
{% block inside %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
<h1>{% trans "Ticket download" %}</h1>
<fieldset>
<legend>{% trans "Ticket download" %}</legend>
{% if request.event.settings.ticket_download and not any_enabled %}
<div class="alert alert-warning">
{% blocktrans trimmed %}

View File

@@ -5,7 +5,7 @@
{% load eventurl %}
{% load eventsignal %}
{% block inside %}
<legend>{% trans "Widget" %}</legend>
<h1>{% trans "Widget" %}</h1>
<p>
{% blocktrans trimmed %}
The pretix widget is a way to embed your ticket shop into your event website. This way, your visitors can

View File

@@ -5,18 +5,6 @@
{% block title %}{% trans "Global settings" %}{% endblock %}
{% block content %}
<h1>{% trans "Global settings" %}</h1>
<ul class="nav nav-pills">
<li {% if "global.settings" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:global.settings' %}">
{% trans "General" %}
</a>
</li>
<li {% if "global.update" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:global.update' %}">
{% trans "Update check" %}
</a>
</li>
</ul>
{% block inner %}
{% endblock %}
{% endblock %}

View File

@@ -36,6 +36,12 @@
item to an existing or newly created quota.
{% endblocktrans %}
</div>
{% elif not object.is_available_by_time %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This product is currently not being sold since you configured below that it should only be available in a certain timeframe.
{% endblocktrans %}
</div>
{% endif %}
{% block inside %}
{% endblock %}

View File

@@ -26,6 +26,7 @@
</fieldset>
<fieldset>
<legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.sales_channels layout="control" %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.max_per_order layout="control" %}

View File

@@ -54,8 +54,16 @@
</td>
<td>
{% if i.available_from or i.available_until %}
<span class="fa fa-clock-o fa-fw text-muted" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}">
</span>
{% if not i.is_available_by_time %}
<span class="label label-danger" data-toggle="tooltip"
title="{% trans "Currently unavailable since a limited timeframe for this product has been set" %}">
<span class="fa fa-clock-o fa-fw" data-toggle="tooltip">
</span>
</span>
{% else %}
<span class="fa fa-clock-o fa-fw text-muted" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}">
</span>
{% endif %}
{% endif %}
</td>
<td>
@@ -69,10 +77,15 @@
{% endif %}
</td>
<td>
{% if i.hide_without_voucher %}
<span class="fa fa-ticket fa-fw text-muted" data-toggle="tooltip" title="{% trans "Only visible with a voucher" %}"></span>
{% if i.category.is_addon %}
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as an add-on product" %}"></span>
{% elif i.hide_without_voucher %}
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
{% elif i.require_voucher %}
<span class="fa fa-ticket fa-fw text-muted" data-toggle="tooltip" title="{% trans "Can only bought using a voucher" %}"></span>
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Can only bought using a voucher" %}"></span>
{% endif %}
</td>
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>

View File

@@ -114,6 +114,10 @@
<dd>{{ order.code }}</dd>
<dt>{% trans "Order date" %}</dt>
<dd>{{ order.datetime }}</dd>
{% if sales_channel %}
<dt>{% trans "Sales channel" %}</dt>
<dd>{{ sales_channel.verbose_name }}</dd>
{% endif %}
<dt>{% trans "Order locale" %}</dt>
<dd>
{{ display_locale }}
@@ -184,6 +188,16 @@
<br/>
{% endif %}
{% endfor %}
{% if can_generate_invoice %}
<br/>
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.geninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% csrf_token %}
<button class="btn btn-default btn-xs">
{% trans "Generate invoice" %}
</button>
</form>
{% endif %}
</dd>
{% elif can_generate_invoice %}
<dt>{% trans "Invoices" %}</dt>
@@ -388,7 +402,7 @@
</div>
{% eventsignal event "pretix.control.signals.order_info" order=order request=request %}
<div class="row payments">
<div class="{% if request.event.settings.invoice_address_asked or order.invoice_address %}col-md-6{% else %}col-md-12{% endif %}">
<div class="col-md-12">
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
@@ -536,6 +550,8 @@
</div>
{% endif %}
</div>
</div>
<div class="row">
{% if request.event.settings.invoice_address_asked or order.invoice_address %}
<div class="col-md-6">
<div class="panel panel-default">
@@ -588,25 +604,27 @@
</div>
</div>
{% endif %}
</div>
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Internal comment" %}
</h3>
</div>
<div class="panel-body">
<form class="form" method="post"
action="{% url "control:event.order.comment" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% csrf_token %}
<div class="row">
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
{% bootstrap_field comment_form.checkin_attention layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
<div class="{% if request.event.settings.invoice_address_asked or order.invoice_address %}col-md-6{% else %}col-md-12{% endif %}">
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Internal comment" %}
</h3>
</div>
<button class="btn btn-default">
{% trans "Update comment" %}
</button>
</form>
<div class="panel-body">
<form class="form" method="post"
action="{% url "control:event.order.comment" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% csrf_token %}
<div class="row">
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
{% bootstrap_field comment_form.checkin_attention layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
</div>
<button class="btn btn-default">
{% trans "Update comment" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@@ -18,17 +18,21 @@
<form method="post" class="form-horizontal" href="">
{% csrf_token %}
<p>{% blocktrans trimmed %}
Do you really want to mark this order as paid?
Do you really want to create a manual payment for this order?
{% endblocktrans %}</p>
<input type="hidden" name="status" value="p" />
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.amount layout='horizontal' %}
{% if form.force %}
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
{% endif %}
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% trans "Cancel" %}
</a>
<button class="btn btn-primary btn-save btn-lg" type="submit">
{% trans "Mark as paid" %}
{% trans "Create payment" %}
</button>
<div class="clearfix"></div>
</div>

View File

@@ -25,7 +25,7 @@
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='horizontal' %}
{% bootstrap_form e.form layout='control' %}
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>

View File

@@ -3,65 +3,6 @@
{% load bootstrap3 %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
{% if 'can_change_organizer_settings' in request.orgapermset %}
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
class="btn btn-default hidden-print">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
{% endif %}
{% if request.user.is_staff and staff_session %}
<a href="{% url "control:organizer.delete" organizer=organizer.slug %}"
class="btn btn-danger hidden-print">
<span class="fa fa-trash"></span>
</a>
{% endif %}
</h1>
<ul class="nav nav-pills hidden-print">
<li {% if "organizer" == url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer" organizer=organizer.slug %}">
{% trans "Events" %}
</a>
</li>
{% 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 "Teams" %}
</a>
</li>
{% endif %}
{% if 'can_change_organizer_settings' in request.orgapermset %}
<li {% if "organizer.display" in url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer.display" organizer=organizer.slug %}">
{% trans "Display" %}
</a>
</li>
{% endif %}
{% if 'can_change_organizer_settings' in request.orgapermset %}
<li {% if "organizer.device" in url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer.devices" organizer=organizer.slug %}">
{% trans "Devices" %}
</a>
</li>
{% endif %}
{% if 'can_change_organizer_settings' in request.orgapermset %}
<li {% if "organizer.webhook" in url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer.webhooks" organizer=organizer.slug %}">
{% trans "Webhooks" %}
</a>
</li>
{% endif %}
{% for nav in nav_organizer %}
<li {% if nav.active %}class="active"{% endif %}>
<a href="{{ nav.url }}">
{{ nav.label }}
</a>
</li>
{% endfor %}
</ul>
{% block inner %}
{% endblock %}

View File

@@ -2,6 +2,9 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
</h1>
{% if events|length == 0 %}
<p>
<em>{% trans "You currently do not have access to any events." %}</em>

View File

@@ -3,7 +3,7 @@
{% load staticfiles %}
{% load bootstrap3 %}
{% block inner %}
<legend>{% trans "Connect to device:" %} {{ device.name }}</legend>
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
<div>
<ol>

View File

@@ -3,9 +3,9 @@
{% load bootstrap3 %}
{% block inner %}
{% if device %}
<legend>{% trans "Device:" %} {{ device.name }}</legend>
<h1>{% trans "Device:" %} {{ device.name }}</h1>
{% else %}
<legend>{% trans "Connect a new device" %}</legend>
<h1>{% trans "Connect a new device" %}</h1>
{% endif %}
<form class="form-horizontal" action="" method="post">
{% if device %}

View File

@@ -2,20 +2,20 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h2>{% trans "Revoke device access:" %} {{ device.name }}</h2>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>
<strong>{% blocktrans %}Are you sure you want remove access for this device?{% endblocktrans %}</strong>
{% trans "All data of this device will stay available, but you can't use the device any more." %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug%}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Revoke" %}
</button>
</div>
</form>
<h1>{% trans "Revoke device access:" %} {{ device.name }}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>
<strong>{% blocktrans %}Are you sure you want remove access for this device?{% endblocktrans %}</strong>
{% trans "All data of this device will stay available, but you can't use the device any more." %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Revoke" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -2,9 +2,9 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<legend>
<h1>
{% trans "Connected devices" %}
</legend>
</h1>
<div class="alert alert-info">
{% blocktrans trimmed %}
This menu allows you to connect hardware devices such as box office terminals or scanning terminals to

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Display settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
@@ -22,6 +23,8 @@
{% endblocktrans %}
</p>
{% bootstrap_field form.primary_color layout="control" %}
{% bootstrap_field form.theme_color_success layout="control" %}
{% bootstrap_field form.theme_color_danger layout="control" %}
{% bootstrap_field form.primary_font layout="control" %}
</fieldset>
<div class="form-group submit-group">

View File

@@ -4,7 +4,16 @@
{% load formset_tags %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizer" %}</h1>
<h1>
{% blocktrans with name=organizer.name %}Organizer settings{% endblocktrans %}
{% if request.user.is_staff and staff_session %}
<a href="{% url "control:organizer.delete" organizer=organizer.slug %}"
class="btn btn-danger hidden-print">
<span class="fa fa-trash"></span>
</a>
{% endif %}
</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>

View File

@@ -2,7 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h2>{% trans "Delete team:" %} {{ team.name }}</h2>
<h1>{% trans "Delete team:" %} {{ team.name }}</h1>
{% if not possible %}
<p>{% blocktrans %}You cannot delete the team because there would be no one left who could change team permissions afterwards.{% endblocktrans %}</p>
<div class="form-group submit-group">

View File

@@ -3,9 +3,9 @@
{% load bootstrap3 %}
{% block inner %}
{% if team %}
<h2>{% trans "Team:" %} {{ team.name }}</h2>
<h1>{% trans "Team:" %} {{ team.name }}</h1>
{% else %}
<h2>{% trans "Create a new team" %}</h2>
<h1>{% trans "Create a new team" %}</h1>
<p>
{% blocktrans trimmed %}
You will be able to add team members in the next step.

View File

@@ -2,14 +2,14 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h2>
<h1>
{% 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>
</h1>
<h3>{% trans "Team members" %}</h3>
<form action="" method="post">
{% csrf_token %}

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Teams" %}</h1>
<p>
{% trans "The list below shows all teams that exist within this organizer." %}
</p>

View File

@@ -3,9 +3,9 @@
{% load bootstrap3 %}
{% block inner %}
{% if webhook %}
<legend>{% trans "Modify webhook" %}</legend>
<h1>{% trans "Modify webhook" %}</h1>
{% else %}
<legend>{% trans "Create a new webhook" %}</legend>
<h1>{% trans "Create a new webhook" %}</h1>
{% endif %}
<form class="form-horizontal" action="" method="post">
{% csrf_token %}

View File

@@ -2,7 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<legend>{% blocktrans with url=webhook.target_url %}Logs for webhook {{ url }}{% endblocktrans %}</legend>
<h1>{% blocktrans with url=webhook.target_url %}Logs for webhook {{ url }}{% endblocktrans %}</h1>
<p>
{% trans "This page shows all calls to your webhook in the past 30 days." %}
</p>

View File

@@ -2,9 +2,9 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<legend>
<h1>
{% trans "Webhooks" %}
</legend>
</h1>
<p>
{% blocktrans trimmed %}
This menu allows you to create webhooks to connect pretix to other online services.

View File

@@ -1,21 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Vouchers" %}{% endblock %}
{% block content %}
<h1>{% trans "Vouchers" %}</h1>
<ul class="nav nav-pills">
<li {% if "event.vouchers" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.vouchers' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "All vouchers" %}
</a>
</li>
<li {% if "event.vouchers.tags" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.vouchers.tags' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Tags" %}
</a>
</li>
</ul>
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -4,7 +4,7 @@
{% load bootstrap3 %}
{% block title %}{% trans "Voucher" %}{% endblock %}
{% block inside %}
<h1>{% trans "Create multiple voucher" %}</h1>
<h1>{% trans "Create multiple vouchers" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}

View File

@@ -1,9 +1,10 @@
{% extends "pretixcontrol/vouchers/base.html" %}
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% block title %}{% trans "Vouchers" %}{% endblock %}
{% block inside %}
{% block content %}
<h1>{% trans "Vouchers" %}</h1>
<p>
{% blocktrans trimmed %}
Vouchers allow you to assign tickets to specific persons for a lower price. They also enable you to

View File

@@ -1,6 +1,8 @@
{% extends "pretixcontrol/vouchers/base.html" %}
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block inside %}
{% block title %}{% trans "Voucher tags" %}{% endblock %}
{% block content %}
<h1>{% trans "Voucher tags" %}</h1>
<p>
{% blocktrans trimmed %}
If you add a "tag" to a voucher, you can here see statistics on their usage.

View File

@@ -96,6 +96,7 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
name='organizer.team.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/slugrng', main.SlugRNG.as_view(), name='events.add.slugrng'),
url(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
url(r'^events/$', main.EventList.as_view(), name='events'),
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
url(r'^events/typeahead/$', typeahead.event_list, name='events.typeahead'),
@@ -113,7 +114,6 @@ urlpatterns = [
url(r'^quickstart/$', event.QuickSetupView.as_view(), name='event.quick'),
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'),
url(r'^settings/payment/(?P<provider>[^/]+)$', event.PaymentProviderSettings.as_view(),
name='event.settings.payment.provider'),
url(r'^settings/payment$', event.PaymentSettings.as_view(), name='event.settings.payment'),

View File

@@ -167,8 +167,8 @@ def invite(request, token):
}
)
inv.delete()
messages.success(request, _('Welcome to pretix! You are now part of the team "{}".').format(inv.team.name))
return redirect('control:index')
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': inv.email})
ctx['form'] = form

View File

@@ -5,6 +5,7 @@ from datetime import timedelta
from decimal import Decimal
from urllib.parse import urlsplit
import bleach
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@@ -38,6 +39,7 @@ from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import markdown_compile
from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
@@ -46,7 +48,6 @@ from pretix.control.forms.event import (
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import nav_event_settings
from pretix.helpers.database import rolledback_transaction
from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import get_domain
@@ -60,12 +61,7 @@ from ..logdisplay import OVERVIEW_BLACKLIST
class EventSettingsViewMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['nav_event_settings'] = []
ctx['is_event_settings'] = True
for recv, retv in nav_event_settings.send(sender=self.request.event, request=self.request):
ctx['nav_event_settings'] += retv
ctx['nav_event_settings'].sort(key=lambda n: n['label'])
return ctx
@@ -620,7 +616,9 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
idx = matched.group('idx')
if idx in self.supported_locale:
with translation.override(self.supported_locale[idx]):
msgs[self.supported_locale[idx]] = v.format_map(self.placeholders(preview_item))
msgs[self.supported_locale[idx]] = bleach.linkify(markdown_compile(
v.format_map(self.placeholders(preview_item))
))
return JsonResponse({
'item': preview_item,

View File

@@ -834,7 +834,10 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
def plugin_forms(self):
forms = []
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
forms.append(resp)
if isinstance(resp, (list, tuple)):
forms.extend(resp)
else:
forms.append(resp)
return forms
def get_success_url(self) -> str:

View File

@@ -26,6 +26,7 @@ from django.views.generic import (
)
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedFile, CachedTicket, Invoice, InvoiceAddress,
@@ -54,8 +55,8 @@ from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.filter import EventOrderFilterForm, RefundFilterForm
from pretix.control.forms.orders import (
CommentForm, ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm,
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm, MarkPaidForm,
OrderContactForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
OrderPositionChangeForm, OrderRefundForm, OtherOperationsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -122,6 +123,12 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
ctx = super().get_context_data(**kwargs)
ctx['can_generate_invoice'] = invoice_qualified(self.order) and (
self.request.event.settings.invoice_generate in ('admin', 'user', 'paid', 'True')
) and (
not self.order.invoices.exists()
or (
self.order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and self.order.invoices.filter(is_cancellation=True).count() >= self.order.invoices.filter(is_cancellation=False).count()
)
)
return ctx
@@ -154,6 +161,7 @@ class OrderDetail(OrderView):
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
ctx['overpaid'] = self.order.pending_sum * -1
ctx['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel)
return ctx
def get_items(self):
@@ -396,7 +404,7 @@ class OrderPaymentConfirm(OrderView):
@cached_property
def mark_paid_form(self):
return MarkPaidForm(
return ConfirmPaymentForm(
instance=self.order,
data=self.request.POST if self.request.method == "POST" else None,
)
@@ -667,7 +675,7 @@ class OrderTransition(OrderView):
def post(self, *args, **kwargs):
to = self.request.POST.get('status', '')
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p' and self.mark_paid_form.is_valid():
ps = self.order.pending_sum
ps = self.mark_paid_form.cleaned_data['amount']
try:
p = self.order.payments.get(
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
@@ -710,7 +718,7 @@ class OrderTransition(OrderView):
messages.warning(self.request, _('The order has been marked as paid, but we were unable to send a '
'confirmation mail.'))
else:
messages.success(self.request, _('The order has been marked as paid.'))
messages.success(self.request, _('The payment has been created successfully.'))
elif self.order.cancel_allowed() and to == 'c':
cancel_order(self.order, user=self.request.user, send_mail=self.request.POST.get("send_email") == "on")
messages.success(self.request, _('The order has been canceled.'))
@@ -738,9 +746,13 @@ class OrderInvoiceCreate(OrderView):
permission = 'can_change_orders'
def post(self, *args, **kwargs):
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid') or not invoice_qualified(self.order):
has_inv = self.order.invoices.exists() and not (
self.order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and self.order.invoices.filter(is_cancellation=True).count() >= self.order.invoices.filter(is_cancellation=False).count()
)
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(self.order):
messages.error(self.request, _('You cannot generate an invoice for this order.'))
elif self.order.invoices.exists():
elif has_inv:
messages.error(self.request, _('An invoice for this order already exists.'))
else:
inv = generate_invoice(self.order)

View File

@@ -17,6 +17,55 @@ from pretix.helpers.daterange import daterange
from pretix.helpers.i18n import i18ncomp
def serialize_user(u):
return {
'id': u.pk,
'type': 'user',
'name': str(u),
'text': str(u),
'url': reverse('control:index')
}
def serialize_orga(o):
return {
'id': o.pk,
'slug': o.slug,
'type': 'organizer',
'name': str(o.name),
'text': str(o.name),
'url': reverse('control:organizer', kwargs={
'organizer': o.slug
})
}
def serialize_event(e):
dr = e.get_date_range_display()
if e.has_subevents:
if e.min_from is None:
dr = pgettext('subevent', 'No dates')
else:
tz = pytz.timezone(e.settings.timezone)
dr = _('Series:') + ' ' + daterange(
e.min_from.astimezone(tz),
(e.max_fromto or e.max_to or e.max_from).astimezone(tz)
)
return {
'id': e.pk,
'slug': e.slug,
'type': 'event',
'organizer': str(e.organizer.name),
'name': str(e.name),
'text': str(e.name),
'date_range': dr,
'url': reverse('control:event.index', kwargs={
'event': e.slug,
'organizer': e.organizer.slug
})
}
def event_list(request):
query = request.GET.get('query', '')
try:
@@ -35,37 +84,12 @@ def event_list(request):
order_from=Coalesce('min_from', 'date_from'),
).order_by('-order_from')
def serialize(e):
dr = e.get_date_range_display()
if e.has_subevents:
if e.min_from is None:
dr = pgettext('subevent', 'No dates')
else:
tz = pytz.timezone(e.settings.timezone)
dr = _('Series:') + ' ' + daterange(
e.min_from.astimezone(tz),
(e.max_fromto or e.max_to or e.max_from).astimezone(tz)
)
return {
'id': e.pk,
'slug': e.slug,
'organizer': str(e.organizer.name),
'name': str(e.name),
'text': str(e.name),
'date_range': dr,
'url': reverse('control:event.index', kwargs={
'event': e.slug,
'organizer': e.organizer.slug
})
}
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
doc = {
'results': [
serialize(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
],
'pagination': {
"more": total >= (offset + pagesize)
@@ -74,6 +98,55 @@ def event_list(request):
return JsonResponse(doc)
def nav_context_list(request):
query = request.GET.get('query', '')
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qs_events = request.user.get_events_with_any_permission(request).filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
).annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
max_to=Max('subevents__date_to'),
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
).annotate(
order_from=Coalesce('min_from', 'date_from'),
).order_by('-order_from')
if request.user.has_active_staff_session(request.session.session_key):
qs_orga = Organizer.objects.all()
else:
qs_orga = Organizer.objects.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
if query:
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
show_user = not query or (
query and request.user.email and query.lower() in request.user.email.lower()
) or (
query and request.user.fullname and query.lower() in request.user.fullname.lower()
)
total = qs_events.count() + qs_orga.count()
pagesize = 20
offset = (page - 1) * pagesize
results = ([
serialize_user(request.user)
] if show_user else []) + [
serialize_orga(e) for e in qs_orga[offset:offset + (pagesize if query else 5)]
] + [
serialize_event(e) for e in qs_events.select_related('organizer')[offset:offset + (pagesize if query else 5)]
]
doc = {
'results': results,
'pagination': {
"more": total >= (offset + pagesize)
}
}
return JsonResponse(doc)
@event_permission_required(None)
def subevent_select2(request, **kwargs):
query = request.GET.get('query', '')

View File

@@ -25,6 +25,7 @@ class DecimalTextInput(TextInput):
def change_decimal_field(field, currency):
places = settings.CURRENCY_PLACES.get(currency, 2)
field.decimal_places = places
field.localize = True
if isinstance(field.widget, NumberInput):
field.widget.attrs['step'] = str(Decimal('1') / 10 ** places).lower()
elif isinstance(field.widget, TextInput):

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-11-08 15:40+0000\n"
"POT-Creation-Date: 2018-12-05 16:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -50,33 +50,6 @@ msgstr ""
msgid "Total"
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:28
msgid ""
"Your request has been queued on the server and will now be processed. If "
"this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:41
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:54
#: pretix/static/pretixbase/js/asynctask.js:148
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:55
#: pretix/static/pretixbase/js/asynctask.js:156
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:39
#: pretix/static/pretixbase/js/asynctask.js:95
msgid ""
@@ -92,6 +65,12 @@ msgid ""
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:69
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
@@ -109,6 +88,17 @@ msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:148
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:156
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixcontrol/js/ui/main.js:28
msgid "Close message"
@@ -182,15 +172,29 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:252
#: pretix/static/pretixcontrol/js/ui/main.js:205
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:209
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:213
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:294
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:253
#: pretix/static/pretixcontrol/js/ui/main.js:295
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:546
#: pretix/static/pretixcontrol/js/ui/main.js:588
msgid "Use a different name internally"
msgstr ""
@@ -230,123 +234,123 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
#: pretix/static/pretixpresale/js/widget/widget.js:9
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:10
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:11
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:12
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:13
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "See variations"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-11-08 15:40+0000\n"
"POT-Creation-Date: 2018-12-05 16:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -49,33 +49,6 @@ msgstr ""
msgid "Total"
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:28
msgid ""
"Your request has been queued on the server and will now be processed. If "
"this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:41
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:54
#: pretix/static/pretixbase/js/asynctask.js:148
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asyncdownload.js:55
#: pretix/static/pretixbase/js/asynctask.js:156
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:39
#: pretix/static/pretixbase/js/asynctask.js:95
msgid ""
@@ -91,6 +64,12 @@ msgid ""
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:69
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
@@ -108,6 +87,17 @@ msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:148
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:156
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixcontrol/js/ui/main.js:28
msgid "Close message"
@@ -181,15 +171,29 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:252
#: pretix/static/pretixcontrol/js/ui/main.js:205
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:209
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:213
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:294
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:253
#: pretix/static/pretixcontrol/js/ui/main.js:295
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:546
#: pretix/static/pretixcontrol/js/ui/main.js:588
msgid "Use a different name internally"
msgstr ""
@@ -223,123 +227,123 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
#: pretix/static/pretixpresale/js/widget/widget.js:9
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:10
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:11
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:12
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:13
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "See variations"
msgstr ""

File diff suppressed because it is too large Load Diff

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