Compare commits

...

82 Commits

Author SHA1 Message Date
Raphael Michel
680e7cb0da Bump version to 1.0.0 2017-02-03 15:56:18 +01:00
Raphael Michel
498ac97bdd Squash migrations 2017-02-03 15:23:19 +01:00
Raphael Michel
3c75072868 Fix manual install docs and improve nginx config 2017-02-03 15:01:47 +01:00
Raphael Michel
8e09aa9b54 Remove redundant sudo in docs 2017-02-03 14:48:56 +01:00
Raphael Michel
c4fe2322a1 Update German translation 2017-02-03 14:09:25 +01:00
Raphael Michel
43abe14293 Fix #397 -- Fix problems with reminder email 2017-02-03 14:09:03 +01:00
Raphael Michel
7146d984d0 Move atomic transaction block 2017-02-03 13:11:24 +01:00
Raphael Michel
5495cd749a Fix #399 -- Optionally create invoices only after successful payment 2017-02-03 12:37:07 +01:00
Raphael Michel
ef93b8ae38 Invoice numbers and localized commata in CSV order export 2017-02-03 12:37:07 +01:00
Raphael Michel
2d370aaf5a Refresh order after PayPal call 2017-02-03 12:37:07 +01:00
jlwt90
3037309711 Fix #381 -- Add location field (#398)
* add location field

* rearrange the display order
2017-02-02 17:51:13 +01:00
Raphael Michel
01c3200258 Fix issues around required payment form fields 2017-02-01 19:04:08 +01:00
Raphael Michel
bf9987e05f Proper localization of Decimals 2017-02-01 18:46:04 +01:00
Raphael Michel
cd61c0833d More safety against duplicate PayPal payments 2017-02-01 18:40:27 +01:00
Raphael Michel
90bdb30449 Require activation before taking events live 2017-02-01 18:25:52 +01:00
Raphael Michel
92c2dcaf25 Improve type hint 2017-01-27 12:10:22 +01:00
Raphael Michel
a3cd8d151d Improve organizer page and tabs 2017-01-27 12:06:55 +01:00
Raphael Michel
ba455a3630 Add request to organizer_edit_tabs signal and actually send it 2017-01-27 11:08:52 +01:00
Raphael Michel
123f47ab39 Added tabs to the organizer page 2017-01-27 11:03:56 +01:00
Lukas Martini
d6503e3e48 Correct wrong module name for Postgres in example configs (#394) 2017-01-26 09:18:56 +01:00
Raphael Michel
4f4e5854f2 Fix log display 2017-01-25 14:32:18 +01:00
Raphael Michel
48461122f8 Set Reply-To headers to the event's contact address 2017-01-24 18:35:20 +01:00
Raphael Michel
aba7652aee Update django-statici18n to 1.3 2017-01-23 00:09:57 +01:00
Raphael Michel
78fc58cc93 Add setting to turn off automatic collapsing of variations 2017-01-22 20:31:59 +01:00
Raphael Michel
3a2ca8d3d6 Change semantics of validate_cart hook 2017-01-22 20:27:16 +01:00
Raphael Michel
fecc5ec307 Add support for restricted plugins 2017-01-22 18:15:31 +01:00
Raphael Michel
0665bd443b Add validate_cart hook 2017-01-22 18:05:47 +01:00
Raphael Michel
221526c979 Prevent duplicate payment confirmation mails 2017-01-22 17:46:56 +01:00
Raphael Michel
04369ff4f1 Fix word-wrapping in invoices 2017-01-22 17:28:56 +01:00
Raphael Michel
e71691d4a5 Allow superusers to access events that are not yet live 2017-01-21 19:10:46 +01:00
Raphael Michel
598e7c5637 Add invoice logo 2017-01-21 17:44:47 +01:00
Raphael Michel
ad73c0e05b Simplify dockerfile 2017-01-21 17:08:26 +01:00
Raphael Michel
d4573e8c25 Permission bypass for superusers 2017-01-21 14:29:56 +01:00
Raphael Michel
1d0def19b1 Add sepadebit to plugin list 2017-01-21 13:44:42 +01:00
Raphael Michel
80a1bcf033 Updated German translation 2017-01-21 13:43:03 +01:00
Raphael Michel
2f0cbcc565 Include provider-specific text in payment notification mail 2017-01-21 13:41:20 +01:00
Raphael Michel
48a2090e01 Make order codes organizer-unique 2017-01-21 13:40:49 +01:00
Jan Felix Wiebe
520b978ab0 Changed pagination of organizer page (#385)
* Changed pagination of organizer page

...to show more than one event per page. Maybe this value should become a configurable value in the future.

* Changed pagination to 30
2017-01-20 15:07:35 +01:00
Raphael Michel
ec42557305 Drop periodic_task call from rebuild command 2017-01-20 13:08:48 +01:00
Raphael Michel
bbb71ef891 New email placeholders invoice_name, invoice_company 2017-01-20 09:12:19 +01:00
Tobias Kunze
7300c60e73 Fix #380 -- Add missing translation strings (#382) 2017-01-19 11:03:06 +01:00
Raphael Michel
9f90ac80a0 Workaround to fix #378
See https://github.com/zyegfryed/django-statici18n/pull/29 for real fix
2017-01-18 10:02:53 +01:00
Raphael Michel
f2260212ee Update German translation 2017-01-18 10:02:53 +01:00
Raphael Michel
b94459c761 Don't show "binding order" for 0€ 2017-01-18 10:02:53 +01:00
Tobias Kunze
3e26a4d9cc Allow superusers to view any event (#377) 2017-01-17 16:54:13 +01:00
Raphael Michel
6bcfa4980f Fix accidental rename 2017-01-15 21:44:08 +01:00
Raphael Michel
829b6a7d56 Fix mistake in German translation 2017-01-15 21:37:18 +01:00
Raphael Michel
56bf3fe459 PayPal: Different types of webhooks 2017-01-15 21:28:54 +01:00
Jan Felix Wiebe
63bdb397e7 Added validation for voucher generation field (#373)
* Check voucher count input field before generating

* changed voucher-bulk-codes-num to numeric type

* Added Jan Felix to AUTHORS

* replaced alert by validation colors

* added another formatting reset

...so that it does not stay red if someone submits something valid
2017-01-15 15:42:15 +01:00
Raphael Michel
d4b3bf4370 Clearer error messages 2017-01-14 16:14:09 +01:00
Raphael Michel
7120e95d2a Fix #192 -- Disable bank import after last date of payments 2017-01-14 16:12:25 +01:00
Raphael Michel
f285390f46 Add pages to the list of plugins 2017-01-14 15:40:58 +01:00
Raphael Michel
559864dd01 Fix errors in install documentation 2017-01-14 13:31:37 +01:00
Raphael Michel
8021e1f269 Update German translation 2017-01-13 16:16:17 +01:00
Raphael Michel
53713acd9a Refs #356 -- Multi-page PDFs 2017-01-13 16:10:53 +01:00
Raphael Michel
5212f6b035 Fix copy-paste-error 2017-01-13 16:10:53 +01:00
Raphael Michel
ea807239b1 Fix #356 -- Download all tickets from an order 2017-01-13 16:10:53 +01:00
Raphael Michel
2ec534e32d Show all events to superuser 2017-01-12 16:44:41 +01:00
Raphael Michel
ec90efbf4a Allow signal receivers of footer_link to add multiple links 2017-01-09 20:23:00 +01:00
Raphael Michel
ffa35a9b9b Fix Django version in PyPI classifier 2017-01-09 20:20:02 +01:00
Raphael Michel
2a6629e075 Fix empty log in category form 2017-01-09 20:19:47 +01:00
Raphael Michel
59d440b213 Fix failure of cancel_order with Celery 4 2017-01-09 20:19:26 +01:00
Raphael Michel
e6a6043a7a Fix docs typo 2017-01-09 20:19:13 +01:00
Raphael Michel
ecb1eedcba Add nav_topbar signal 2017-01-08 00:07:19 +01:00
Raphael Michel
41c8ed2400 EventWizard do not require copying 2017-01-07 18:26:53 +01:00
Raphael Michel
b2cd633248 Update django-bootstrap3, bump version 2017-01-07 17:23:41 +01:00
Raphael Michel
0acee0e362 Get rid of User.givenname and User.familyname 2017-01-07 16:35:04 +01:00
Raphael Michel
33265d05fb Add event_live_issues signal 2017-01-07 14:41:53 +01:00
Raphael Michel
2182a4e361 Add improved UI if no event can be created 2017-01-07 14:25:01 +01:00
Raphael Michel
2336505309 Replace "Slug" with "Short form" for organizers 2017-01-07 14:24:38 +01:00
Raphael Michel
15b5e66da9 Add URL to permission test list 2017-01-07 14:13:14 +01:00
Raphael Michel
c7676cd17a Refs #39 -- Add permission editor for organizers 2017-01-07 14:10:31 +01:00
Raphael Michel
e53562dda2 Updated German translation 2017-01-07 13:14:21 +01:00
Raphael Michel
d134dcf6a9 Added team invitations 2017-01-07 13:05:36 +01:00
Raphael Michel
981d82b0ee Updated German translation 2017-01-07 11:39:55 +01:00
Raphael Michel
e75bce37bc Fix incorrect paths in Makefile 2017-01-05 13:08:40 +01:00
Raphael Michel
ef432252f0 Fix invalid URL usage 2017-01-05 12:20:58 +01:00
Raphael Michel
e9e743f312 Add more log texts 2017-01-05 12:20:18 +01:00
Raphael Michel
0998814e69 Improve session inheritation 2017-01-05 12:15:38 +01:00
Raphael Michel
d3f21353ca Allow to access not-yet-live shop on different domain 2017-01-05 12:11:50 +01:00
Raphael Michel
f6d8b825d5 Add plugin installation docs and rebuild command 2017-01-04 23:33:07 +01:00
Raphael Michel
4012658596 Add PyPI badges 2017-01-04 22:45:38 +01:00
109 changed files with 5333 additions and 1739 deletions

View File

@@ -21,3 +21,4 @@ an awesome project. Thank you all!
Tobias Kunze <rixx@cutebit.de>
Oliver Knapp <github@oliverknapp.de>
Vishal Sodani <vishalsodani@rediffmail.com>
Jan Felix Wiebe <git@jfwie.be>

View File

@@ -1,50 +1,45 @@
FROM debian:jessie
RUN apt-get update && apt-get install -y python3 git python3-pip \
libxml2-dev libxslt1-dev python-dev python-virtualenv locales libffi-dev \
build-essential python3-dev zlib1g-dev libssl-dev gettext \
libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \
aqbanking-tools supervisor nginx sudo \
--no-install-recommends
WORKDIR /
RUN dpkg-reconfigure locales && \
RUN apt-get update && \
apt-get install -y python3 git python3-pip \
libxml2-dev libxslt1-dev python-dev python-virtualenv locales libffi-dev \
build-essential python3-dev zlib1g-dev libssl-dev gettext \
libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \
aqbanking-tools supervisor nginx sudo \
--no-install-recommends && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
dpkg-reconfigure locales && \
locale-gen C.UTF-8 && \
/usr/sbin/update-locale LANG=C.UTF-8
ENV LC_ALL C.UTF-8
/usr/sbin/update-locale LANG=C.UTF-8 && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash -d /pretix -u 15371 pretixuser
RUN echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers
RUN mkdir /etc/pretix
RUN mkdir /data
VOLUME /etc/pretix
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
RUN chmod +x /usr/local/bin/pretix
COPY deployment/docker/supervisord.conf /etc/supervisord.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
RUN rm /etc/nginx/sites-enabled/default
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src
WORKDIR /pretix/src
ADD deployment/docker/production_settings.py /pretix/src/production_settings.py
ENV DJANGO_SETTINGS_MODULE production_settings
RUN pip3 install -U pip wheel setuptools
RUN pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
-r requirements/memcached.txt -r requirements/redis.txt \
-r requirements/py34.txt gunicorn
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
pip3 install -U pip wheel setuptools && \
cd /pretix/src && \
pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
-r requirements/memcached.txt -r requirements/redis.txt \
-r requirements/py34.txt gunicorn && \
mkdir -p data && \
chown -R pretixuser:pretixuser /static /pretix /data data && \
sudo -u pretixuser make production
RUN mkdir /static && chown -R pretixuser:pretixuser /static /pretix /data
USER pretixuser
RUN make production
VOLUME ["/etc/pretix", "/data"]
EXPOSE 80
ENTRYPOINT ["pretix"]
CMD ["all"]

View File

@@ -1,6 +1,9 @@
pretix
======
.. image:: https://img.shields.io/pypi/v/pretix.svg
:target: https://pypi.python.org/pypi/pretix
.. image:: https://readthedocs.org/projects/pretix/badge/?version=latest
:target: https://docs.pretix.eu/en/latest/
@@ -13,19 +16,23 @@ pretix
Reinventing ticket presales, one bit at a time.
Project status
--------------
Most features are present and sufficiently stable. pretix has been in use for multiple event and
sold a few thousand tickets so far. There is still a bunch of features to come and there surely is
still a bunch of bugs in there, but we consider it stable enough that we use it in production ourselves.
Project status & release cycle
------------------------------
If you deploy and use pretix, there will be a safe upgrade path for all changes to come. We're planning
on an 1.0 release in late 2016 or early 2017. Until then, we take the liberty of changing the code as we
like, but we try to keep the changes to documented APIs as small as possible. If you want to use pretix
in production or develop a plugin now, I invite you to send me an email so that I can notify you of changes
and bugs that require your attention.
While there is always a lot to do and improve on, pretix by now has been in use for more than a dozen
conferences that sold over ten thousand tickets combined without major problems. We therefore think of
pretix as being stable and ready to use.
Since very recently we now have an `installation guide`_ in our documentation.
If you want to use or extend pretix, we strongly recommend to follow our `blog`_. We will announce all
releases there. You can always find the latest stable version on PyPI or in the ``release/X.Y`` branch of
this repository. The ``master`` branch contains a development version that we also try to keep stable in
the sense that it does not break your data, but its APIs might change without prior notice.
To get started using pretix on your own server, look at the `installation guide`_ in our documentation.
This project is 100 percent free and open source software. If you are interested in commercial support,
hosting services or supporting this project financially, please go to `pretix.eu`_ or contact us at
support@pretix.eu.
Contributing
------------
@@ -40,10 +47,7 @@ See the LICENSE file for the complete license text.
This project is maintained by Raphael Michel <mail@raphaelmichel.de>. See the
AUTHORS file for a list of all the awesome folks who contributed to this project.
This project is 100 percent free and open source software. If you are interested in
commercial support, hosting services or supporting this project financially, please
go to `pretix.eu`_ or contact Raphael directly.
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html
.. _pretix.eu: https://pretix.eu
.. _blog: https://pretix.eu/about/en/blog/

View File

@@ -48,6 +48,14 @@ http {
expires 7d;
access_log off;
}
location ^~ /media/cachedfiles {
deny all;
return 404;
}
location ^~ /media/invoices {
deny all;
return 404;
}
location /static/ {
alias /static/;
access_log off;

View File

@@ -33,7 +33,7 @@ fi
if [ "$1" == "taskworker" ]; then
export C_FORCE_ROOT=True
exec celery -A pretix worker -l info
exec celery -A pretix.celery_app worker -l info
fi
if [ "$1" == "shell" ]; then

View File

@@ -1,6 +1,5 @@
from pretix.settings import *
LOGGING['handlers']['mail_admins']['include_html'] = True
STATIC_ROOT = '/static'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

View File

@@ -247,7 +247,7 @@ Sentry
------
pretix has native support for sentry, a tool that you can use to track errors in the
application. If you want to use sentry, you need to set a DSN in the configuration file.
application. If you want to use sentry, you need to set a DSN in the configuration file::
[sentry]
dsn=https://<key>:<secret>@sentry.io/<project>

View File

@@ -111,7 +111,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
datadir=/data
[database]
; Replace mysql with psycopg2 for PostgreSQL
; Replace mysql with postgresql_psycopg2 for PostgreSQL
backend=mysql
name=pretix
user=pretix
@@ -235,6 +235,26 @@ Updates are fairly simple, but require at least a short downtime::
Restarting the service can take a few seconds, especially if the update requires changes to the database.
Install a plugin
----------------
To install a plugin, you need to build your own docker image. To do so, create a new directory and place a file
named ``Dockerfile`` in it. The Dockerfile could look like this (replace ``pretix-passbook`` with the plugins of your
choice)::
FROM pretix/standalone
USER root
RUN pip3 install pretix-passbook
USER pretixuser
RUN make production
Then, go to that directory and build the image::
$ docker build -t mypretix
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/

View File

@@ -38,10 +38,10 @@ Unix user
As we do not want to run pretix as root, we first create a new unprivileged user::
# sudo adduser pretix --disabled-password --home /var/pretix
# adduser pretix --disabled-password --home /var/pretix
In this guide, all code lines prepended with a ``#`` symbol are commands that you need to execute on your server as
``root`` user; all lines prepended with a ``$`` symbol should be run by the unprivileged user.
``root`` user (e.g. using ``sudo``); all lines prepended with a ``$`` symbol should be run by the unprivileged user.
Database
--------
@@ -82,7 +82,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
datadir=/var/pretix/data
[database]
; Replace mysql with psycopg2 for PostgreSQL
; Replace mysql with postgresql_psycopg2 for PostgreSQL
backend=mysql
name=pretix
user=pretix
@@ -100,13 +100,13 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
sessions=true
[celery]
backend=redis://127.0.0.1?virtual_host=1
broker=redis://127.0.0.1?virtual_host=2
backend=redis://127.0.0.1/1
broker=redis://127.0.0.1/2
See :ref:`email configuration <mail-settings>` to learn more about configuring mail features.
Install pretix from source
--------------------------
Install pretix from PyPI
------------------------
Now we will install pretix itself. The following steps are to be executed as the ``pretix`` user. Before we
actually install pretix, we will create a virtual environment to isolate the python packages from your global
@@ -116,14 +116,13 @@ python installation::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
We now clone pretix and install its Python dependencies (replace ``mysql`` with ``postgres`` if you're running
PostgreSQL)::
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
command if you're running PostgreSQL::
(venv)$ git clone https://github.com/pretix/pretix.git /var/pretix/source
(venv)$ cd /var/pretix/source/src
(venv)$ pip3 install -r requirements.txt -r requirements/mysql.txt \
-r requirements/redis.txt \
-r requirements/py34.txt gunicorn
(venv)$ pip3 install "pretix[mysql]" gunicorn
If you are running Python 3.4, you also need to ``pip3 install typing``. This is not required on 3.5 or newer.
You can find out your Python version using ``python -V``.
We also need to create a data directory::
@@ -131,8 +130,8 @@ We also need to create a data directory::
Finally, we compile static files and translation data and create the database structure::
(venv)$ make production
(venv)$ python manage.py migrate
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
Start pretix as a service
@@ -154,7 +153,7 @@ named ``/etc/systemd/system/pretix-web.service`` with the following content::
--name pretix --workers 5 \
--max-requests 1200 --max-requests-jitter 50 \
--log-level=info --bind=127.0.0.1:8345
WorkingDirectory=/var/pretix/source/src
WorkingDirectory=/var/pretix
Restart=on-failure
[Install]
@@ -171,8 +170,8 @@ For background tasks we need a second service ``/etc/systemd/system/pretix-worke
Group=pretix
Environment="VIRTUAL_ENV=/var/pretix/venv"
Environment="PATH=/var/pretix/venv/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/var/pretix/venv/bin/celery -A pretix worker -l info
WorkingDirectory=/var/pretix/source/src
ExecStart=/var/pretix/venv/bin/celery -A pretix.celery_app worker -l info
WorkingDirectory=/var/pretix
Restart=on-failure
[Install]
@@ -191,7 +190,7 @@ Cronjob
You need to set up a cronjob that runs the management command ``runperiodic``. The exact interval is not important
but should be something between every minute and every hour. You could for example configure cron like this::
15,45 * * * * export PATH=/var/pretix/venv/bin:$PATH && cd /var/pretix/source/src && ./manage.py runperiodic
15,45 * * * * export PATH=/var/pretix/venv/bin:$PATH && cd /var/pretix/source/src && python -m pretix runperiodic
The cronjob should run as the ``pretix`` user (``crontab -e -u pretix``).
@@ -227,6 +226,15 @@ The following snippet is an example on how to configure a nginx proxy for pretix
access_log off;
}
location ^~ /media/cachedfiles {
deny all;
return 404;
}
location ^~ /media/invoices {
deny all;
return 404;
}
location /static/ {
alias /var/pretix/source/src/pretix/static.dist/;
access_log off;
@@ -254,16 +262,27 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
``mysql`` with ``postgres`` if necessary)::
$ source /var/pretix/venv/bin/activate
(venv)$ cd /var/pretix/source/src
(venv)$ git pull origin master
(venv)$ pip3 install -r requirements.txt -r requirements/mysql.txt \
-r requirements/redis.txt \
-r requirements/py34.txt gunicorn
(venv)$ python manage.py migrate
(venv)$ make production
(venv)$ python manage.py updatestyles
(venv)$ pip3 install -U pretix[mysql] gunicorn
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Install a plugin
----------------
To install a plugin, just use ``pip``! Depending on the plugin, you should probably apply database migrations and
rebuild the static files afterwards. Replace ``pretix-passbook`` with the plugin of your choice in the following
example::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install pretix-passbook
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
# systemctl restart pretix-web pretix-worker
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/

View File

@@ -11,7 +11,7 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task
:members: periodic_task, event_live_issues
Order events
""""""""""""
@@ -19,7 +19,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: order_paid, order_placed
:members: validate_cart, order_paid, order_placed
Frontend
--------
@@ -47,7 +47,7 @@ Backend
-------
.. automodule:: pretix.control.signals
:members: nav_event, html_head, quota_detail_html
:members: nav_event, html_head, quota_detail_html, nav_topbar, organizer_edit_tabs
.. automodule:: pretix.base.signals

View File

@@ -42,6 +42,13 @@ configuration class. The metadata class must define the following attributes:
``description`` (``str``):
A more verbose description of what your plugin does.
``visible`` (``bool``):
``True`` by default, can hide a plugin so it cannot be normally activated.
``restricted`` (``bool``):
``False`` by default, restricts a plugin such that it can only be enabled for an event
by system administrators / superusers.
A working example would be::
# file: pretix/plugins/timerestriction/__init__.py
@@ -57,6 +64,8 @@ A working example would be::
name = _("PayPal")
author = _("the pretix team")
version = '1.0.0'
visible = True
restricted = False
description = _("This plugin allows you to receive payments via PayPal")

View File

@@ -64,4 +64,6 @@ The output class
.. automethod:: generate
.. automethod:: generate_order
.. autoattribute:: download_button_text

View File

@@ -17,6 +17,8 @@ ways that pretix itself is:
The following plugins are not shipped with pretix but are maintained by the
same team:
* `SEPA direct debit`_
* `Pages`_
* `Passbook/Wallet ticket output`_
* `Cartshare`_
@@ -25,6 +27,8 @@ no statements about their stability:
* `esPass ticket output`_
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
.. _Cartshare: https://github.com/pretix/pretix-cartshare
.. _Pages: https://github.com/pretix/pretix-pages
.. _esPass ticket output: https://github.com/esPass/pretix-espass

View File

@@ -6,7 +6,7 @@ localecompile:
localegen:
./manage.py makemessages --all --ignore "pretix/helpers/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "static/jsi18n/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "build/*"
staticfiles: jsi18n
./manage.py collectstatic --noinput

View File

@@ -1 +1 @@
__version__ = "1.0.0b1"
__version__ = "1.0.0"

View File

@@ -18,3 +18,7 @@ class PretixBaseConfig(AppConfig):
default_app_config = 'pretix.base.PretixBaseConfig'
try:
import pretix.celery_app as celery # NOQA
except ImportError:
pass

View File

@@ -7,6 +7,7 @@ import pytz
from django import forms
from django.db.models import Sum
from django.dispatch import receiver
from django.utils.formats import localize
from django.utils.translation import ugettext as _
from pretix.base.models import InvoiceAddress, Order, OrderPosition
@@ -50,7 +51,7 @@ class OrderListExporter(BaseExporter):
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
qs = self.event.orders.all().select_related('invoice_address')
qs = self.event.orders.all().select_related('invoice_address').prefetch_related('invoices')
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
tax_rates = self._get_all_tax_rates(qs)
@@ -58,7 +59,7 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Payment date'), _('Payment type'), _('Payment method fee')
_('Payment date'), _('Payment type'), _('Payment method fee'), _('Invoice numbers')
]
for tr in tax_rates:
@@ -86,7 +87,7 @@ class OrderListExporter(BaseExporter):
for order in qs.order_by('datetime'):
row = [
order.code,
str(order.total),
localize(order.total),
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -107,7 +108,7 @@ class OrderListExporter(BaseExporter):
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
provider_names.get(order.payment_provider, order.payment_provider),
str(order.payment_fee)
localize(order.payment_fee)
]
for tr in tax_rates:
@@ -117,11 +118,12 @@ class OrderListExporter(BaseExporter):
taxrate_values['taxsum'] += order.payment_fee_tax_value
row += [
str(taxrate_values['grosssum']),
str(taxrate_values['grosssum'] - taxrate_values['taxsum']),
str(taxrate_values['taxsum']),
localize(taxrate_values['grosssum']),
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']),
localize(taxrate_values['taxsum']),
]
row.append(', '.join([i.number for i in order.invoices.all()]))
writer.writerow(row)
return 'orders.csv', 'text/csv', output.getvalue().encode("utf-8")

View File

@@ -39,8 +39,7 @@ class UserSettingsForm(forms.ModelForm):
class Meta:
model = User
fields = [
'givenname',
'familyname',
'fullname',
'locale',
# 'timezone',
'email'

View File

@@ -0,0 +1,12 @@
from django.core.management import call_command
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Rebuild static files and language files"
def handle(self, *args, **options):
call_command('compilemessages', verbosity=1, interactive=False)
call_command('compilejsi18n', verbosity=1, interactive=False)
call_command('collectstatic', verbosity=1, interactive=False)
call_command('compress', verbosity=1, interactive=False)

View File

@@ -0,0 +1,658 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-03 14:20
from __future__ import unicode_literals
import datetime
import uuid
from decimal import Decimal
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations, models
import pretix.base.i18n
import pretix.base.models.base
import pretix.base.models.invoices
import pretix.base.models.items
import pretix.base.models.orders
import pretix.base.models.vouchers
def initial_user(apps, schema_editor):
User = apps.get_model("pretixbase", "User")
user = User(email='admin@localhost')
user.is_staff = True
user.is_superuser = True
user.password = make_password('admin')
user.save()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0001_initial'), ('pretixbase', '0002_auto_20160209_0940'), ('pretixbase', '0003_eventpermission_can_change_vouchers'), ('pretixbase', '0004_auto_20160209_1023'), ('pretixbase', '0005_auto_20160211_1459'), ('pretixbase', '0006_auto_20160211_1630'), ('pretixbase', '0007_auto_20160211_1710'), ('pretixbase', '0008_invoiceaddress'), ('pretixbase', '0009_auto_20160222_2002'), ('pretixbase', '0010_orderposition_secret'), ('pretixbase', '0011_auto_20160311_2052'), ('pretixbase', '0012_auto_20160312_1040'), ('pretixbase', '0013_invoice_locale'), ('pretixbase', '0014_invoice_additional_text'), ('pretixbase', '0015_auto_20160312_1924'), ('pretixbase', '0016_voucher_variation'), ('pretixbase', '0017_auto_20160324_1615'), ('pretixbase', '0018_auto_20160326_1104'), ('pretixbase', '0019_auto_20160326_1139'), ('pretixbase', '0020_auto_20160418_2106'), ('pretixbase', '0021_auto_20160418_2117'), ('pretixbase', '0020_auto_20160421_1943'), ('pretixbase', '0022_merge'), ('pretixbase', '0023_auto_20160601_1039'), ('pretixbase', '0024_auto_20160728_1725'), ('pretixbase', '0025_auto_20160802_2202'), ('pretixbase', '0026_order_comment'), ('pretixbase', '0027_auto_20160815_1254'), ('pretixbase', '0028_auto_20160816_1242')]
initial = True
dependencies = [
('auth', '0006_require_contenttypes_0002'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('is_staff', models.BooleanField(default=False, verbose_name='Is site admin')),
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='Date joined')),
('locale', models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)')], default='en', max_length=50, verbose_name='Language')),
('timezone', models.CharField(default='UTC', max_length=100, verbose_name='Timezone')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
},
),
migrations.RunPython(
code=initial_user,
),
migrations.CreateModel(
name='CachedFile',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('expires', models.DateTimeField(blank=True, null=True)),
('date', models.DateTimeField(blank=True, null=True)),
('filename', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.base.cachedfile_name)),
],
),
migrations.CreateModel(
name='CachedTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(max_length=255)),
('cachedfile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.CachedFile')),
],
),
migrations.CreateModel(
name='CartPosition',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
('voucher_discount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
('base_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('cart_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)')),
('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
('expires', models.DateTimeField(verbose_name='Expiration date')),
],
options={
'verbose_name': 'Cart position',
'verbose_name_plural': 'Cart positions',
},
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', pretix.base.i18n.I18nCharField(max_length=200, verbose_name='Name')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
('is_public', models.BooleanField(default=False, help_text="If selected, this event may show up on the ticket system's start page or an organization profile.", verbose_name='Visible in public lists')),
('presale_end', models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True, verbose_name='End of presale')),
('presale_start', models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True, verbose_name='Start of presale')),
('plugins', models.TextField(blank=True, null=True, verbose_name='Plugins')),
],
options={
'verbose_name': 'Event',
'ordering': ('date_from', 'name'),
'verbose_name_plural': 'Events',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='EventLock',
fields=[
('event', models.CharField(max_length=36, primary_key=True, serialize=False)),
('date', models.DateTimeField(auto_now=True)),
('token', models.UUIDField(default=uuid.uuid4)),
],
),
migrations.CreateModel(
name='EventPermission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('can_change_settings', models.BooleanField(default=True, verbose_name='Can change event settings')),
('can_change_items', models.BooleanField(default=True, verbose_name='Can change product settings')),
('can_view_orders', models.BooleanField(default=True, verbose_name='Can view orders')),
('can_change_permissions', models.BooleanField(default=True, verbose_name='Can change permissions')),
('can_change_orders', models.BooleanField(default=True, verbose_name='Can change orders')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_perms', to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL)),
('can_change_vouchers', models.BooleanField(default=True, verbose_name='Can change vouchers')),
],
options={
'verbose_name': 'Event permission',
'verbose_name_plural': 'Event permissions',
},
),
migrations.CreateModel(
name='EventSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=255)),
('value', models.TextField()),
('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='setting_objects', to='pretixbase.Event')),
],
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Item name')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('description', pretix.base.i18n.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')),
('default_price', models.DecimalField(decimal_places=2, max_digits=7, null=True, verbose_name='Default price')),
('tax_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Taxes included in percent')),
('admission', models.BooleanField(default=False, help_text='Whether or not buying this product allows a person to enter your event', verbose_name='Is an admission ticket')),
('position', models.IntegerField(default=0)),
('picture', models.ImageField(blank=True, null=True, upload_to=pretix.base.models.items.itempicture_upload_to, verbose_name='Product picture')),
('available_from', models.DateTimeField(blank=True, help_text='This product will not be sold before the given date.', null=True, verbose_name='Available from')),
('available_until', models.DateTimeField(blank=True, help_text='This product will not be sold after the given date.', null=True, verbose_name='Available until')),
],
options={
'verbose_name': 'Product',
'ordering': ('category__position', 'category', 'position'),
'verbose_name_plural': 'Products',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='ItemCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Category name')),
('position', models.IntegerField(default=0)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='pretixbase.Event')),
('description', pretix.base.i18n.I18nTextField(blank=True, verbose_name='Category description')),
],
options={
'verbose_name': 'Product category',
'ordering': ('position', 'id'),
'verbose_name_plural': 'Product categories',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='ItemVariation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Description')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('position', models.PositiveIntegerField(default=0, verbose_name='Position')),
('default_price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Default price')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variations', to='pretixbase.Item')),
],
options={
'verbose_name': 'Product variation',
'ordering': ('position', 'id'),
'verbose_name_plural': 'Product variations',
},
),
migrations.CreateModel(
name='LogEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('datetime', models.DateTimeField(auto_now_add=True)),
('action_type', models.CharField(max_length=255)),
('data', models.TextField(default='{}')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=16, verbose_name='Order code')),
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
('datetime', models.DateTimeField(verbose_name='Date')),
('expires', models.DateTimeField(verbose_name='Expiration date')),
('payment_date', models.DateTimeField(blank=True, null=True, verbose_name='Payment date')),
('payment_provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')),
('payment_fee', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee')),
('payment_info', models.TextField(blank=True, null=True, verbose_name='Payment information')),
('payment_manual', models.BooleanField(default=False, verbose_name='Payment state was manually modified')),
('total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Total amount')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='pretixbase.Event', verbose_name='Event')),
],
options={
'verbose_name': 'Order',
'ordering': ('-datetime',),
'verbose_name_plural': 'Orders',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='OrderPosition',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
('voucher_discount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
('base_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item', verbose_name='Item')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='positions', to='pretixbase.Order', verbose_name='Order')),
('variation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation')),
],
options={
'verbose_name': 'Order position',
'verbose_name_plural': 'Order positions',
},
),
migrations.CreateModel(
name='Organizer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
],
options={
'verbose_name': 'Organizer',
'ordering': ('name',),
'verbose_name_plural': 'Organizers',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='OrganizerPermission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('can_create_events', models.BooleanField(default=True, verbose_name='Can create events')),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_perms', to='pretixbase.Organizer')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Organizer permission',
'verbose_name_plural': 'Organizer permissions',
},
),
migrations.CreateModel(
name='OrganizerSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=255)),
('value', models.TextField()),
('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='setting_objects', to='pretixbase.Organizer')),
],
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', pretix.base.i18n.I18nTextField(verbose_name='Question')),
('type', models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], max_length=5, verbose_name='Question type')),
('required', models.BooleanField(default=False, verbose_name='Required question')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='pretixbase.Event')),
('items', models.ManyToManyField(blank=True, help_text='This question will be asked to buyers of the selected products', related_name='questions', to='pretixbase.Item', verbose_name='Products')),
],
options={
'verbose_name': 'Question',
'verbose_name_plural': 'Questions',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='QuestionAnswer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('answer', models.TextField()),
('cartposition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.CartPosition')),
('orderposition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.OrderPosition')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.Question')),
],
),
migrations.CreateModel(
name='Quota',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('size', models.PositiveIntegerField(blank=True, help_text='Leave empty for an unlimited number of tickets.', null=True, verbose_name='Total capacity')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.Event', verbose_name='Event')),
('items', models.ManyToManyField(blank=True, related_name='quotas', to='pretixbase.Item', verbose_name='Item')),
('variations', models.ManyToManyField(blank=True, related_name='quotas', to='pretixbase.ItemVariation', verbose_name='Variations')),
],
options={
'verbose_name': 'Quota',
'verbose_name_plural': 'Quotas',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='Voucher',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=255, verbose_name='Voucher code')),
('valid_until', models.DateTimeField(blank=True, null=True, verbose_name='Valid until')),
('block_quota', models.BooleanField(default=False, help_text="If activated, this voucher will be substracted from the affected product's quotas, such that it is guaranteed that anyone with this voucher code does receive a ticket.", verbose_name='Reserve ticket from quota')),
('allow_ignore_quota', models.BooleanField(default=False, help_text='If activated, a holder of this voucher code can buy tickets, even if there are none left.', verbose_name='Allow to bypass quota')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Set product price to')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Event', verbose_name='Event')),
],
options={
'verbose_name': 'Voucher',
'verbose_name_plural': 'Vouchers',
},
),
migrations.AddField(
model_name='organizer',
name='permitted',
field=models.ManyToManyField(related_name='organizers', through='pretixbase.OrganizerPermission', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='orderposition',
name='voucher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher'),
),
migrations.AddField(
model_name='item',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.ItemCategory', verbose_name='Category'),
),
migrations.AddField(
model_name='item',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.Event', verbose_name='Event'),
),
migrations.AddField(
model_name='event',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='pretixbase.Organizer'),
),
migrations.AddField(
model_name='event',
name='permitted',
field=models.ManyToManyField(related_name='events', through='pretixbase.EventPermission', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='cartposition',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event', verbose_name='Event'),
),
migrations.AddField(
model_name='cartposition',
name='item',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item', verbose_name='Item'),
),
migrations.AddField(
model_name='cartposition',
name='variation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation'),
),
migrations.AddField(
model_name='cartposition',
name='voucher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher'),
),
migrations.AddField(
model_name='cachedticket',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order'),
),
migrations.AddField(
model_name='voucher',
name='item',
field=models.ForeignKey(blank=True, help_text="This product is added to the user's cart if the voucher is redeemed.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Item', verbose_name='Product'),
),
migrations.AlterField(
model_name='voucher',
name='price',
field=models.DecimalField(blank=True, decimal_places=2, help_text='If empty, the product will cost its normal price.', max_digits=10, null=True, verbose_name='Set product price to'),
),
migrations.AddField(
model_name='voucher',
name='redeemed',
field=models.BooleanField(default=False, verbose_name='Redeemed'),
),
migrations.AlterUniqueTogether(
name='voucher',
unique_together=set([('event', 'code')]),
),
migrations.AlterField(
model_name='voucher',
name='code',
field=models.CharField(default=pretix.base.models.vouchers.generate_code, max_length=255, verbose_name='Voucher code'),
),
migrations.AddField(
model_name='orderposition',
name='tax_rate',
field=models.DecimalField(decimal_places=2, default=0, max_digits=7, verbose_name='Tax rate'),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='tax_value',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Tax value'),
preserve_default=False,
),
migrations.AlterField(
model_name='item',
name='tax_rate',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=7, verbose_name='Taxes included in percent'),
),
migrations.CreateModel(
name='InvoiceAddress',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_modified', models.DateTimeField(auto_now=True)),
('company', models.CharField(blank=True, max_length=255, verbose_name='Company name')),
('name', models.CharField(blank=True, max_length=255, verbose_name='Full name')),
('street', models.TextField(verbose_name='Address')),
('zipcode', models.CharField(max_length=30, verbose_name='ZIP code')),
('city', models.CharField(max_length=255, verbose_name='City')),
('country', models.CharField(max_length=255, verbose_name='Country')),
('vat_id', models.CharField(blank=True, max_length=255, verbose_name='VAT ID')),
('order', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoice_address', to='pretixbase.Order')),
],
),
migrations.AddField(
model_name='event',
name='live',
field=models.BooleanField(default=False, verbose_name='Shop is live'),
),
migrations.AddField(
model_name='orderposition',
name='secret',
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_position_secret, max_length=64),
),
migrations.AddField(
model_name='order',
name='payment_fee_tax_rate',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee tax rate'),
preserve_default=False,
),
migrations.AddField(
model_name='order',
name='payment_fee_tax_value',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee tax'),
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invoice_no', models.PositiveIntegerField(db_index=True)),
('is_cancelled', models.BooleanField(default=False)),
('invoice_from', models.TextField()),
('invoice_to', models.TextField()),
('date', models.DateField(default=datetime.date.today)),
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.invoices.invoice_filename)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pretixbase.Event')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pretixbase.Order')),
],
),
migrations.CreateModel(
name='InvoiceLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField()),
('gross_value', models.DecimalField(decimal_places=2, max_digits=10)),
('tax_value', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=7)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='pretixbase.Invoice')),
],
),
migrations.AddField(
model_name='invoice',
name='locale',
field=models.CharField(default='en', max_length=50),
),
migrations.AddField(
model_name='invoice',
name='additional_text',
field=models.TextField(blank=True),
),
migrations.RemoveField(
model_name='invoice',
name='is_cancelled',
),
migrations.AddField(
model_name='invoice',
name='is_cancellation',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='invoice',
name='refers',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='refered', to='pretixbase.Invoice'),
),
migrations.AlterUniqueTogether(
name='invoice',
unique_together=set([('event', 'invoice_no')]),
),
migrations.AddField(
model_name='voucher',
name='variation',
field=models.ForeignKey(blank=True, help_text='This variation of the product select above is being used.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.ItemVariation', verbose_name='Product variation'),
),
migrations.AlterModelOptions(
name='logentry',
options={'ordering': ('-datetime',)},
),
migrations.AddField(
model_name='item',
name='free_price',
field=models.BooleanField(default=False, help_text='If this option is active, your users can choose the price themselves. The price configured above is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect additional donations for your event.', verbose_name='Free price input'),
),
migrations.CreateModel(
name='QuestionOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('answer', pretix.base.i18n.I18nCharField(verbose_name='Answer')),
],
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list')], max_length=5, verbose_name='Question type'),
),
migrations.AddField(
model_name='questionoption',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='pretixbase.Question'),
),
migrations.AddField(
model_name='questionanswer',
name='options',
field=models.ManyToManyField(blank=True, related_name='answers', to='pretixbase.QuestionOption'),
),
migrations.AddField(
model_name='voucher',
name='quota',
field=models.ForeignKey(blank=True, help_text='If enabled, the voucher is valid for any product affected by this quota.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quota', to='pretixbase.Quota', verbose_name='Quota'),
),
migrations.AddField(
model_name='voucher',
name='comment',
field=models.TextField(blank=True, help_text='The text entered in this field will not be visible to the user and is available for your convenience.', verbose_name='Comment'),
),
migrations.AddField(
model_name='voucher',
name='tag',
field=models.CharField(blank=True, db_index=True, help_text='You can use this field to group multiple vouchers together. If you enter the same value for multiple vouchers, you can get statistics on how many of them have been redeemed etc.', max_length=255, verbose_name='Tag'),
),
migrations.AddField(
model_name='item',
name='hide_without_voucher',
field=models.BooleanField(default=False, help_text='This product will be hidden from the event page until the user enters a voucher code that is specifically tied to this product (and not via a quota).', verbose_name='This product will only be shown if a voucher matching the product is redeemed.'),
),
migrations.AddField(
model_name='item',
name='require_voucher',
field=models.BooleanField(default=False, help_text='To buy this product, the user needs a voucher that applies to this product either directly or via a quota.', verbose_name='This product can only be bought using a voucher.'),
),
migrations.AlterField(
model_name='logentry',
name='datetime',
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name='logentry',
name='object_id',
field=models.PositiveIntegerField(db_index=True),
),
migrations.AlterField(
model_name='order',
name='code',
field=models.CharField(db_index=True, max_length=16, verbose_name='Order code'),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], db_index=True, max_length=3, verbose_name='Status'),
),
migrations.AlterField(
model_name='voucher',
name='code',
field=models.CharField(db_index=True, default=pretix.base.models.vouchers.generate_code, max_length=255, verbose_name='Voucher code'),
),
migrations.AddField(
model_name='order',
name='comment',
field=models.TextField(blank=True, help_text='The text entered in this field will not be visible to the user and is available for your convenience.', verbose_name='Comment'),
),
migrations.RemoveField(
model_name='cartposition',
name='base_price',
),
migrations.RemoveField(
model_name='cartposition',
name='voucher_discount',
),
migrations.RemoveField(
model_name='orderposition',
name='base_price',
),
migrations.RemoveField(
model_name='orderposition',
name='voucher_discount',
),
]

View File

@@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-03 14:21
from __future__ import unicode_literals
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.validators
def preserve_event_settings(apps, schema_editor):
Event = apps.get_model('pretixbase', 'Event')
EventSetting = apps.get_model('pretixbase', 'EventSetting')
for e in Event.objects.all():
EventSetting.objects.create(object=e, key='mail_days_order_expire_warning', value='0')
def forwards42(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order')
EventSetting = apps.get_model('pretixbase', 'EventSetting')
etz = {
s['object_id']: s['value']
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
}
for order in Order.objects.all():
tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save()
def forwards44(apps, schema_editor):
CachedTicket = apps.get_model('pretixbase', 'CachedTicket')
CachedTicket.objects.all().delete()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0031_auto_20160816_0648'), ('pretixbase', '0032_question_position'), ('pretixbase', '0033_auto_20160821_2222'), ('pretixbase', '0034_auto_20160830_1952'), ('pretixbase', '0032_item_allow_cancel'), ('pretixbase', '0033_auto_20160822_1044'), ('pretixbase', '0035_merge'), ('pretixbase', '0036_auto_20160902_0755'), ('pretixbase', '0037_invoice_payment_provider_text'), ('pretixbase', '0038_auto_20160924_1448'), ('pretixbase', '0039_user_require_2fa'), ('pretixbase', '0040_u2fdevice'), ('pretixbase', '0041_auto_20161018_1654'), ('pretixbase', '0042_order_expires'), ('pretixbase', '0043_globalsetting'), ('pretixbase', '0044_auto_20161101_1610'), ('pretixbase', '0045_auto_20161108_1542'), ('pretixbase', '0046_order_meta_info'), ('pretixbase', '0047_auto_20161126_1300'), ('pretixbase', '0048_auto_20161129_1330')]
dependencies = [
('pretixbase', '0030_auto_20160816_0646'),
]
operations = [
migrations.RenameField(
model_name='invoice',
old_name='invoice_no_charfield',
new_name='invoice_no',
),
migrations.AddField(
model_name='invoice',
name='footer_text',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='invoice',
name='introductory_text',
field=models.TextField(blank=True),
),
migrations.AlterUniqueTogether(
name='invoice',
unique_together=set([('event', 'invoice_no')]),
),
migrations.AddField(
model_name='question',
name='position',
field=models.IntegerField(default=0),
),
migrations.AlterModelOptions(
name='question',
options={'ordering': ('position', 'id'), 'verbose_name': 'Question', 'verbose_name_plural': 'Questions'},
),
migrations.AddField(
model_name='item',
name='allow_cancel',
field=models.BooleanField(default=True, help_text='If you deactivate this, an order including this product might not be cancelled by the user. It may still be cancelled by you.', verbose_name='Allow product to be cancelled'),
),
migrations.AddField(
model_name='order',
name='expiry_reminder_sent',
field=models.BooleanField(default=False),
),
migrations.RunPython(
code=preserve_event_settings,
),
migrations.AddField(
model_name='invoice',
name='payment_provider_text',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='eventpermission',
name='can_view_vouchers',
field=models.BooleanField(default=True, verbose_name='Can view vouchers'),
),
migrations.AlterField(
model_name='item',
name='allow_cancel',
field=models.BooleanField(default=True, help_text='If you deactivate this, an order including this product might not be canceled by the user. It may still be canceled by you.', verbose_name='Allow product to be canceled'),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'canceled'), ('r', 'refunded')], db_index=True, max_length=3, verbose_name='Status'),
),
migrations.AddField(
model_name='user',
name='require_2fa',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='U2FDevice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='The human-readable name of this device.', max_length=64)),
('confirmed', models.BooleanField(default=True, help_text='Is this device ready for use?')),
('json_data', models.TextField()),
('user', models.ForeignKey(help_text='The user that this device belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AlterField(
model_name='cachedticket',
name='cachedfile',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.CachedFile'),
),
migrations.RunPython(
code=forwards42,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.CreateModel(
name='GlobalSetting',
fields=[
('key', models.CharField(max_length=255, primary_key=True, serialize=False)),
('value', models.TextField()),
],
),
migrations.RunPython(
code=forwards44,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='cachedticket',
name='order',
),
migrations.AddField(
model_name='cachedticket',
name='order_position',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.OrderPosition'),
preserve_default=False,
),
migrations.AlterField(
model_name='cartposition',
name='expires',
field=models.DateTimeField(db_index=True, verbose_name='Expiration date'),
),
migrations.AlterField(
model_name='voucher',
name='redeemed',
field=models.BooleanField(db_index=True, default=False, verbose_name='Redeemed'),
),
migrations.AlterField(
model_name='voucher',
name='valid_until',
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Valid until'),
),
migrations.AddField(
model_name='order',
name='meta_info',
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
),
migrations.AddField(
model_name='voucher',
name='max_usages',
field=models.PositiveIntegerField(default=1, help_text='Number of times this voucher can be redeemed.', verbose_name='Maximum usages'),
),
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',
name='redeemed',
field=models.PositiveIntegerField(default=0, verbose_name='Redeemed'),
),
migrations.AddField(
model_name='voucher',
name='price_mode',
field=models.CharField(choices=[('none', 'No effect'), ('set', 'Set product price to'), ('subtract', 'Subtract from product price'), ('percent', 'Reduce product price by (%)')], default='set', max_length=100, verbose_name='Price mode'),
),
migrations.RenameField(
model_name='voucher',
old_name='price',
new_name='value',
),
migrations.AlterField(
model_name='voucher',
name='value',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Voucher value'),
),
]

View File

@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-03 14:12
from __future__ import unicode_literals
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import pretix.base.i18n
import pretix.base.models.event
import pretix.base.models.orders
import pretix.base.models.organizer
import pretix.base.validators
def forwards50(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order')
for o in Order.objects.all():
for i, p in enumerate(o.positions.all()):
p.positionid = i + 1
p.save()
def invalidate_ticket_cache(apps, schema_editor):
CachedTicket = apps.get_model('pretixbase', 'CachedTicket')
for ct in CachedTicket.objects.all():
try:
if ct.cachedfile:
ct.cachedfile.delete()
if ct.cachedfile.file:
ct.cachedfile.file.delete(False)
except models.Model.DoesNotExist:
pass
ct.delete()
def merge_names(apps, schema_editor):
User = apps.get_model('pretixbase', 'User')
for u in User.objects.all():
if u.givenname:
if u.familyname:
u.fullname = u.givenname + " " + u.familyname
else:
u.fullname = u.givenname
elif u.familyname:
u.fullname = u.familyname
u.save()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0050_orderposition_positionid'), ('pretixbase', '0051_auto_20161221_1720'), ('pretixbase', '0052_auto_20161231_1533'), ('pretixbase', '0053_auto_20170104_1252'), ('pretixbase', '0054_auto_20170107_1058'), ('pretixbase', '0055_organizerpermission_can_change_permissions'), ('pretixbase', '0056_auto_20170107_1251'), ('pretixbase', '0057_auto_20170107_1531'), ('pretixbase', '0058_auto_20170107_1533'), ('pretixbase', '0059_cachedcombinedticket'), ('pretixbase', '0060_auto_20170113_1438'), ('pretixbase', '0061_event_location')]
dependencies = [
('pretixbase', '0049_checkin'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='positionid',
field=models.PositiveIntegerField(default=1),
),
migrations.RunPython(
code=forwards50,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RunPython(
code=invalidate_ticket_cache,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='cachedticket',
name='cachedfile',
),
migrations.AddField(
model_name='cachedticket',
name='extension',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='cachedticket',
name='file',
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedticket_name),
),
migrations.AddField(
model_name='cachedticket',
name='type',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='checkin',
name='datetime',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AlterField(
model_name='checkin',
name='position',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to='pretixbase.OrderPosition'),
),
migrations.AlterField(
model_name='voucher',
name='price_mode',
field=models.CharField(choices=[('none', 'No effect'), ('set', 'Set product price to'), ('subtract', 'Subtract from product price'), ('percent', 'Reduce product price by (%)')], default='none', max_length=100, verbose_name='Price mode'),
),
migrations.CreateModel(
name='RequiredAction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True, db_index=True)),
('done', models.BooleanField(default=False)),
('action_type', models.CharField(max_length=255)),
('data', models.TextField(default='{}')),
],
options={
'ordering': ('datetime',),
},
),
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event'),
),
migrations.AddField(
model_name='requiredaction',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='eventpermission',
name='invite_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='eventpermission',
name='invite_token',
field=models.CharField(blank=True, default=pretix.base.models.event.generate_invite_token, max_length=64, null=True),
),
migrations.AlterField(
model_name='eventpermission',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='organizerpermission',
name='can_change_permissions',
field=models.BooleanField(default=True, verbose_name='Can change permissions'),
),
migrations.AddField(
model_name='organizerpermission',
name='invite_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='organizerpermission',
name='invite_token',
field=models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True),
),
migrations.AlterField(
model_name='organizerpermission',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='user',
name='fullname',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Full name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.RunPython(
code=merge_names,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='user',
name='familyname',
),
migrations.RemoveField(
model_name='user',
name='givenname',
),
migrations.CreateModel(
name='CachedCombinedTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('extension', models.CharField(max_length=255)),
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')),
('created', models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now)),
],
),
migrations.AddField(
model_name='cachedticket',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='event',
name='location',
field=pretix.base.i18n.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'),
),
]

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 10:58
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.event
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0053_auto_20170104_1252'),
]
operations = [
migrations.AddField(
model_name='eventpermission',
name='invite_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='eventpermission',
name='invite_token',
field=models.CharField(blank=True, default=pretix.base.models.event.generate_invite_token, max_length=64, null=True),
),
migrations.AlterField(
model_name='eventpermission',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 12:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0054_auto_20170107_1058'),
]
operations = [
migrations.AddField(
model_name='organizerpermission',
name='can_change_permissions',
field=models.BooleanField(default=True, verbose_name='Can change permissions'),
),
]

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 12:51
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.organizer
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0055_organizerpermission_can_change_permissions'),
]
operations = [
migrations.AddField(
model_name='organizerpermission',
name='invite_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='organizerpermission',
name='invite_token',
field=models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True),
),
migrations.AlterField(
model_name='organizerpermission',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 15:31
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import pretix.base.validators
def merge_names(apps, schema_editor):
User = apps.get_model('pretixbase', 'User')
for u in User.objects.all():
if u.givenname:
if u.familyname:
u.fullname = u.givenname + " " + u.familyname
else:
u.fullname = u.givenname
elif u.familyname:
u.fullname = u.familyname
u.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0056_auto_20170107_1251'),
]
operations = [
migrations.AddField(
model_name='user',
name='fullname',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Full name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.RunPython(merge_names, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 15:33
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0057_auto_20170107_1531'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='familyname',
),
migrations.RemoveField(
model_name='user',
name='givenname',
),
]

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-13 14:07
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0058_auto_20170107_1533'),
]
operations = [
migrations.CreateModel(
name='CachedCombinedTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('extension', models.CharField(max_length=255)),
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')),
],
),
]

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-13 14:38
from __future__ import unicode_literals
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0059_cachedcombinedticket'),
]
operations = [
migrations.AddField(
model_name='cachedcombinedticket',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='cachedticket',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-01 04:31
from __future__ import unicode_literals
from django.db import migrations
import pretix.base.i18n
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0060_auto_20170113_1438'),
]
operations = [
migrations.AddField(
model_name='event',
name='location',
field=pretix.base.i18n.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'),
),
]

View File

@@ -3,6 +3,7 @@ from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .event import (
Event, EventLock, EventPermission, EventSetting, RequiredAction,
generate_invite_token,
)
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
@@ -11,8 +12,10 @@ from .items import (
)
from .log import LogEntry
from .orders import (
AbstractPosition, CachedTicket, CartPosition, InvoiceAddress, Order,
OrderPosition, QuestionAnswer, generate_position_secret, generate_secret,
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
InvoiceAddress, Order, OrderPosition, QuestionAnswer,
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
from .vouchers import Voucher

View File

@@ -43,10 +43,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:param email: The user's email address, used for identification.
:type email: str
:param givenname: The user's given name. May be empty or null.
:type givenname: str
:param familyname: The user's given name. May be empty or null.
:type familyname: str
:param fullname: The user's full name. May be empty or null.
:type fullname: str
:param is_active: Whether this user account is activated.
:type is_active: bool
:param is_staff: ``True`` for system operators.
@@ -64,10 +62,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('E-mail'))
givenname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Given name'))
familyname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Family name'))
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
verbose_name=_('Is active'))
is_staff = models.BooleanField(default=False,
@@ -100,14 +96,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
Returns the first of the following user properties that is found to exist:
* Given name
* Family name
* Full name
* Email address
Only present for backwards compatibility
"""
if self.givenname:
return self.givenname
elif self.familyname:
return self.familyname
if self.fullname:
return self.fullname
else:
return self.email
@@ -115,20 +110,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
Returns the first of the following user properties that is found to exist:
* A combination of given name and family name, depending on the locale
* Given name
* Family name
* User name
* Full name
* Email address
"""
if self.givenname and not self.familyname:
return self.givenname
elif not self.givenname and self.familyname:
return self.familyname
elif self.familyname and self.givenname:
return _('%(family)s, %(given)s') % {
'family': self.familyname,
'given': self.givenname
}
if self.fullname:
return self.fullname
else:
return self.email

View File

@@ -1,3 +1,4 @@
import string
import uuid
from datetime import date, datetime, time
@@ -50,6 +51,8 @@ class Event(LoggedModel):
:type presale_start: datetime
:param presale_end: No tickets will be sold after this date.
:type presale_end: datetime
:param location: venue
:type location: str
:param plugins: A comma-separated list of plugin names that are active for this
event.
:type plugins: str
@@ -98,6 +101,11 @@ class Event(LoggedModel):
verbose_name=_("Start of presale"),
help_text=_("No products will be sold before this date."),
)
location = I18nCharField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
plugins = models.TextField(
null=True, blank=True,
verbose_name=_("Plugins"),
@@ -294,6 +302,10 @@ class Event(LoggedModel):
s.save()
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
class EventPermission(models.Model):
"""
The relation between an Event and a User who has permissions to
@@ -314,7 +326,9 @@ class EventPermission(models.Model):
"""
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True)
invite_email = models.EmailField(null=True, blank=True)
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
can_change_settings = models.BooleanField(
default=True,
verbose_name=_("Can change event settings")

View File

@@ -101,7 +101,7 @@ class LogEntry(models.Model):
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
a_map = {
'href': reverse('control:event.items.categories.show', kwargs={
'href': reverse('control:event.items.categories.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'category': co.id

View File

@@ -1,4 +1,5 @@
import copy
import os
import string
from datetime import datetime
from decimal import Decimal
@@ -228,7 +229,7 @@ class Order(LoggedModel):
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
if not Order.objects.filter(event=self.event, code=code).exists():
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
self.code = code
return
@@ -268,12 +269,15 @@ class Order(LoggedModel):
def _can_be_paid(self) -> Union[bool, str]:
error_messages = {
'late': _("The payment is too late to be accepted."),
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
"payment settings is over."),
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
"payments should be accepted in the payment settings."),
}
if self.event.settings.get('payment_term_last'):
if now() > self.event.payment_term_last:
return error_messages['late']
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
return True
if not self.event.settings.get('payment_term_accept_late'):
@@ -565,12 +569,24 @@ class InvoiceAddress(models.Model):
def cachedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
return 'tickets/{org}/{ev}/{code}-{no}-{prov}-{secret}.pdf'.format(
return 'tickets/{org}/{ev}/{code}-{no}-{prov}-{secret}.dat'.format(
org=instance.order_position.order.event.organizer.slug,
ev=instance.order_position.order.event.slug,
prov=instance.provider,
no=instance.order_position.positionid,
code=instance.order_position.order.code,
secret=secret,
ext=os.path.splitext(filename)[1]
)
def cachedcombinedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
return 'tickets/{org}/{ev}/{code}-{prov}-{secret}.dat'.format(
org=instance.order.event.organizer.slug,
ev=instance.order.event.slug,
prov=instance.provider,
code=instance.order.code,
secret=secret
)
@@ -581,6 +597,16 @@ class CachedTicket(models.Model):
type = models.CharField(max_length=255)
extension = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name)
created = models.DateTimeField(auto_now_add=True)
class CachedCombinedTicket(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
provider = models.CharField(max_length=255)
type = models.CharField(max_length=255)
extension = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name)
created = models.DateTimeField(auto_now_add=True)
@receiver(post_delete, sender=CachedTicket)

View File

@@ -1,5 +1,8 @@
import string
from django.core.validators import RegexValidator
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
@@ -38,7 +41,7 @@ class Organizer(LoggedModel):
),
OrganizerSlugBlacklistValidator()
],
verbose_name=_("Slug"),
verbose_name=_("Short form"),
)
permitted = models.ManyToManyField(User, through='OrganizerPermission',
related_name="organizers")
@@ -76,6 +79,10 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self)
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
class OrganizerPermission(models.Model):
"""
The relation between an Organizer and a User who has permissions to
@@ -91,11 +98,17 @@ class OrganizerPermission(models.Model):
"""
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="organizer_perms")
user = models.ForeignKey(User, related_name="organizer_perms", on_delete=models.CASCADE, null=True, blank=True)
invite_email = models.EmailField(null=True, blank=True)
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
can_create_events = models.BooleanField(
default=True,
verbose_name=_("Can create events"),
)
can_change_permissions = models.BooleanField(
default=True,
verbose_name=_("Can change permissions"),
)
class Meta:
verbose_name = _("Organizer permission")

View File

@@ -21,6 +21,15 @@ from pretix.base.signals import register_payment_providers
from pretix.presale.views import get_cart_total
class PaymentProviderForm(Form):
def clean(self):
cleaned_data = super().clean()
for k, v in self.fields.items():
val = cleaned_data.get(k)
if v._required and not val:
self.add_error(k, _('This field is required.'))
class BasePaymentProvider:
"""
This is the base class for all payment providers.
@@ -187,8 +196,12 @@ class BasePaymentProvider:
process. The default implementation constructs the form using
:py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
and all fields and fills the form with data form the user's session.
If you overwrite this, we strongly suggest that you inherit from
``PaymentProviderForm`` (from this module) that handles some nasty issues about
required fields for you.
"""
form = Form(
form = PaymentProviderForm(
data=(request.POST if request.method == 'POST' else None),
prefix='payment_%s' % self.identifier,
initial={
@@ -198,6 +211,12 @@ class BasePaymentProvider:
}
)
form.fields = self.payment_form_fields
for k, v in form.fields.items():
v._required = v.required
v.required = False
v.widget.is_required = False
return form
def _is_still_available(self, now_dt=None):

View File

@@ -3,17 +3,17 @@ import tempfile
from collections import defaultdict
from datetime import date
from decimal import Decimal
from locale import format as lformat
from django.contrib.staticfiles import finders
from django.core.files.base import ContentFile
from django.db import transaction
from django.utils.formats import date_format
from django.utils.formats import date_format, localize
from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext as _
from reportlab.lib import pagesizes
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
@@ -252,6 +252,13 @@ def _invoice_generate_german(invoice, f):
textobject.textLine(date_format(invoice.order.datetime, "DATE_FORMAT"))
canvas.drawText(textobject)
if invoice.event.settings.invoice_logo_image:
logo_file = invoice.event.settings.get('invoice_logo_image', binary_file=True)
canvas.drawImage(ImageReader(logo_file),
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n')
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Event').upper())
@@ -306,6 +313,7 @@ def _invoice_generate_german(invoice, f):
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('LEFTPADDING', (0, 0), (0, -1), 0),
@@ -320,16 +328,16 @@ def _invoice_generate_german(invoice, f):
total = Decimal('0.00')
for line in invoice.lines.all():
tdata.append((
line.description,
lformat("%.2f", line.tax_rate) + " %",
lformat("%.2f", line.net_value) + " " + invoice.event.currency,
lformat("%.2f", line.gross_value) + " " + invoice.event.currency,
Paragraph(line.description, styles['Normal']),
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + invoice.event.currency,
localize(line.gross_value) + " " + invoice.event.currency,
))
taxvalue_map[line.tax_rate] += line.tax_value
grossvalue_map[line.tax_rate] += line.gross_value
total += line.gross_value
tdata.append([pgettext('invoice', 'Invoice total'), '', '', lformat("%.2f", total) + " " + invoice.event.currency])
tdata.append([pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + invoice.event.currency])
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
@@ -361,10 +369,10 @@ def _invoice_generate_german(invoice, f):
tax = taxvalue_map[rate]
tdata.append((
'',
lformat("%.2f", rate) + " %",
lformat("%.2f", (gross - tax)) + " " + invoice.event.currency,
lformat("%.2f", gross) + " " + invoice.event.currency,
lformat("%.2f", tax) + " " + invoice.event.currency,
localize(rate) + " %",
localize((gross - tax)) + " " + invoice.event.currency,
localize(gross) + " " + invoice.event.currency,
localize(tax) + " " + invoice.event.currency,
))
if len(tdata) > 2:

View File

@@ -1,5 +1,5 @@
import logging
from typing import Any, Dict
from typing import Any, Dict, Union
import bleach
import cssutils
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
from inlinestyler.utils import inline_css
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Event, Order
from pretix.base.models import Event, InvoiceAddress, Order
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -30,7 +30,7 @@ class SendMailException(Exception):
pass
def mail(email: str, subject: str, template: str,
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None):
"""
@@ -43,7 +43,7 @@ def mail(email: str, subject: str, template: str,
:param template: The filename of a template to be used. It will be rendered with the locale given in the locale
argument and the context given in the next argument. Alternatively, you can pass a LazyI18nString and
``context`` will be used as the argument to a Python ``.format()`` call on the template.
``context`` will be used as the argument to a Python ``.format_map()`` call on the template.
:param context: The context for rendering the template (see ``template`` parameter)
@@ -63,7 +63,20 @@ def mail(email: str, subject: str, template: str,
if email == INVALID_ADDRESS:
return
headers = headers or {}
with language(locale):
if isinstance(context, dict) and order:
try:
context.update({
'invoice_name': order.invoice_address.name,
'invoice_company': order.invoice_address.company
})
except InvoiceAddress.DoesNotExist:
context.update({
'invoice_name': '',
'invoice_company': ''
})
if isinstance(template, LazyI18nString):
body = str(template)
if context:
@@ -92,6 +105,9 @@ def mail(email: str, subject: str, template: str,
htmlctx['event'] = event
htmlctx['color'] = event.settings.primary_color
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail:
headers['Reply-To'] = event.settings.contact_mail
prefix = event.settings.get('mail_prefix')
if prefix:
subject = "[%s] %s" % (prefix, subject)

View File

@@ -7,6 +7,7 @@ from typing import List, Optional
import pytz
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.db import transaction
from django.db.models import F, Q
from django.dispatch import receiver
@@ -63,7 +64,7 @@ logger = logging.getLogger(__name__)
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
force: bool=False, send_mail: bool=True, user: User=None) -> Order:
force: bool=False, send_mail: bool=True, user: User=None, mail_text='') -> Order:
"""
Marks an order as paid. This sets the payment provider, info and date and returns
the order object.
@@ -81,8 +82,13 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
:type send_mail: boolean
:param user: The user that performed the change
:param mail_text: Additional text to be included in the email
:type mail_text: str
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
if order.status == Order.STATUS_PAID:
return order
with order.event.lock() as now_dt:
can_be_paid = order._can_be_paid()
if not force and can_be_paid is not True:
@@ -98,14 +104,24 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
order.log_action('pretix.event.order.paid', {
'provider': provider,
'info': info,
'date': date,
'date': date or now_dt,
'manual': manual,
'force': force
}, user=user)
order_paid.send(order.event, order=order)
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
if not order.invoices.exists():
generate_invoice(order)
if send_mail:
with language(order.locale):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
mail(
order.email, _('Payment received for your order: %(code)s') % {'code': order.code},
order.event.settings.mail_text_order_paid,
@@ -115,7 +131,10 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
'order': order.code,
'secret': order.secret
}),
'downloads': order.event.settings.get('ticket_download', as_type=bool)
'downloads': order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
},
order.event, locale=order.locale
)
@@ -171,7 +190,7 @@ def _cancel_order(order, user=None):
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
return order
return order.pk
class OrderError(LazyLocaleException):
@@ -270,7 +289,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
raise OrderError(err)
@transaction.atomic
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
meta_info: dict=None):
@@ -299,33 +317,35 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
if last_date < expires:
expires = last_date
order = Order.objects.create(
status=Order.STATUS_PENDING,
event=event,
email=email,
datetime=now_dt,
expires=expires,
locale=locale,
total=total,
payment_fee=payment_fee,
payment_provider=payment_provider.identifier,
meta_info=json.dumps(meta_info or {}),
)
OrderPosition.transform_cart_positions(positions, order)
with transaction.atomic():
order = Order.objects.create(
status=Order.STATUS_PENDING,
event=event,
email=email,
datetime=now_dt,
expires=expires,
locale=locale,
total=total,
payment_fee=payment_fee,
payment_provider=payment_provider.identifier,
meta_info=json.dumps(meta_info or {}),
)
OrderPosition.transform_cart_positions(positions, order)
if address is not None:
try:
addr = InvoiceAddress.objects.get(
pk=address
)
if addr.order is not None:
addr.pk = None
addr.order = order
addr.save()
except InvoiceAddress.DoesNotExist:
pass
if address is not None:
try:
addr = InvoiceAddress.objects.get(
pk=address
)
if addr.order is not None:
addr.pk = None
addr.order = order
addr.save()
except InvoiceAddress.DoesNotExist:
pass
order.log_action('pretix.event.order.placed')
order.log_action('pretix.event.order.placed')
order_placed.send(event, order=order)
return order
@@ -363,6 +383,14 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
mailtext = event.settings.mail_text_order_free
else:
mailtext = event.settings.mail_text_order_placed
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
mail(
order.email, _('Your order: %(code)s') % {'code': order.code},
mailtext,
@@ -375,7 +403,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
'order': order.code,
'secret': order.secret
}),
'paymentinfo': str(pprov.order_pending_mail_render(order))
'paymentinfo': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
event, locale=order.locale
)
@@ -404,29 +434,39 @@ def send_expiry_warnings(sender, **kwargs):
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).select_related('event'):
settings = eventcache.get(o.event.pk, None)
if settings is None:
settings = o.event.settings
eventcache[o.event.pk] = settings
eventsettings = eventcache.get(o.event.pk, None)
if eventsettings is None:
eventsettings = o.event.settings
eventcache[o.event.pk] = eventsettings
days = settings.get('mail_days_order_expire_warning', as_type=int)
days = eventsettings.get('mail_days_order_expire_warning', as_type=int)
tz = pytz.timezone(eventsettings.get('timezone', settings.TIME_ZONE))
if days and (o.expires - today).days <= days:
o.expiry_reminder_sent = True
o.save()
try:
mail(
o.email, _('Your order is about to expire: %(code)s') % {'code': o.code},
settings.mail_text_order_expire_warning,
{
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'expire_date': date_format(o.expires, 'SHORT_DATE_FORMAT')
},
o.event, locale=o.locale
)
invoice_name = o.invoice_address.name
invoice_company = o.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
try:
with language(o.locale):
mail(
o.email, _('Your order is about to expire: %(code)s') % {'code': o.code},
eventsettings.mail_text_order_expire_warning,
{
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
o.event, locale=o.locale
)
except SendMailException:
logger.exception('Reminder email could not be sent')
else:
@@ -557,6 +597,12 @@ class OrderChangeManager:
def _notify_user(self):
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
mail(
self.order.email, _('Your order has been changed: %(code)s') % {'code': self.order.code},
self.order.event.settings.mail_text_order_changed,
@@ -566,6 +612,8 @@ class OrderChangeManager:
'order': self.order.code,
'secret': self.order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
self.order.event, locale=self.order.locale
)

View File

@@ -5,7 +5,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.i18n import language
from pretix.base.models import CachedTicket, Event, Order, OrderPosition
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, Order, OrderPosition,
)
from pretix.base.services.async import ProfiledTask
from pretix.base.signals import register_ticket_outputs
from pretix.celery_app import app
@@ -37,6 +39,31 @@ def generate(order_position: str, provider: str):
ct.file.save(filename, ContentFile(data))
@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)
path, ext = os.path.splitext(filename)
ct.extension = ext
ct.save()
ct.file.save(filename, ContentFile(data))
class DummyRollbackException(Exception):
pass

View File

@@ -128,6 +128,10 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'show_variations_expanded': {
'default': 'False',
'type': bool
},
'ticket_download': {
'default': 'False',
'type': bool
@@ -231,6 +235,8 @@ Your {event} team"""))
we successfully received your payment for {event}. Thank you!
{payment_info}
You can change your order details and view the status of your order at
{url}
@@ -299,6 +305,10 @@ Your {event} team"""))
'default': None,
'type': File
},
'invoice_logo_image': {
'default': None,
'type': File
},
'frontpage_text': {
'default': '',
'type': LazyI18nString
@@ -359,7 +369,7 @@ class SettingsProxy:
settings[key] = self.get(key)
return settings
def _unserialize(self, value: str, as_type: type) -> Any:
def _unserialize(self, value: str, as_type: type, binary_file=False) -> Any:
if as_type is None and value is not None and value.startswith('file://'):
as_type = File
@@ -375,7 +385,7 @@ class SettingsProxy:
return value == 'True'
elif as_type == File:
try:
fi = default_storage.open(value[7:], 'r')
fi = default_storage.open(value[7:], 'rb' if binary_file else 'r')
fi.url = default_storage.url(value[7:])
return fi
except OSError:
@@ -414,7 +424,7 @@ class SettingsProxy:
raise TypeError('Unable to serialize %s into a setting.' % str(type(value)))
def get(self, key: str, default=None, as_type: type=None):
def get(self, key: str, default=None, as_type: type=None, binary_file=False):
"""
Get a setting specified by key ``key``. Normally, settings are strings, but
if you put non-strings into the settings object, you can request unserialization
@@ -440,7 +450,7 @@ class SettingsProxy:
if value is None and default is not None:
value = default
return self._unserialize(value, as_type)
return self._unserialize(value, as_type, binary_file=binary_file)
def __getitem__(self, key: str) -> Any:
return self.get(key)

View File

@@ -52,6 +52,18 @@ class EventPluginSignal(django.dispatch.Signal):
return responses
event_live_issues = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to determine whether an event can be taken live. If you want to
prevent the event from going live, return a string that will be displayed to the user
as the error message. If you don't, your receiver should return ``None``.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_payment_providers = EventPluginSignal(
providing_args=[]
)
@@ -82,6 +94,18 @@ subclass of pretix.base.exporter.BaseExporter
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_cart = EventPluginSignal(
providing_args=["positions"]
)
"""
This signal is sent out before the user starts checkout. It includes an iterable
with the current CartPosition objects.
The response of receivers will be ignored, but you can raise a CartError with an
appropriate exception message.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_placed = EventPluginSignal(
providing_args=["order"]
)

View File

@@ -1,11 +1,14 @@
import os
import tempfile
from collections import OrderedDict
from typing import Tuple
from zipfile import ZipFile
from django import forms
from django.http import HttpRequest
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, OrderPosition
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.settings import SettingsSandbox
@@ -29,7 +32,7 @@ class BaseTicketOutput:
"""
return self.settings.get('_enabled', as_type=bool)
def generate(self, order: OrderPosition) -> Tuple[str, str, str]:
def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
"""
This method should generate the download file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
@@ -37,6 +40,29 @@ class BaseTicketOutput:
"""
raise NotImplementedError()
def generate_order(self, order: Order) -> Tuple[str, str, str]:
"""
This method is the same as order() but should not generate one file per order position
but instead one file for the full order.
This method is optional to implement. If you don't implement it, the default
implementation will offer a zip file of the generate() results for the order positions.
This method should generate a download file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
which is otherwise ignored.
"""
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for pos in order.positions.all():
fname, __, content = self.generate(pos)
zipf.writestr('{}-{}{}'.format(
order.code, pos.positionid, os.path.splitext(fname)[1]
), content)
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}-{}.zip'.format(order.code, self.identifier), 'application/zip', zipf.read()
@property
def verbose_name(self) -> str:
"""

View File

@@ -1,9 +1,7 @@
from django.utils import timezone
from django.views.decorators.cache import cache_page
from django.views.decorators.http import etag
from django.views.i18n import (
get_javascript_catalog, render_javascript_catalog, to_locale,
)
from django.views.i18n import get_javascript_catalog, render_javascript_catalog
# Yes, we want to regenerate this every time the module has been imported to
# refresh the cache at least at every code deployment
@@ -21,5 +19,5 @@ js_info_dict = {
@cache_page(3600, key_prefix='js18n-%s' % import_date)
def js_catalog(request, lang):
packages = ['pretix']
catalog, plural = get_javascript_catalog(to_locale(lang), 'djangojs', packages)
catalog, plural = get_javascript_catalog(lang, 'djangojs', packages)
return render_javascript_catalog(catalog, plural)

View File

@@ -1,7 +1,7 @@
from django.conf import settings
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
from .signals import html_head, nav_event
from .signals import html_head, nav_event, nav_topbar
from .utils.i18n import get_javascript_format, get_moment_locale
@@ -37,6 +37,11 @@ def contextprocessor(request):
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
ctx['nav_event'] = _nav_event
_nav_topbar = []
for receiver, response in nav_topbar.send(request, request=request):
_nav_topbar += response
ctx['nav_topbar'] = _nav_topbar
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()

View File

@@ -56,7 +56,8 @@ class EventWizardBasicsForm(I18nModelForm):
'date_from',
'date_to',
'presale_start',
'presale_end'
'presale_end',
'location',
]
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -107,7 +108,7 @@ class EventWizardCopyForm(forms.Form):
),
widget=forms.RadioSelect,
empty_label=_('Do not copy'),
required=True
required=False
)
@@ -131,6 +132,7 @@ class EventUpdateForm(I18nModelForm):
'is_public',
'presale_start',
'presale_end',
'location',
]
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -329,7 +331,8 @@ class InvoiceSettingsForm(SettingsForm):
('False', _('No')),
('admin', _('Manually in admin panel')),
('user', _('Automatically on user request')),
('True', _('Automatically for all created orders'))
('True', _('Automatically for all created orders')),
('paid', _('Automatically on payment')),
)
)
invoice_address_from = forms.CharField(
@@ -361,6 +364,12 @@ class InvoiceSettingsForm(SettingsForm):
label=_("Invoice language"),
choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES,
)
invoice_logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
required=False,
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
)
class MailSettingsForm(SettingsForm):
@@ -378,31 +387,32 @@ class MailSettingsForm(SettingsForm):
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}")
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}, "
"{invoice_name}, {invoice_company}")
)
mail_text_order_paid = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}")
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}")
)
mail_text_order_free = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}")
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}")
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
)
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}")
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
)
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
@@ -421,7 +431,7 @@ class MailSettingsForm(SettingsForm):
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {expire_date}")
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}")
)
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
@@ -491,6 +501,10 @@ class DisplaySettingsForm(SettingsForm):
required=False,
widget=I18nTextarea
)
show_variations_expanded = forms.BooleanField(
label=_("Show variations of a product expanded by default"),
required=False
)
class TicketSettingsForm(SettingsForm):

View File

@@ -21,10 +21,10 @@ def _display_order_changed(event: Event, logentry: LogEntry):
if logentry.action_type == 'pretix.event.order.changed.item':
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(event.itemvariations.get(pk=data['old_variation']))
old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
new_item = str(event.items.get(pk=data['new_item']))
if data['new_variation']:
new_item += ' - ' + str(event.itemvariations.get(pk=data['new_variation']))
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
return text + ' ' + _('Position #{posid}: {old_item} ({old_price} {currency}) changed '
'to {new_item} ({new_price} {currency}).').format(
posid=data.get('positionid', '?'),
@@ -109,6 +109,14 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.plugins.disabled': _('A plugin has been disabled.'),
'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.changed': _('The event settings have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'),
'pretix.event.permissions.added': _('A user has been added to the event team.'),
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
}
data = json.loads(logentry.data)

View File

@@ -9,7 +9,9 @@ from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_str
from django.utils.translation import ugettext as _
from pretix.base.models import Event, EventPermission, Organizer
from pretix.base.models import (
Event, EventPermission, Organizer, OrganizerPermission,
)
class PermissionMiddleware(MiddlewareMixin):
@@ -24,7 +26,8 @@ class PermissionMiddleware(MiddlewareMixin):
"auth.login.2fa",
"auth.register",
"auth.forgot",
"auth.forgot.recover"
"auth.forgot.recover",
"auth.invite",
)
def process_request(self, request):
@@ -58,29 +61,53 @@ class PermissionMiddleware(MiddlewareMixin):
return redirect_to_login(
path, resolved_login_url, REDIRECT_FIELD_NAME)
request.user.events_cache = request.user.events.order_by(
events = Event.objects.all() if request.user.is_superuser else request.user.events
request.user.events_cache = events.order_by(
"organizer", "date_from").prefetch_related("organizer")
if 'event' in url.kwargs and 'organizer' in url.kwargs:
try:
request.event = Event.objects.filter(
slug=url.kwargs['event'],
permitted__id__exact=request.user.id,
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
request.eventperm = EventPermission.objects.get(
event=request.event,
user=request.user
)
if request.user.is_superuser:
request.event = Event.objects.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
request.eventperm = EventPermission(
event=request.event,
user=request.user
)
else:
request.event = Event.objects.filter(
slug=url.kwargs['event'],
permitted__id__exact=request.user.id,
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
request.eventperm = EventPermission.objects.get(
event=request.event,
user=request.user
)
request.organizer = request.event.organizer
except IndexError:
raise Http404(_("The selected event was not found or you "
"have no permission to administrate it."))
elif 'organizer' in url.kwargs:
try:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
permitted__id__exact=request.user.id,
)[0]
if request.user.is_superuser:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
)[0]
request.orgaperm = OrganizerPermission(
organizer=request.organizer,
user=request.user
)
else:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
permitted__id__exact=request.user.id,
)[0]
request.orgaperm = OrganizerPermission.objects.get(
organizer=request.organizer,
user=request.user
)
except IndexError:
raise Http404(_("The selected organizer was not found or you "
"have no permission to administrate it."))

View File

@@ -14,6 +14,8 @@ def event_permission_required(permission):
if not request.user.is_authenticated: # NOQA
# just a double check, should not ever happen
raise PermissionDenied()
if request.user.is_superuser:
return function(request, *args, **kw)
try:
perm = EventPermission.objects.get(
event=request.event,
@@ -28,7 +30,7 @@ def event_permission_required(permission):
allowed = getattr(perm, permission)
except AttributeError:
pass
if allowed:
if allowed or request.user.is_superuser:
return function(request, *args, **kw)
raise PermissionDenied(_('You do not have permission to view this content.'))
return wrapper
@@ -58,6 +60,8 @@ def organizer_permission_required(permission):
if not request.user.is_authenticated: # NOQA
# just a double check, should not ever happen
raise PermissionDenied()
if request.user.is_superuser:
return function(request, *args, **kw)
try:
perm = OrganizerPermission.objects.get(
organizer=request.organizer,

View File

@@ -28,7 +28,7 @@ nav_event = EventPluginSignal(
)
"""
This signal allows you to add additional views to the admin panel
navigation. You will get the request as a keyword argument ``return``.
navigation. You will get the request as a keyword argument ``request``.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You can also return
a fontawesome icon name with the key ``icon``, it will be respected depending
@@ -42,6 +42,24 @@ in pretix.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
nav_topbar = Signal(
providing_args=["request"]
)
"""
This signal allows you to add additional views to the top navigation bar.
You will get the request as a keyword argument ``return``.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You can also return
a fontawesome icon name with the key ``icon``, it will be respected depending
on the type of navigation. If set, on desktops only the ``icon`` will be shown.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
This is no ``EventPluginSignal``, so you do not get the event in the ``sender`` argument
and you may get the signal regardless of whether your plugin is active.
"""
event_dashboard_widgets = EventPluginSignal(
providing_args=[]
)
@@ -104,3 +122,15 @@ quota as argument in the ``quota`` keyword argument.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
organizer_edit_tabs = Signal(
providing_args=['organizer', 'request']
)
"""
This signal is sent out to include tabs on the detail page of an organizer. Receivers
should return a tuple with the first item being the tab title and the second item
being the content as HTML. The receivers get the ``organizer`` and the ``request`` as
keyword arguments.
This is a regular django signal (no pretix event signal).
"""

View File

@@ -0,0 +1,31 @@
{% extends "pretixcontrol/auth/base.html" %}
{% load bootstrap3 %}
{% load staticfiles %}
{% load i18n %}
{% block content %}
<form class="form-signin" action="" method="post">
<h3>{% trans "Accept an invitation" %}</h3>
<p>
{% url "control:auth.login" as loginurl %}
{% blocktrans trimmed with login_href='href="'|add:loginurl|add:'"'|safe %}
If you already have an account on this site with a different email address, you can
<a {{ login_href }}>log in</a> first and then click this link again to accept the
invitation with your existing account.
{% endblocktrans %}
</p>
{% bootstrap_form_errors form type='all' layout='inline' %}
{% csrf_token %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
{% bootstrap_field form.password_repeat %}
<div class="form-group buttons">
<a href="{% url "control:auth.login" %}" class="btn btn-link">
&laquo; {% trans "Login" %}
</a>
<button type="submit" class="btn btn-primary">
{% trans "Register" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -74,6 +74,35 @@
</li>
</ul>
<ul class="nav navbar-nav navbar-top-links navbar-right">
{% for nav in nav_topbar %}
<li {% if nav.children %}class="dropdown"{% endif %}>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="dropdown-toggle" data-toggle="dropdown"{% endif %}>
{% if nav.icon %}
<span class="fa fa-{{ nav.icon }}"></span>
<span class="visible-xs-inline">{{ nav.label }}</span>
{% else %}
{{ nav.label }}
{% endif %}
</a>
{% if nav.children %}
<ul class="dropdown-menu" role="menu">
{% for item in nav.children %}
<li>
<a href="{{ item.url }}"
{% if item.active %}class="active"{% endif %}>
{% if item.icon %}
<span class="fa fa-{{ item.icon }}"></span>
{% endif %}
{{ item.label|safe }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
<li>
<a href="{% url 'control:user.settings' %}">
<i class="fa fa-user"></i> {{ request.user.get_full_name }}

View File

@@ -0,0 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to the team of an event that uses pretix for their
ticket sales.
Event: {{ event }}
If you want to join that team, just click on the following link:
{{ url }}
If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
{% endblocktrans %}

View File

@@ -0,0 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to the team of an event organizer that uses pretix
for their ticket sales.
Organizer: {{ organizer }}
If you want to join that team, just click on the following link:
{{ url }}
If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
{% endblocktrans %}

View File

@@ -10,6 +10,7 @@
{% bootstrap_field form.primary_color layout="horizontal" %}
{% bootstrap_field form.logo_image layout="horizontal" %}
{% bootstrap_field form.frontpage_text layout="horizontal" %}
{% bootstrap_field form.show_variations_expanded layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -3,12 +3,21 @@
{% load eventurl %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>
{{ request.event.name }}
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-default btn-sm" target="_blank">
{% trans "Go to shop" %}
</a>
</h1>
<form action="{% eventurl request.event "presale:event.auth" %}" method="post" target="_blank">
<h1>
{{ request.event.name }}
{% if has_domain and not request.event.live %}
<input type="hidden" value="{{ new_session }}" name="session">
<button type="submit" class="btn btn-default btn-sm">
{% trans "Go to shop" %}
</button>
{% else %}
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-default btn-sm" target="_blank">
{% trans "Go to shop" %}
</a>
{% endif %}
</h1>
</form>
{% if actions|length > 0 %}
<div class="panel panel-danger">
@@ -42,10 +51,14 @@
<div class="row dashboard">
{% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
{% if w.url %}
{% if w.url %}{# backwards compatibility #}
<a href="{{ w.url }}" class="widget">
{{ w.content|safe }}
</a>
{% elif w.link %}
<a href="{{ w.link }}" class="widget">
{{ w.content|safe }}
</a>
{% else %}
<div class="widget">
{{ w.content|safe }}

View File

@@ -17,6 +17,7 @@
{% bootstrap_field form.invoice_introductory_text layout="horizontal" %}
{% bootstrap_field form.invoice_additional_text layout="horizontal" %}
{% bootstrap_field form.invoice_footer_text layout="horizontal" %}
{% bootstrap_field form.invoice_logo_image layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">

View File

@@ -28,7 +28,7 @@
</p>
<ul>
{% for issue in issues %}
<li>{{ issue }}</li>
<li>{{ issue|safe }}</li>
{% endfor %}
</ul>
</div>

View File

@@ -26,7 +26,16 @@
<tbody>
{% for form in formset %}
<tr>
<td>{{ form.id }}{{ form.instance.user }}</td>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_change_settings }}</td>
<td>{{ form.can_change_items }}</td>
<td>{{ form.can_view_orders }}</td>
@@ -37,6 +46,19 @@
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% blocktrans trimmed %}
To add a new user, you can enter their email address here. If they already have a
pretix account, they will immediately be added to the event. Otherwise, they will
be sent an email with an invitation.
{% endblocktrans %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
@@ -53,7 +75,7 @@
<td>{{ add_form.can_change_vouchers }}</td>
<td>{{ add_form.can_view_vouchers }}</td>
</tr>
</tbody>
</tfoot>
</table>
</div>
</fieldset>

View File

@@ -23,6 +23,8 @@
<div class="col-sm-4">
{% if plugin.app.compatibility_errors %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
{% elif plugin.restricted and not request.user.is_superuser %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
{% elif plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
{% else %}
@@ -42,6 +44,11 @@
{% endblocktrans %}</p>
{% endif %}
<p>{{ plugin.description }}</p>
{% if plugin.restricted and not request.user.is_superuser %}
<div class="alert alert-warning">
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
</div>
{% endif %}
{% if plugin.app.compatibility_errors %}
<div class="alert alert-warning">
{% trans "This plugin cannot be enabled for the following reasons:" %}

View File

@@ -11,6 +11,7 @@
{% bootstrap_field form.slug layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.is_public layout="horizontal" %}
</fieldset>

View File

@@ -6,22 +6,33 @@
<h1>{% trans "Create a new event" %} <small>{% blocktrans trimmed with step=wizard.steps.step1 %}
Step {{ step }}
{% endblocktrans %}</small></h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{{ wizard.management_form }}
{% bootstrap_form_errors form %}
{% block form %}
{% endblock %}
<div class="form-group submit-group">
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
class="btn btn-default btn-lg pull-left">
{% trans "Back" %}
{% if has_organizer %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{{ wizard.management_form }}
{% bootstrap_form_errors form %}
{% block form %}
{% endblock %}
<div class="form-group submit-group">
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
class="btn btn-default btn-lg pull-left">
{% trans "Back" %}
</button>
{% endif %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
{% endif %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
</div>
</form>
{% else %}
<div class="alert alert-danger">
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
</div>
</form>
{% if request.user.is_superuser %}
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
{% trans "Create a new organizer" %}
</a>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -8,6 +8,7 @@
{% bootstrap_field form.slug layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
</fieldset>
<fieldset>

View File

@@ -8,23 +8,25 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
<div class="row">
<div class="col-xs-12 col-lg-10">
<div class="col-xs-12{% if category %} col-lg-10{% endif %}">
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.description layout="horizontal" %}
</fieldset>
</div>
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Category history" %}
</h3>
{% if category %}
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Category history" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=category %}
</div>
{% include "pretixcontrol/includes/logs.html" with obj=category %}
</div>
</div>
{% endif %}
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -3,19 +3,157 @@
{% load bootstrap3 %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizer" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</h1>
<ul class="nav nav-tabs">
<li class="active">
<a href="#tab-events" data-toggle="tab">{% trans "Events" %}</a>
</li>
{% if request.orgaperm.can_change_permissions %}
<li>
<a href="#tab-permissions" data-toggle="tab">{% trans "Team" %}</a>
</li>
{% endif %}
{% for title, content in tabs %}
<li>
<a href="#tab-{{ forloop.counter }}" data-toggle="tab">
{{ title }}
</a>
</li>
{% endfor %}
</ul>
<div class="tab-content organizer-tabs">
<div class="tab-pane active" id="tab-events">
<div class="tab-inner">
{% if events|length == 0 %}
<p>
<em>{% trans "You currently do not have access to any events." %}</em>
</p>
{% else %}
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Start date" %}</th>
</tr>
</thead>
<tbody>
{% for e in events %}
<tr>
<td>
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.get_date_from_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
</div>
</div>
</form>
{% if request.orgaperm.can_change_permissions %}
<div class="tab-pane" id="tab-permissions">
<div class="tab-inner">
<form action="" method="post" class="form-horizontal form-permissions">
{% csrf_token %}
<p>
{% blocktrans trimmed %}
You can use the following list to control who can create new events in the name of
this organizer and who can add more people to this list. This does <strong>not</strong>
control who has access to a particular event. You can control the access to an
event in the "Permissions" section of the event's settings. A user does not need to
be on the list here to get access to an event.
{% endblocktrans %}
</p>
<p>
{% trans "Everyone on this list can control the organizer settings on this page." %}
</p>
{% bootstrap_formset_errors formset %}
{{ formset.management_form }}
<div class="table-responsive">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "Create events" %}</th>
<th>{% trans "Change permissions" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_create_events }}</td>
<td>{{ form.can_change_permissions }}</td>
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% blocktrans trimmed %}
To add a new user, you can enter their email address here. If they
already have a pretix account, they will immediately be added to the team.
Otherwise, they will be sent an email with an invitation.
{% endblocktrans %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
<div class="col-sm-12">
{% bootstrap_field add_form.user layout='inline' %}
</div>
</div>
</td>
<td>{{ add_form.can_create_events }}</td>
<td>{{ add_form.can_change_permissions }}</td>
</tr>
</tfoot>
</table>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
</div>
</div>
{% endif %}
{% for title, content in tabs %}
<div class="tab-pane" id="tab-{{ forloop.counter }}">
<div class="tab-inner">
{{ content }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizer" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -20,7 +20,7 @@
{% for o in organizers %}
<tr>
<td><strong>
<a href="{% url "control:organizer.edit" organizer=o.slug %}">{{ o.name }}</a>
<a href="{% url "control:organizer" organizer=o.slug %}">{{ o.name }}</a>
</strong></td>
</tr>
{% endfor %}

View File

@@ -9,8 +9,7 @@
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.givenname layout='horizontal' %}
{% bootstrap_field form.familyname layout='horizontal' %}
{% bootstrap_field form.fullname layout='horizontal' %}
{% bootstrap_field form.locale layout='horizontal' %}
</fieldset>
<fieldset>

View File

@@ -13,7 +13,7 @@
<div class="form-group">
<div class="col-md-7 col-sm-12 col-md-offset-3">
<div class="input-group">
<input type="text" class="form-control input-xs"
<input type="number" class="form-control input-xs"
id="voucher-bulk-codes-num"
placeholder="{% trans "Number" %}">
<div class="input-group-btn">

View File

@@ -10,6 +10,7 @@ urlpatterns = [
url(r'^login$', auth.login, name='auth.login'),
url(r'^login/2fa$', auth.Login2FAView.as_view(), name='auth.login.2fa'),
url(r'^register$', auth.register, name='auth.register'),
url(r'^invite/(?P<token>[a-zA-Z0-9]+)$', auth.invite, name='auth.invite'),
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
url(r'^$', dashboards.user_index, name='index'),
@@ -31,6 +32,7 @@ urlpatterns = [
name='user.settings.2fa.delete'),
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^events/$', main.EventList.as_view(), name='events'),
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),

View File

@@ -23,7 +23,9 @@ from u2flib_server.utils import rand_bytes
from pretix.base.forms.auth import (
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
)
from pretix.base.models import U2FDevice, User
from pretix.base.models import (
EventPermission, OrganizerPermission, U2FDevice, User,
)
from pretix.base.services.mail import SendMailException, mail
from pretix.helpers.urls import build_absolute_uri
@@ -99,6 +101,68 @@ def register(request):
return render(request, 'pretixcontrol/auth/register.html', ctx)
def invite(request, token):
"""
Registration form in case of an invite
"""
ctx = {}
try:
perm = EventPermission.objects.get(invite_token=token)
desc = perm.event.name
except EventPermission.DoesNotExist:
try:
perm = OrganizerPermission.objects.get(invite_token=token)
desc = perm.organizer.name
except OrganizerPermission.DoesNotExist:
messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar '
'and make sure it is correct and that the link has not been used before.'))
return redirect('control:auth.login')
if request.user.is_authenticated:
try:
if isinstance(perm, EventPermission):
EventPermission.objects.get(event=perm.event, user=request.user)
else:
OrganizerPermission.objects.get(organizer=perm.organizer, user=request.user)
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
'this team.').format(desc))
return redirect('control:index')
except (EventPermission.DoesNotExist, OrganizerPermission.DoesNotExist):
pass
perm.invite_token = None
perm.invite_email = None
perm.user = request.user
perm.save()
messages.success(request, _('You have now access to "{}".').format(desc))
return redirect('control:index')
if request.method == 'POST':
form = RegistrationForm(data=request.POST)
if form.is_valid():
user = User.objects.create_user(
form.cleaned_data['email'], form.cleaned_data['password'],
locale=request.LANGUAGE_CODE,
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE
)
user = authenticate(email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
perm.invite_token = None
perm.invite_email = None
perm.user = user
perm.save()
messages.success(request, _('Welcome to pretix! You have now access to "{}".').format(desc))
return redirect('control:index')
else:
form = RegistrationForm(initial={'email': perm.invite_email})
ctx['form'] = form
return render(request, 'pretixcontrol/auth/invite.html', ctx)
class Forgot(TemplateView):
template_name = 'pretixcontrol/auth/forgot.html'

View File

@@ -1,5 +1,7 @@
from decimal import Decimal
from importlib import import_module
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db.models import Sum
@@ -17,6 +19,7 @@ from pretix.control.signals import (
from ..logdisplay import OVERVIEW_BLACKLIST
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
@@ -167,11 +170,22 @@ def event_index(request, organizer, event):
a_qs = request.event.requiredaction_set.filter(done=False)
has_domain = request.event.organizer.domains.exists()
ctx = {
'widgets': rearrange(widgets),
'logs': qs[:5],
'actions': a_qs[:5] if request.eventperm.can_change_orders else []
'actions': a_qs[:5] if request.eventperm.can_change_orders else [],
'has_domain': has_domain
}
if not request.event.live and has_domain:
s = SessionStore()
s['pretix_event_access_{}'.format(request.event.pk)] = request.session.session_key
s.create()
ctx['new_session'] = s.session_key
request.session['event_access'] = True
for a in ctx['actions']:
a.display = a.display(request)

View File

@@ -22,8 +22,9 @@ from pretix.base.models import (
)
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
event_live_issues, register_payment_providers, register_ticket_outputs,
)
from pretix.control.forms.event import (
DisplaySettingsForm, EventSettingsForm, EventUpdateForm,
@@ -31,6 +32,7 @@ from pretix.control.forms.event import (
TicketSettingsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
from pretix.presale.style import regenerate_css
from . import UpdateView
@@ -130,6 +132,9 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
if key.startswith("plugin:"):
module = key.split(":")[1]
if value == "enable" and module in plugins_available:
if getattr(plugins_available[module], 'restricted', False):
if not request.user.is_superuser:
continue
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': module})
if module not in plugins_active:
@@ -544,27 +549,57 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
ctx['add_form'] = self.add_form
return ctx
def _send_invite(self, instance):
try:
mail(
instance.invite_email,
_('Account information changed'),
'pretixcontrol/email/invitation.txt',
{
'user': self,
'event': self.request.event.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.invite_token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
try:
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
self.add_form.instance.event = self.request.event
self.add_form.instance.event_id = self.request.event.id
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
except User.DoesNotExist:
messages.error(self.request, _('There is no user with the email address you entered.'))
return self.get(*args, **kwargs)
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
if EventPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
event=self.request.event).exists():
messages.error(self.request, _('This user already has been invited for this event.'))
return self.get(*args, **kwargs)
self.add_form.save()
self._send_invite(self.add_form.instance)
self.request.event.log_action(
'pretix.event.permissions.invited', user=self.request.user, data=logdata
)
else:
if EventPermission.objects.filter(user=self.add_form.instance.user,
event=self.request.event).exists():
messages.error(self.request, _('This user already has permissions for this event.'))
return self.get(*args, **kwargs)
self.add_form.save()
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
logdata['user'] = self.add_form.instance.user_id
self.request.event.log_action(
'pretix.event.permissions.added', user=self.request.user, data=logdata
@@ -583,6 +618,14 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
return self.get(*args, **kwargs)
for form in self.formset.deleted_forms:
logdata = {
k: v for k, v in form.cleaned_data.items()
}
self.request.event.log_action(
'pretix.event.permissions.deleted', user=self.request.user, data=logdata
)
self.formset.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
@@ -628,6 +671,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if not self.request.event.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
responses = event_live_issues.send(self.request.event)
for receiver, response in responses:
if response:
issues.append(response)
return issues
def post(self, request, *args, **kwargs):

View File

@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView
from formtools.wizard.views import SessionWizardView
from pretix.base.models import Event, EventPermission
from pretix.base.models import Event, EventPermission, OrganizerPermission
from pretix.control.forms.event import (
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
)
@@ -20,11 +20,16 @@ class EventList(ListView):
template_name = 'pretixcontrol/events/index.html'
def get_queryset(self):
return Event.objects.filter(
permitted__id__exact=self.request.user.pk
).select_related("organizer").prefetch_related(
"setting_objects", "organizer__setting_objects"
)
if self.request.user.is_superuser:
return Event.objects.all().select_related("organizer").prefetch_related(
"setting_objects", "organizer__setting_objects"
)
else:
return Event.objects.filter(
permitted__id__exact=self.request.user.pk
).select_related("organizer").prefetch_related(
"setting_objects", "organizer__setting_objects"
)
def condition_copy(wizard):
@@ -48,6 +53,12 @@ class EventWizard(SessionWizardView):
'copy': condition_copy
}
def get_context_data(self, form, **kwargs):
ctx = super().get_context_data(form, **kwargs)
ctx['has_organizer'] = OrganizerPermission.objects.filter(user=self.request.user,
can_create_events=True).exists()
return ctx
def get_form_kwargs(self, step=None):
kwargs = {
'user': self.request.user

View File

@@ -12,8 +12,8 @@ from django.views.generic import DetailView, ListView, TemplateView, View
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, CachedTicket, Invoice, Item, ItemVariation, Order, Quota,
generate_position_secret, generate_secret,
CachedFile, CachedTicket, Invoice, InvoiceAddress, Item, ItemVariation,
Order, Quota, generate_position_secret, generate_secret,
)
from pretix.base.services.export import export
from pretix.base.services.invoices import (
@@ -323,6 +323,12 @@ class OrderResendLink(OrderView):
def post(self, *args, **kwargs):
with language(self.order.locale):
try:
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
mail(
self.order.email, _('Your order: %(code)s') % {'code': self.order.code},
self.order.event.settings.mail_text_resend_link,
@@ -332,6 +338,8 @@ class OrderResendLink(OrderView):
'order': self.order.code,
'secret': self.order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
self.order.event, locale=self.order.locale
)

View File

@@ -1,12 +1,21 @@
from django import forms
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms import modelformset_factory
from django.shortcuts import redirect
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, UpdateView
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from pretix.base.models import Organizer, OrganizerPermission
from pretix.base.forms import I18nModelForm
from pretix.base.models import Organizer, OrganizerPermission, User
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import organizer_edit_tabs
from pretix.helpers.urls import build_absolute_uri
class OrganizerList(ListView):
@@ -24,10 +33,168 @@ class OrganizerList(ListView):
)
class OrganizerPermissionForm(I18nModelForm):
class Meta:
model = OrganizerPermission
fields = (
'can_create_events', 'can_change_permissions'
)
class OrganizerPermissionCreateForm(OrganizerPermissionForm):
user = forms.EmailField(required=False, label=_('User'))
class OrganizerDetail(OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/detail.html'
permission = None
context_object_name = 'organizer'
def get_object(self, queryset=None) -> Organizer:
return self.request.organizer
@cached_property
def formset(self):
fs = modelformset_factory(
OrganizerPermission,
form=OrganizerPermissionForm,
can_delete=True, can_order=False, extra=0
)
return fs(
data=(
self.request.POST
if self.request.method == "POST" and 'id_formset-TOTAL_FORMS' in self.request.POST
else None
),
prefix="formset",
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer)
)
@cached_property
def add_form(self):
return OrganizerPermissionCreateForm(
data=(
self.request.POST
if self.request.method == "POST" and 'id_formset-TOTAL_FORMS' in self.request.POST
else None
),
prefix="add"
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['add_form'] = self.add_form
ctx['events'] = self.request.organizer.events.all()
ctx['tabs'] = []
for recv, retv in organizer_edit_tabs.send(sender=self.request.organizer, request=self.request,
organizer=self.request.organizer):
ctx['tabs'].append(retv)
return ctx
def _send_invite(self, instance):
try:
mail(
instance.invite_email,
_('Account information changed'),
'pretixcontrol/email/invitation_organizer.txt',
{
'user': self,
'organizer': self.request.organizer.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.invite_token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, *args, **kwargs):
if not self.request.orgaperm.can_change_permissions:
raise PermissionDenied(_("You have no permission to do this."))
if 'id_formset-TOTAL_FORMS' not in self.request.POST:
return self.get(*args, **kwargs)
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
try:
self.add_form.instance.organizer = self.request.organizer
self.add_form.instance.organizer_id = self.request.organizer.id
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
except User.DoesNotExist:
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
if OrganizerPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
organizer=self.request.organizer).exists():
messages.error(self.request, _('This user already has been invited for this team.'))
return self.get(*args, **kwargs)
self.add_form.save()
self._send_invite(self.add_form.instance)
self.request.organizer.log_action(
'pretix.organizer.permissions.invited', user=self.request.user, data=logdata
)
else:
if OrganizerPermission.objects.filter(user=self.add_form.instance.user,
organizer=self.request.organizer).exists():
messages.error(self.request, _('This user already has permissions for this team.'))
return self.get(*args, **kwargs)
self.add_form.save()
logdata['user'] = self.add_form.instance.user_id
self.request.organizer.log_action(
'pretix.organizer.permissions.added', user=self.request.user, data=logdata
)
for form in self.formset.forms:
if form.has_changed():
changedata = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
changedata['user'] = form.instance.user_id
self.request.organizer.log_action(
'pretix.organizer.permissions.changed', user=self.request.user, data=changedata
)
if form.instance.user_id == self.request.user.pk:
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
return self.get(*args, **kwargs)
for form in self.formset.deleted_forms:
logdata = {
k: v for k, v in form.cleaned_data.items()
}
self.request.organizer.log_action(
'pretix.organizer.permissions.deleted', user=self.request.user, data=logdata
)
self.formset.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('Your changes could not be saved.'))
return self.get(*args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:organizer', kwargs={
'organizer': self.request.organizer.slug,
})
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm
template_name = 'pretixcontrol/organizers/detail.html'
template_name = 'pretixcontrol/organizers/edit.html'
permission = None
context_object_name = 'organizer'

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-03 23:46+0000\n"
"POT-Creation-Date: 2017-02-03 12:16+0000\n"
"PO-Revision-Date: 2017-01-01 20:40+0100\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
@@ -46,33 +46,34 @@ msgstr "Gesamtumsatz"
msgid "Contacting Stripe …"
msgstr "Kontaktiere Stripe …"
#: static/pretixcontrol/js/clipboard.js:23
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr "Kopiert!"
#: static/pretixcontrol/js/clipboard.js:29
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr "Drücken Sie Strg+C zum Kopieren!"
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
#: pretix/static/pretixcontrol/js/ui/main.js:28
#: pretix/static/pretixpresale/js/ui/main.js:99
msgid "Close message"
msgstr "Schließen"
#: static/pretixcontrol/js/ui/main.js:43
#: pretix/static/pretixcontrol/js/ui/main.js:43
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: static/pretixcontrol/js/ui/question.js:41
#: pretix/static/pretixcontrol/js/ui/question.js:41
msgid "Others"
msgstr "Sonstige"
#: static/pretixcontrol/js/ui/question.js:70
#: pretix/static/pretixcontrol/js/ui/question.js:70
msgid "Count"
msgstr "Anzahl"
#: static/pretixpresale/js/ui/asyncdownload.js:27
#: static/pretixpresale/js/ui/asynctask.js:27
#: static/pretixpresale/js/ui/asynctask.js:64
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:64
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 "
@@ -82,19 +83,19 @@ msgstr ""
"dies länger als zwei Minuten dauert, kontaktieren Sie uns bitte oder gehen "
"Sie in Ihrem Browser einen Schritt zurück und versuchen es erneut."
#: static/pretixpresale/js/ui/asyncdownload.js:40
#: static/pretixpresale/js/ui/asynctask.js:43
#: static/pretixpresale/js/ui/asynctask.js:85
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:40
#: pretix/static/pretixpresale/js/ui/asynctask.js:43
#: pretix/static/pretixpresale/js/ui/asynctask.js:85
msgid "An error of type {code} occured."
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
#: static/pretixpresale/js/ui/asyncdownload.js:53
#: static/pretixpresale/js/ui/asynctask.js:103
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:53
#: pretix/static/pretixpresale/js/ui/asynctask.js:103
msgid "We are processing your request …"
msgstr "Wir verarbeiten deine Anfrage …"
msgstr "Wir verarbeiten Ihre Anfrage …"
#: static/pretixpresale/js/ui/asyncdownload.js:54
#: static/pretixpresale/js/ui/asynctask.js:104
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:54
#: pretix/static/pretixpresale/js/ui/asynctask.js:104
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 "
@@ -104,7 +105,7 @@ msgstr ""
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
"Seite neu laden und es erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:46
#: pretix/static/pretixpresale/js/ui/asynctask.js:46
msgid ""
"We currenctly cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -112,22 +113,22 @@ msgstr ""
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
"Letzter Fehlercode: {code}"
#: static/pretixpresale/js/ui/asynctask.js:76
#: pretix/static/pretixpresale/js/ui/asynctask.js:76
msgid "The request took to long. Please try again."
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:88
#: pretix/static/pretixpresale/js/ui/asynctask.js:88
msgid ""
"We currenctly cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
"einmal. Fehlercode: {code}"
#: static/pretixpresale/js/ui/cart.js:10
#: pretix/static/pretixpresale/js/ui/cart.js:10
msgid "The items in your cart are no longer reserved for you."
msgstr "Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert."
#: static/pretixpresale/js/ui/cart.js:14
#: pretix/static/pretixpresale/js/ui/cart.js:14
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-03 23:46+0000\n"
"PO-Revision-Date: 2017-01-01 20:41+0100\n"
"POT-Creation-Date: 2017-02-03 12:16+0000\n"
"PO-Revision-Date: 2017-01-18 09:42+0100\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
"Language: de\n"
@@ -46,33 +46,34 @@ msgstr "Gesamtumsatz"
msgid "Contacting Stripe …"
msgstr "Kontaktiere Stripe …"
#: static/pretixcontrol/js/clipboard.js:23
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr "Kopiert!"
#: static/pretixcontrol/js/clipboard.js:29
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr "Drücke Strg+C zum kopieren!"
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
#: pretix/static/pretixcontrol/js/ui/main.js:28
#: pretix/static/pretixpresale/js/ui/main.js:99
msgid "Close message"
msgstr "Schließen"
#: static/pretixcontrol/js/ui/main.js:43
#: pretix/static/pretixcontrol/js/ui/main.js:43
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: static/pretixcontrol/js/ui/question.js:41
#: pretix/static/pretixcontrol/js/ui/question.js:41
msgid "Others"
msgstr "Sonstige"
#: static/pretixcontrol/js/ui/question.js:70
#: pretix/static/pretixcontrol/js/ui/question.js:70
msgid "Count"
msgstr "Anzahl"
#: static/pretixpresale/js/ui/asyncdownload.js:27
#: static/pretixpresale/js/ui/asynctask.js:27
#: static/pretixpresale/js/ui/asynctask.js:64
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:64
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 "
@@ -82,19 +83,19 @@ msgstr ""
"dies länger als zwei Minuten dauert, kontaktiere uns bitte oder gehe in "
"deinem Browser einen Schritt zurück und versuche es erneut."
#: static/pretixpresale/js/ui/asyncdownload.js:40
#: static/pretixpresale/js/ui/asynctask.js:43
#: static/pretixpresale/js/ui/asynctask.js:85
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:40
#: pretix/static/pretixpresale/js/ui/asynctask.js:43
#: pretix/static/pretixpresale/js/ui/asynctask.js:85
msgid "An error of type {code} occured."
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
#: static/pretixpresale/js/ui/asyncdownload.js:53
#: static/pretixpresale/js/ui/asynctask.js:103
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:53
#: pretix/static/pretixpresale/js/ui/asynctask.js:103
msgid "We are processing your request …"
msgstr "Wir verarbeiten deine Anfrage …"
#: static/pretixpresale/js/ui/asyncdownload.js:54
#: static/pretixpresale/js/ui/asynctask.js:104
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:54
#: pretix/static/pretixpresale/js/ui/asynctask.js:104
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 "
@@ -104,7 +105,7 @@ msgstr ""
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
"neu laden und es erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:46
#: pretix/static/pretixpresale/js/ui/asynctask.js:46
msgid ""
"We currenctly cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -112,22 +113,22 @@ msgstr ""
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
"Letzter Fehlercode: {code}"
#: static/pretixpresale/js/ui/asynctask.js:76
#: pretix/static/pretixpresale/js/ui/asynctask.js:76
msgid "The request took to long. Please try again."
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:88
#: pretix/static/pretixpresale/js/ui/asynctask.js:88
msgid ""
"We currenctly cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
"einmal. Fehlercode: {code}"
#: static/pretixpresale/js/ui/cart.js:10
#: pretix/static/pretixpresale/js/ui/cart.js:10
msgid "The items in your cart are no longer reserved for you."
msgstr "Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert."
#: static/pretixpresale/js/ui/cart.js:14
#: pretix/static/pretixpresale/js/ui/cart.js:14
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""

View File

@@ -2,36 +2,45 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
{% if no_more_payments %}
<div class="alert alert-danger">
{% blocktrans trimmed with date=request.event.settings.payment_term_last|date:"SHORT_DATE_FORMAT" %}
In the payment settings of your event, you set the {{ date }} as the last date of any payments.
Therefore, you won't be able to mark any order as paid here.
{% endblocktrans %}
</div>
<div class="panel-body">
<p>{% blocktrans trimmed %}
This page allows you to upload bank statement files to process incoming payments.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
Currently, this feature supports <code>.csv</code> files and files in the MT940 format.
{% endblocktrans %}</p>
{% if job_running %}
<div class="alert alert-info" data-job-waiting data-job-waiting-url="{% url "plugins:banktransfer:import.job" event=request.event.slug organizer=request.event.organizer.slug job=job_running.pk %}?ajax=1">
<span class="fa fa-cog fa-spin"></span>
{% trans "An import is currently being processed, please try again in a few minutes." %}
</div>
{% else %}
<form action="" method="post" enctype="multipart/form-data" class="form-inline">
{% csrf_token %}
<div class="form-group">
<label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file"/>
{% else %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
</div>
<div class="panel-body">
<p>{% blocktrans trimmed %}
This page allows you to upload bank statement files to process incoming payments.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
Currently, this feature supports <code>.csv</code> files and files in the MT940 format.
{% endblocktrans %}</p>
{% if job_running %}
<div class="alert alert-info" data-job-waiting data-job-waiting-url="{% url "plugins:banktransfer:import.job" event=request.event.slug organizer=request.event.organizer.slug job=job_running.pk %}?ajax=1">
<span class="fa fa-cog fa-spin"></span>
{% trans "An import is currently being processed, please try again in a few minutes." %}
</div>
<div class="clearfix"></div>
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start upload" %}
</button>
</form>
{% endif %}
{% else %}
<form action="" method="post" enctype="multipart/form-data" class="form-inline">
{% csrf_token %}
<div class="form-group">
<label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file"/>
</div>
<div class="clearfix"></div>
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start upload" %}
</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if transactions_unhandled|length > 0 or request.GET.search %}
<div class="panel panel-default">
<div class="panel-heading">

View File

@@ -2,7 +2,6 @@ import csv
import json
import logging
from datetime import timedelta
from locale import format as lformat
from django.contrib import messages
from django.core.urlresolvers import reverse
@@ -135,6 +134,8 @@ class ActionView(EventPermissionRequiredMixin, View):
})
def get(self, request, *args, **kwargs):
from django.utils.formats import localize
query = request.GET.get('query', '')
if len(query) < 2:
return JsonResponse({'results': []})
@@ -145,7 +146,7 @@ class ActionView(EventPermissionRequiredMixin, View):
{
'code': o.code,
'status': o.get_status_display(),
'total': lformat("%.2f", o.total) + ' ' + self.request.event.currency
'total': localize(o.total) + ' ' + self.request.event.currency
} for o in qs
]
})
@@ -354,4 +355,8 @@ class ImportView(EventPermissionRequiredMixin, ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['job_running'] = self.job_running
ctx['no_more_payments'] = False
if self.request.event.settings.get('payment_term_last'):
if now() > self.request.event.payment_term_last:
ctx['no_more_payments'] = True
return ctx

View File

@@ -8,7 +8,7 @@ from django.contrib import messages
from django.template.loader import get_template
from django.utils.translation import ugettext as __, ugettext_lazy as _
from pretix.base.models import Quota, RequiredAction
from pretix.base.models import Order, Quota, RequiredAction
from pretix.base.payment import BasePaymentProvider
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
@@ -160,32 +160,34 @@ class Paypal(BasePaymentProvider):
return self._execute_payment(payment, request, order)
def _execute_payment(self, payment, request, order):
payment.replace([
{
"op": "replace",
"path": "/transactions/0/item_list",
"value": {
"items": [
{
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
"quantity": 1,
"price": str(order.total),
"currency": order.event.currency
}
]
if payment.state == 'created':
payment.replace([
{
"op": "replace",
"path": "/transactions/0/item_list",
"value": {
"items": [
{
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
"quantity": 1,
"price": str(order.total),
"currency": order.event.currency
}
]
}
},
{
"op": "replace",
"path": "/transactions/0/description",
"value": __('Order {order} for {event}').format(
event=request.event.name,
order=order.code
)
}
},
{
"op": "replace",
"path": "/transactions/0/description",
"value": __('Order {order} for {event}').format(
event=request.event.name,
order=order.code
)
}
])
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
])
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
order.refresh_from_db()
if payment.state == 'pending':
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
'payment completed.'))
@@ -199,6 +201,10 @@ class Paypal(BasePaymentProvider):
logger.error('Invalid state: %s' % str(payment))
return
if order.status == Order.STATUS_PAID:
logger.warning('PayPal success event even though order is already marked as paid')
return
try:
mark_order_paid(order, 'paypal', json.dumps(payment.to_dict()))
except Quota.QuotaExceededException as e:

View File

@@ -71,11 +71,14 @@ def webhook(request, *args, **kwargs):
prov.init_api()
# We do not check the signature, we just use it as a trigger to look the charge up.
if event_json['resource_type'] != 'sale':
if event_json['resource_type'] not in ('sale', 'refund'):
return HttpResponse("Not interested in this resource type", status=200)
try:
sale = paypalrestsdk.Sale.find(event_json['resource']['id'])
if event_json['resource_type'] == 'sale':
sale = paypalrestsdk.Sale.find(event_json['resource']['id'])
else:
sale = paypalrestsdk.Sale.find(event_json['resource']['sale_id'])
except:
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
return HttpResponse('Sale not found', status=500)

View File

@@ -20,7 +20,8 @@ class MailForm(forms.Form):
self.fields['message'] = I18nFormField(
widget=I18nTextarea, required=True,
langcodes=event.settings.get('locales'),
help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}")
help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}, "
"{invoice_name}, {invoice_company}")
)
choices = list(Order.STATUS_CHOICE)
if not event.settings.get('payment_term_expire_automatically', as_type=bool):

View File

@@ -1,4 +1,4 @@
url(r'^webhook/$', webhook, name='webhook'),{% load i18n %}
{% load i18n %}
{% if payment_info %}
{% if order.status == "p" %}

View File

@@ -9,6 +9,7 @@ from django.core.files import File
from django.core.files.storage import default_storage
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Order
from pretix.base.ticketoutput import BaseTicketOutput
from pretix.control.forms import ExtFileField
@@ -20,27 +21,11 @@ class PdfTicketOutput(BaseTicketOutput):
verbose_name = _('PDF output')
download_button_text = _('PDF')
def generate(self, op):
def _draw_page(self, p, op, order):
from reportlab.graphics.shapes import Drawing
from reportlab.pdfgen import canvas
from reportlab.lib import pagesizes, units
from reportlab.lib import units
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics import renderPDF
from PyPDF2 import PdfFileWriter, PdfFileReader
order = op.order
pagesize = self.settings.get('pagesize', default='A4')
if hasattr(pagesizes, pagesize):
pagesize = getattr(pagesizes, pagesize)
else:
pagesize = pagesizes.A4
orientation = self.settings.get('orientation', default='portrait')
if hasattr(pagesizes, orientation):
pagesize = getattr(pagesizes, orientation)(pagesize)
buffer = BytesIO()
p = canvas.Canvas(buffer, pagesize=pagesize)
event_s = self.settings.get('event_s', default=22, as_type=float)
if event_s:
@@ -102,8 +87,41 @@ class PdfTicketOutput(BaseTicketOutput):
p.showPage()
def generate_order(self, order: Order):
buffer = BytesIO()
p = self._create_canvas(buffer)
for op in order.positions.all():
self._draw_page(p, op, order)
p.save()
outbuffer = self._render_with_background(buffer)
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
def generate(self, op):
buffer = BytesIO()
p = self._create_canvas(buffer)
order = op.order
self._draw_page(p, op, order)
p.save()
outbuffer = self._render_with_background(buffer)
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
def _create_canvas(self, buffer):
from reportlab.pdfgen import canvas
from reportlab.lib import pagesizes
pagesize = self.settings.get('pagesize', default='A4')
if hasattr(pagesizes, pagesize):
pagesize = getattr(pagesizes, pagesize)
else:
pagesize = pagesizes.A4
orientation = self.settings.get('orientation', default='portrait')
if hasattr(pagesizes, orientation):
pagesize = getattr(pagesizes, orientation)(pagesize)
return canvas.Canvas(buffer, pagesize=pagesize)
def _render_with_background(self, buffer):
from PyPDF2 import PdfFileWriter, PdfFileReader
buffer.seek(0)
new_pdf = PdfFileReader(buffer)
output = PdfFileWriter()
@@ -121,7 +139,7 @@ class PdfTicketOutput(BaseTicketOutput):
outbuffer = BytesIO()
output.write(outbuffer)
outbuffer.seek(0)
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
return outbuffer
@property
def settings_form_fields(self) -> dict:

View File

@@ -41,7 +41,10 @@ def contextprocessor(request):
for receiver, response in html_head.send(request.event, request=request):
_html_head.append(response)
for receiver, response in footer_link.send(request.event, request=request):
_footer.append(response)
if isinstance(response, list):
_footer += response
else:
_footer.append(response)
if request.event.settings.presale_css_file:
ctx['css_file'] = default_storage.url(request.event.settings.presale_css_file)

View File

@@ -124,7 +124,11 @@
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-primary btn-lg" type="submit">
{% trans "Place binding order" %}
{% if cart.total > 0 %}
{% trans "Place binding order" %}
{% else %}
{% trans "Submit registration" %}
{% endif %}
</button>
</div>
<div class="clearfix"></div>

View File

@@ -113,13 +113,15 @@
{% endif %}
</div>
<div class="col-md-2 col-xs-6 availability-box">
<a href="#" data-toggle="variations" class="js-only">
{% trans "Show variants" %}
</a>
{% if not event.settings.show_variations_expanded %}
<a href="#" data-toggle="variations" class="js-only">
{% trans "Show variants" %}
</a>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
<div class="variations">
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
{% for var in item.available_variations %}
<div class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">

View File

@@ -73,6 +73,17 @@
You can download your tickets using the buttons below. Please have your ticket ready when entering the event.
{% endblocktrans %}
</div>
{% if cart.positions|length > 1 %}
<p>
{% trans "Download all tickets at once:" %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
class="btn btn-default btn-sm" data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
</p>
{% endif %}
{% elif not download_buttons %}
<div class="alert alert-info">
{% blocktrans trimmed with date=event.settings.ticket_download_date|date:"SHORT_DATE_FORMAT" %}

View File

@@ -1,7 +1,7 @@
{% extends "pretixpresale/organizers/base.html" %}
{% load i18n %}
{% load eventurl %}
{% block title %}Event list{% endblock %}
{% block title %}{% trans "Event list" %}{% endblock %}
{% block content %}
{% if "old" in request.GET %}
<h3>Past events</h3>
@@ -9,7 +9,7 @@
<small><a href="?">{% trans "Show upcoming" %}</a></small>
</p>
{% else %}
<h3>Upcoming events </h3>
<h3>{% trans "Upcoming events" %}</h3>
<p>
<small><a href="?old=1">{% trans "Show past events" %}</a></small>
</p>

View File

@@ -45,12 +45,16 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
pretix.presale.views.order.OrderPayChangeMethod.as_view(),
name='event.order.pay.change'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download.combined'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<position>[0-9]+)/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[0-9]+)$',
pretix.presale.views.order.InvoiceDownload.as_view(),
name='event.invoice.download'),
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'),
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
]

View File

@@ -1,5 +1,7 @@
from importlib import import_module
from urllib.parse import urljoin
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import resolve
from django.http import Http404
@@ -11,6 +13,8 @@ from pretix.base.models import Event, EventPermission, Organizer
from pretix.multidomain.urlreverse import get_domain
from pretix.presale.signals import process_request, process_response
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
def _detect_event(request, require_live=True):
url = resolve(request.path_info)
@@ -59,8 +63,27 @@ def _detect_event(request, require_live=True):
LocaleMiddleware().process_request(request)
if require_live and not request.event.live:
if not request.user.is_authenticated or not EventPermission.objects.filter(
event=request.event, user=request.user).exists():
can_access = (
url.url_name == 'event.auth'
or (
request.user.is_authenticated
and (
request.user.is_superuser
or EventPermission.objects.filter(event=request.event, user=request.user).exists()
)
)
)
if not can_access and 'pretix_event_access_{}'.format(request.event.pk) in request.session:
sparent = SessionStore(request.session.get('pretix_event_access_{}'.format(request.event.pk)))
try:
parentdata = sparent.load()
except:
pass
else:
can_access = 'event_access' in parentdata
if not can_access:
raise PermissionDenied(_('The selected ticket shop is currently not available.'))
for receiver, response in process_request.send(request.event, request=request):

View File

@@ -42,8 +42,10 @@ class CartActionMixin:
amount = int(value)
except ValueError:
raise CartError(_('Please enter numbers only.'))
if amount <= 0:
if amount < 0:
raise CartError(_('Please enter positive numbers only.'))
elif amount == 0:
return
price = self.request.POST.get('price_' + "_".join(parts[1:]), "")
if key.startswith('item_'):

View File

@@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
from pretix.base.models import CartPosition
from pretix.base.services.cart import CartError
from pretix.base.signals import validate_cart
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.checkoutflow import get_checkout_flow
@@ -12,12 +14,20 @@ from pretix.presale.checkoutflow import get_checkout_flow
class CheckoutView(View):
def dispatch(self, request, *args, **kwargs):
self.request = request
has_cart = CartPosition.objects.filter(
cart_id=self.request.session.session_key, event=self.request.event).exists()
if not has_cart and "async_id" not in request.GET:
cart_pos = CartPosition.objects.filter(
cart_id=self.request.session.session_key, event=self.request.event
)
if not cart_pos.exists() and "async_id" not in request.GET:
messages.error(request, _("Your cart is empty"))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
try:
validate_cart.send(sender=self.request.event, positions=cart_pos)
except CartError as e:
messages.error(request, str(e))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
flow = get_checkout_flow(self.request.event)
for step in flow:
if not step.is_applicable(request):

View File

@@ -1,13 +1,24 @@
import sys
from importlib import import_module
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Prefetch, Q
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from pretix.base.models import ItemVariation
from pretix.multidomain.urlreverse import eventreverse
from . import CartMixin, EventViewMixin
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
def item_group_by_category(items):
return sorted(
@@ -90,3 +101,32 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
context['cart'] = self.get_cart()
context['frontpage_text'] = str(self.request.event.settings.frontpage_text)
return context
class EventAuth(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
s = SessionStore(request.POST.get('session'))
try:
data = s.load()
except:
raise PermissionDenied(_('Please go back and try again.'))
parent = data.get('pretix_event_access_{}'.format(request.event.pk))
sparent = SessionStore(parent)
try:
parentdata = sparent.load()
except:
raise PermissionDenied(_('Please go back and try again.'))
else:
if 'event_access' not in parentdata:
raise PermissionDenied(_('Please go back and try again.'))
request.session['pretix_event_access_{}'.format(request.event.pk)] = parent
return redirect(eventreverse(request.event, 'presale:event.index'))

View File

@@ -1,3 +1,5 @@
from datetime import timedelta
from django.contrib import messages
from django.db import transaction
from django.db.models import Sum
@@ -9,12 +11,12 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
from pretix.base.models.orders import InvoiceAddress
from pretix.base.models.orders import CachedCombinedTicket, InvoiceAddress
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
)
from pretix.base.services.orders import cancel_order
from pretix.base.services.tickets import generate
from pretix.base.services.tickets import generate, generate_order
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
)
@@ -508,7 +510,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
if not self.output or not self.output.is_enabled:
messages.error(request, _('You requested an invalid ticket output type.'))
return redirect(self.get_order_url())
if not self.order or not self.order_position:
if not self.order or ('position' in kwargs and not self.order_position):
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.order.status != Order.STATUS_PAID:
messages.error(request, _('Order is not paid.'))
@@ -519,6 +521,39 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
messages.error(request, _('Ticket download is not (yet) enabled.'))
return redirect(self.get_order_url())
if 'position' in kwargs:
return self._download_position()
else:
return self._download_order()
def _download_order(self):
try:
ct = CachedCombinedTicket.objects.filter(
order=self.order, provider=self.output.identifier
).last()
except CachedCombinedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedCombinedTicket.objects.create(
order=self.order, provider=self.output.identifier,
extension='', type='', file=None)
generate_order.apply_async(args=(self.order.id, self.output.identifier))
if 'ajax' in self.request.GET:
return HttpResponse('1' if ct and ct.file else '0')
elif not ct.file:
if now() - ct.created > timedelta(minutes=110):
generate_order.apply_async(args=(self.order.id, self.output.identifier))
return render(self.request, "pretixbase/cachedfiles/pending.html", {})
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, ct.extension
)
return resp
def _download_position(self):
try:
ct = CachedTicket.objects.filter(
order_position=self.order_position, provider=self.output.identifier
@@ -532,10 +567,12 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
extension='', type='', file=None)
generate.apply_async(args=(self.order_position.id, self.output.identifier))
if 'ajax' in request.GET:
if 'ajax' in self.request.GET:
return HttpResponse('1' if ct and ct.file else '0')
elif not ct.file:
return render(request, "pretixbase/cachedfiles/pending.html", {})
if now() - ct.created > timedelta(minutes=110):
generate.apply_async(args=(self.order_position.id, self.output.identifier))
return render(self.request, "pretixbase/cachedfiles/pending.html", {})
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(

View File

@@ -10,7 +10,7 @@ class OrganizerIndex(OrganizerViewMixin, ListView):
model = Event
context_object_name = 'events'
template_name = 'pretixpresale/organizers/index.html'
paginate_by = 1
paginate_by = 30
def get_queryset(self):
query = Q(is_public=True)

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