Compare commits

..

9 Commits

Author SHA1 Message Date
Raphael Michel
dc0b73bf19 Fix issues introduced in rebase 2016-11-27 17:13:26 +01:00
Raphael Michel
ed31f31c04 Added a test for the cart methods 2016-11-27 16:13:58 +01:00
Raphael Michel
b1e78b5b78 Fix failing tests 2016-11-27 16:13:58 +01:00
Raphael Michel
4e2d31154a Fix dummy lock function 2016-11-27 16:13:58 +01:00
Raphael Michel
2e5a598b5f Restructure checkout to reduce locking times 2016-11-27 16:13:58 +01:00
Raphael Michel
4b535b067a Move two calls out of the lock period in OrderChangeManager 2016-11-27 16:13:58 +01:00
Raphael Michel
4f6eb903c7 mark_order_paid: Only lock when necessary 2016-11-27 16:13:58 +01:00
Raphael Michel
4d916df7c0 Restructure add_to_cart 2016-11-27 16:13:57 +01:00
Raphael Michel
61a331493e Reduce locked timeframe in add_items_to_cart 2016-11-27 16:12:38 +01:00
633 changed files with 3560 additions and 11701 deletions

View File

@@ -21,4 +21,3 @@ 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,45 +1,49 @@
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 && \
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 && \
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 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
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
WORKDIR /
RUN dpkg-reconfigure locales && \
locale-gen C.UTF-8 && \
/usr/sbin/update-locale LANG=C.UTF-8
ENV LC_ALL C.UTF-8
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
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
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
RUN rm /etc/nginx/sites-enabled/default
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 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 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 mkdir /static && chown -R pretixuser:pretixuser /static /pretix /data
USER pretixuser
VOLUME ["/etc/pretix", "/data"]
RUN make production
EXPOSE 80
ENTRYPOINT ["pretix"]
CMD ["all"]

41
README.md Normal file
View File

@@ -0,0 +1,41 @@
pretix
======
[![Docs](https://readthedocs.org/projects/pretix/badge/?version=latest)](http://docs.pretix.eu/en/latest/)
[![Build Status](https://travis-ci.org/pretix/pretix.svg?branch=master)](https://travis-ci.org/pretix/pretix)
[![Coverage Status](https://img.shields.io/coveralls/pretix/pretix.svg)](https://coveralls.io/r/pretix/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.
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.
Since very recently we now have an [installation guide](https://docs.pretix.eu/en/latest/admin/installation/index.html)
in our documentation.
Contributing
------------
If you want to contribute to pretix, please read the [developer documentation](https://docs.pretix.eu/en/latest/development/index.html)
in our documentation. If you have any further questions, please do not hesitate to ask!
License
-------
The code in this repository is published under the terms of the Apache License.
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](https://pretix.eu) or contact Raphael directly.

View File

@@ -1,53 +0,0 @@
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/
.. image:: https://travis-ci.org/pretix/pretix.svg?branch=master
:target: https://travis-ci.org/pretix/pretix
.. image:: https://coveralls.io/repos/github/pretix/pretix/badge.svg?branch=master
:target: https://coveralls.io/r/pretix/pretix
Reinventing ticket presales, one bit at a time.
Project status & release cycle
------------------------------
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.
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
------------
If you want to contribute to pretix, please read the `developer documentation`_
in our documentation. If you have any further questions, please do not hesitate to ask!
License
-------
The code in this repository is published under the terms of the Apache License.
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.
.. _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,14 +48,6 @@ 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.celery_app worker -l info
exec celery -A pretix worker -l info
fi
if [ "$1" == "shell" ]; then

View File

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

View File

@@ -16,9 +16,7 @@ the files found before.
The file is expected to be in the INI format as specified in the `Python documentation`_.
The config file may contain the following sections (all settings are optional and have
default values). We suggest that you start from the examples given in one of the
installation tutorials.
The config file may contain the following sections (all settings are optional and have default values).
pretix settings
---------------
@@ -203,9 +201,6 @@ You can use an existing memcached server as pretix's caching backend::
If no memcached is configured, pretix will use Django's built-in local-memory caching method.
.. note:: If you use memcached and you deploy pretix across multiple servers, you should use *one*
shared memcached instance, not multiple ones, because cache invalidations would not be
propagated otherwise.
Redis
-----
@@ -243,19 +238,6 @@ RabbitMQ might be the better choice if you have a complex, multi-server, high-pe
but as you already should have a redis instance ready for session and lock storage, we recommend
redis for convenience. See the `Celery documentation`_ for more details.
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::
[sentry]
dsn=https://<key>:<secret>@sentry.io/<project>
``dsn``
You will be given this value by your sentry installation.
Secret length
-------------

View File

@@ -74,7 +74,7 @@ redis instance to be running on the same host. To avoid the hassle with network
recommend connecting to redis via a unix socket. To enable redis on unix sockets, add the following to your
``/etc/redis/redis.conf``::
unixsocket /var/run/redis/redis.sock
unixsocket /tmp/redis.sock
unixsocketperm 777
Now restart redis-server::
@@ -111,7 +111,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
datadir=/data
[database]
; Replace mysql with postgresql_psycopg2 for PostgreSQL
; Replace mysql with psycopg2 for PostgreSQL
backend=mysql
name=pretix
user=pretix
@@ -127,14 +127,14 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
host=172.17.0.1
[redis]
location=unix:///var/run/redis/redis.sock?db=0
location=unix:///tmp/redis.sock?db=0
; Remove the following line if you are unsure about your redis' security
; to reduce impact if redis gets compromised.
sessions=true
[celery]
backend=redis+socket:///var/run/redis/redis.sock?virtual_host=1
broker=redis+socket:///var/run/redis/redis.sock?virtual_host=2
backend=redis+socket:///tmp/redis.sock?virtual_host=1
broker=redis+socket:///tmp/redis.sock?virtual_host=2
See :ref:`email configuration <mail-settings>` to learn more about configuring mail features.
@@ -160,7 +160,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
ExecStart=/usr/bin/docker run --name %n -p 8345:80 \
-v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \
-v /tmp/redis.sock:/tmp/redis.sock \
-v /var/run/mysqld:/var/run/mysqld \
pretix/standalone all
ExecStop=/usr/bin/docker stop %n
@@ -168,7 +168,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
[Install]
WantedBy=multi-user.target
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following commands
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following comamnds
to enable and start the service::
# systemctl daemon-reload
@@ -235,26 +235,6 @@ 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

@@ -5,33 +5,51 @@ General remarks
Requirements
------------
To use pretix, you wull need the following things:
To use pretix, the most minimal setup consists of:
* **pretix** and the python packages it depends on
* An **WSGI application server** (we recommend gunicorn)
* A periodic task runner, e.g. ``cron``
* **A database**. This needs to be a SQL-based that is supported by Django. We highly recommend to either
go for **PostgreSQL** or **MySQL/MariaDB**. If you do not provide one, pretix will run on SQLite, which is useful
for evaluation and development purposes.
To run pretix, you will need **at least Python 3.4**. We only recommend installations on **Linux**, Windows is not
officially supported (but might work).
.. warning:: Do not ever use SQLite in production. It will break.
Optional requirements
---------------------
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption.
pretix is built in a way that makes many of the following requirements optional. However, performance or security might
be very low if you skip some of them, therefore they are only partly optional.
.. warning:: Do not ever run without SSL in production. Your users deserve encrypted connections and thanks to
`Let's Encrypt`_ SSL certificates can be obtained for free these days.
Database
A good SQL-based database to run on that is supported by Django. We highly recommend to either go for **PostgreSQL**
or **MySQL/MariaDB**.
If you do not provide one, pretix will run on SQLite, which is useful for evaluation and development purposes.
* A **redis** server. This will be used for caching, session storage and task queuing.
.. warning:: Do not ever use SQLite in production. It will break.
.. warning:: pretix can run without redis, however this is only intended for development and should never be
used in production.
Reverse proxy
pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix is capable of
doing this, having this handled by a proper web server like **nginx** or **Apache** will be much faster. Also, you
need a proxying web server in front to provide SSL encryption.
* Optionally: RabbitMQ or memcached. Both of them might provide speedups, but if they are not present,
redis will take over their job.
.. warning:: Do not ever run without SSL in production. Your users deserve encrypted connections and thanks to
`Let's Encrypt`_ SSL certificates can be obtained for free these days.
Task worker
When pretix has to do heavy stuff, it is better to offload it into a background process instead of having the
users connection wait. Therefore pretix provides a background service that can be used to work on those
longer-running tasks.
This requires at least Redis (and optionally RabbitMQ).
Redis
If you provide a redis instance, pretix is able to make use of it in the three following ways:
* Caching
* Fast session storage
* Queuing and result storage for the task worker queue
RabbitMQ
RabbitMQ can be used as a more advanced queue manager for the task workers if necessary.
.. _Let's Encrypt: https://letsencrypt.org/

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::
# adduser pretix --disabled-password --home /var/pretix
# sudo 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 (e.g. using ``sudo``); all lines prepended with a ``$`` symbol should be run by the unprivileged user.
``root`` user; 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 postgresql_psycopg2 for PostgreSQL
; Replace mysql with 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/1
broker=redis://127.0.0.1/2
backend=redis://127.0.0.1?virtual_host=1
broker=redis://127.0.0.1?virtual_host=2
See :ref:`email configuration <mail-settings>` to learn more about configuring mail features.
Install pretix from PyPI
------------------------
Install pretix from source
--------------------------
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,13 +116,14 @@ python installation::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
command if you're running PostgreSQL::
We now clone pretix and install its Python dependencies (replace ``mysql`` with ``postgres`` if you're running
PostgreSQL)::
(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``.
(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
We also need to create a data directory::
@@ -130,8 +131,8 @@ We also need to create a data directory::
Finally, we compile static files and translation data and create the database structure::
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ make production
(venv)$ python manage.py migrate
Start pretix as a service
@@ -153,7 +154,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
WorkingDirectory=/var/pretix/source/src
Restart=on-failure
[Install]
@@ -170,8 +171,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.celery_app worker -l info
WorkingDirectory=/var/pretix
ExecStart=/var/pretix/venv/bin/celery -A pretix worker -l info
WorkingDirectory=/var/pretix/source/src
Restart=on-failure
[Install]
@@ -190,7 +191,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 && python -m pretix runperiodic
15,45 * * * * export PATH=/var/pretix/venv/bin:$PATH && cd /var/pretix/source/src && ./manage.py runperiodic
The cronjob should run as the ``pretix`` user (``crontab -e -u pretix``).
@@ -226,15 +227,6 @@ 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;
@@ -262,27 +254,16 @@ 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)$ pip3 install -U pretix[mysql] gunicorn
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles
(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
# 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

@@ -38,7 +38,6 @@ extensions = [
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinxcontrib.httpdomain',
]
# Add any paths that contain templates here, relative to this directory.

View File

@@ -11,7 +11,7 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues
:members: periodic_task
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: validate_cart, order_paid, order_placed
:members: order_paid, order_placed
Frontend
--------
@@ -47,11 +47,11 @@ Backend
-------
.. automodule:: pretix.control.signals
:members: nav_event, html_head, quota_detail_html, nav_topbar, organizer_edit_tabs
:members: nav_event, html_head, quota_detail_html
.. automodule:: pretix.base.signals
:members: logentry_display, requiredaction_display
:members: logentry_display
Vouchers
""""""""

View File

@@ -70,6 +70,8 @@ The provider class
.. automethod:: is_allowed
.. automethod:: is_allowed_for_order
.. autoattribute:: payment_form_fields
.. automethod:: checkout_prepare

View File

@@ -42,13 +42,6 @@ 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
@@ -64,8 +57,6 @@ 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,6 +64,6 @@ The output class
.. automethod:: generate
.. automethod:: generate_order
.. autoattribute:: download_button_text
.. autoattribute:: download_button_icon

View File

@@ -8,12 +8,12 @@ Python code
Use `flake8`_ to check for conformance problems. The project includes a setup.cfg file
with a default configuration for flake8 that excludes migrations and other non-relevant
code parts. It also silences a few checks, e.g. ``N802`` (function names should be lowercase)
and increases the maximum line length to more than 79 characters. **However** you should
code parts. It also silences a few checks, e.g. ``N802`` (function names should be lowercase)
and increases the maximum line length to more than 79 characters. **However** you should
still name all your functions lowercase [#f1]_ and keep your lines short when possible.
* Our build server will reject all code violating other flake8 checks than the following:
* E123: closing bracket does not match indentation of opening brackets line
* F403: ``from module import *`` used; unable to detect undefined names
* F401: module imported but unused

View File

@@ -29,9 +29,6 @@ Organizers and events
.. autoclass:: pretix.base.models.EventPermission
:members:
.. autoclass:: pretix.base.models.RequiredAction
:members:
Items
-----
@@ -67,9 +64,6 @@ Carts and Orders
:members:
.. autoclass:: pretix.base.models.QuestionAnswer
:members:
.. autoclass:: pretix.base.models.Checkin
:members:
Logging

View File

@@ -17,9 +17,6 @@ External Dependencies
* ``pyvenv`` for Python 3 (Debian package: ``python3-venv``)
* ``libffi`` (Debian package: ``libffi-dev``)
* ``libssl`` (Debian package: ``libssl-dev``)
* ``libxml2`` (Debian package ``libxml2-dev``)
* ``libxslt`` (Debian package ``libxslt1-dev``)
* ``msgfmt`` (Debian package ``gettext``)
* ``git``
Your local python environment
@@ -80,7 +77,7 @@ and head to http://localhost:8000/
As we did not implement an overall front page yet, you need to go directly to
http://localhost:8000/control/ for the admin view or, if you imported the test
data as suggested above, to the event page at http://localhost:8000/bigevents/2017/
data as suggested above, to the event page at http://localhost:8000/mrmcd/2015/
.. _`checksandtests`:
@@ -121,8 +118,8 @@ Then execute ``python -m smtpd -n -c DebuggingServer localhost:1025``.
Working with translations
^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to translate new strings that are not yet known to the translation system,
you can use the following command to scan the source code for strings to be translated
If you want to translate new strings that are not yet known to the translation system,
you can use the following command to scan the source code for strings to be translated
and update the ``*.po`` files accordingly::
make localegen

View File

@@ -8,5 +8,4 @@ Contents:
admin/index
development/index
plugins/index

View File

@@ -1,13 +0,0 @@
Plugin documentation
====================
This part of the documentation contains information about available plugins
that can be used to extend pretix's functionality.
If you want to **create** a plugin, please go to the
:ref:`Developer documentation <pluginsetup>` instead.
.. toctree::
:maxdepth: 2
list
pretixdroid

View File

@@ -1,34 +0,0 @@
List of plugins
===============
The following plugins are shipped with pretix and are supported in the same
ways that pretix itself is:
* Bank transfer
* PayPal
* Stripe
* Check-in lists
* pretixdroid
* Report exporter
* Send out emails
* Statistics
* PDF ticket output
The following plugins are not shipped with pretix but are maintained by the
same team:
* `SEPA direct debit`_
* `Pages`_
* `Passbook/Wallet ticket output`_
* `Cartshare`_
The following plugins are from independent third-party authors, so we can make
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

@@ -1,174 +0,0 @@
pretixdroid HTTP API
====================
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
uses to communicate with the pretix server.
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
Redeems a ticket, i.e. checks the user in.
**Example request**:
.. sourcecode:: http
POST /pretixdroid/api/demoorga/democon/redeem/?key=ABCDEF HTTP/1.1
Host: demo.pretix.eu
Accept: application/json, text/javascript
Content-Type: application/x-www-form-urlencoded
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3
**Example successful response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: text/json
{
"status": "ok"
"version": 2
}
**Example error response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: text/json
{
"status": "error",
"reason": "already_redeemed",
"version": 2
}
Possible error reasons:
* ``unpaid`` - Ticket is not paid for or has been refunded
* ``already_redeemed`` - Ticket already has been redeemed
* ``unknown_ticket`` - Secret does not match a ticket in the database
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. http:get:: /pretixdroid/api/(organizer)/(event)/search/
Searches for a ticket.
At most 25 results will be returned. **Queries with less than 4 characters will always return an empty result set.**
**Example request**:
.. sourcecode:: http
GET /pretixdroid/api/demoorga/democon/search/?key=ABCDEF&query=Peter HTTP/1.1
Host: demo.pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: text/json
{
"results": [
{
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
"order": "ABCE6",
"item": "Standard ticket",
"variation": null,
"attendee_name": "Peter Higgs",
"redeemed": false,
"paid": true
},
...
],
"version": 2
}
:query query: Search query
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. http:get:: /pretixdroid/api/(organizer)/(event)/status/
Returns status information, such as the total number of tickets and the
number of performed checkins.
**Example request**:
.. sourcecode:: http
GET /pretixdroid/api/demoorga/democon/status/?key=ABCDEF HTTP/1.1
Host: demo.pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: text/json
{
"checkins": 17,
"total": 42,
"version": 2,
"event": {
"name": "Demo Converence",
"slug": "democon",
"date_from": "2016-12-27T17:00:00Z",
"date_to": "2016-12-30T18:00:00Z",
"timezone": "UTC",
"url": "https://demo.pretix.eu/demoorga/democon/",
"organizer": {
"name": "Demo Organizer",
"slug": "demoorga"
},
},
"items": [
{
"name": "T-Shirt",
"id": 1,
"checkins": 1,
"admission": False,
"total": 1,
"variations": [
{
"name": "Red",
"id": 1,
"checkins": 1,
"total": 12
},
{
"name": "Blue",
"id": 2,
"checkins": 4,
"total": 8
}
]
},
{
"name": "Ticket",
"id": 2,
"checkins": 15,
"admission": True,
"total": 22,
"variations": []
}
]
}
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. _pretixdroid Android app: https://github.com/pretix/pretixdroid

View File

@@ -1,4 +1,3 @@
-r ../src/requirements.txt
sphinx
sphinx-rtd-theme
sphinxcontrib-httpdomain

3
src/.gitignore vendored
View File

@@ -7,4 +7,5 @@ build/
dist/
*.egg-info/
*.bak
pretix/static/jsi18n/
static/jsi18n/

View File

@@ -1,13 +1,11 @@
include LICENSE
include README.rst
recursive-include pretix/static *
recursive-include pretix/static.dist *
recursive-include pretix/locale *
recursive-include pretix/base/templates *
recursive-include pretix/control/templates *
recursive-include pretix/presale/templates *
recursive-include pretix/plugins/banktransfer/templates *
recursive-include pretix/plugins/banktransfer/static *
recursive-include pretix/plugins/paypal/templates *
recursive-include pretix/plugins/pretixdroid/templates *
recursive-include pretix/plugins/pretixdroid/static *

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 "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "build/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*"
staticfiles: jsi18n
./manage.py collectstatic --noinput

View File

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

View File

@@ -12,13 +12,9 @@ class PretixBaseConfig(AppConfig):
from .services import export, mail, tickets, cart, orders, cleanup # NOQA
try:
from .celery_app import app as celery_app # NOQA
from .celery import app as celery_app # NOQA
except ImportError:
pass
default_app_config = 'pretix.base.PretixBaseConfig'
try:
import pretix.celery_app as celery # NOQA
except ImportError:
pass

View File

@@ -7,7 +7,6 @@ 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
@@ -51,7 +50,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').prefetch_related('invoices')
qs = self.event.orders.all().select_related('invoice_address')
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
tax_rates = self._get_all_tax_rates(qs)
@@ -59,7 +58,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'), _('Invoice numbers')
_('Payment date'), _('Payment type'), _('Payment method fee')
]
for tr in tax_rates:
@@ -87,7 +86,7 @@ class OrderListExporter(BaseExporter):
for order in qs.order_by('datetime'):
row = [
order.code,
localize(order.total),
str(order.total),
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -108,7 +107,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),
localize(order.payment_fee)
str(order.payment_fee)
]
for tr in tax_rates:
@@ -118,12 +117,11 @@ class OrderListExporter(BaseExporter):
taxrate_values['taxsum'] += order.payment_fee_tax_value
row += [
localize(taxrate_values['grosssum']),
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']),
localize(taxrate_values['taxsum']),
str(taxrate_values['grosssum']),
str(taxrate_values['grosssum'] - taxrate_values['taxsum']),
str(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

@@ -23,12 +23,11 @@ class BaseI18nModelForm(BaseModelForm):
"""
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
locales = kwargs.pop('locales', None)
super().__init__(*args, **kwargs)
if event or locales:
if event:
for k, field in self.fields.items():
if isinstance(field, I18nFormField):
field.widget.enabled_langcodes = event.settings.get('locales') if event else locales
field.widget.enabled_langcodes = event.settings.get('locales')
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
@@ -98,14 +97,12 @@ class SettingsForm(forms.Form):
)
def __init__(self, *args, **kwargs):
self.obj = kwargs.pop('obj', None)
self.locales = kwargs.pop('locales', None)
self.obj = kwargs.pop('obj')
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
if self.obj or self.locales:
for k, field in self.fields.items():
if isinstance(field, I18nFormField):
field.widget.enabled_langcodes = self.obj.settings.get('locales') if self.obj else self.locales
for k, field in self.fields.items():
if isinstance(field, I18nFormField):
field.widget.enabled_langcodes = self.obj.settings.get('locales')
def save(self):
"""

View File

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

View File

@@ -69,16 +69,14 @@ class LazyI18nString:
if isinstance(self.data, dict):
firstpart = lng.split('-')[0]
similar = [l for l in self.data.keys() if (l.startswith(firstpart + "-") or firstpart == l) and l != lng]
if self.data.get(lng):
similar = [l for l in self.data.keys() if l.startswith(firstpart + "-") or firstpart == l]
if lng in self.data and self.data[lng]:
return self.data[lng]
elif self.data.get(firstpart):
elif firstpart in self.data:
return self.data[firstpart]
elif similar and any([self.data.get(s) for s in similar]):
for s in similar:
if self.data.get(s):
return self.data.get(s)
elif self.data.get(settings.LANGUAGE_CODE):
elif similar:
return self.data[similar[0]]
elif settings.LANGUAGE_CODE in self.data and self.data[settings.LANGUAGE_CODE]:
return self.data[settings.LANGUAGE_CODE]
elif len(self.data):
return list(self.data.items())[0][1]
@@ -147,7 +145,6 @@ class I18nWidget(forms.MultiWidget):
data = []
first_enabled = None
any_filled = False
any_enabled_filled = False
if not isinstance(value, LazyI18nString):
value = LazyI18nString(value)
for i, lng in enumerate(self.langcodes):
@@ -161,13 +158,9 @@ class I18nWidget(forms.MultiWidget):
any_filled = any_filled or (lng in self.enabled_langcodes and dataline)
if not first_enabled and lng in self.enabled_langcodes:
first_enabled = i
if dataline:
any_enabled_filled = True
data.append(dataline)
if value and not isinstance(value.data, dict):
data[first_enabled] = value.data
elif value and not any_enabled_filled:
data[first_enabled] = value.localize(self.enabled_langcodes[0])
return data
def render(self, name, value, attrs=None):

View File

@@ -1,12 +0,0 @@
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

@@ -1,658 +0,0 @@
# -*- 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

@@ -1,212 +0,0 @@
# -*- 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

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2016-11-29 13:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0047_auto_20161126_1300'),
]
operations = [
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

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-08 16:47
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0048_auto_20161129_1330'),
('pretixdroid', '0002_auto_20161208_1644'),
]
state_operations = [
migrations.CreateModel(
name='Checkin',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('position', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pretixdroid_checkins', to='pretixbase.OrderPosition')),
],
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-21 17:05
from __future__ import unicode_literals
from django.db import migrations, models
def forwards(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()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0049_checkin'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='positionid',
field=models.PositiveIntegerField(default=1),
),
migrations.RunPython(
forwards, migrations.RunPython.noop
),
]

View File

@@ -1,219 +0,0 @@
# -*- 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

@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-21 17:20
from __future__ import unicode_literals
from django.db import migrations, models
import pretix.base.models.orders
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()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0050_orderposition_positionid'),
]
operations = [
migrations.RunPython(
invalidate_ticket_cache, migrations.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,
),
]

View File

@@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-31 15:33
from __future__ import unicode_literals
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0051_auto_20161221_1720'),
]
operations = [
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'),
),
]

View File

@@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-04 12:52
from __future__ import unicode_literals
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.validators
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0052_auto_20161231_1533'),
]
operations = [
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),
),
]

View File

@@ -1,34 +0,0 @@
# -*- 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

@@ -1,20 +0,0 @@
# -*- 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

@@ -1,34 +0,0 @@
# -*- 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

@@ -1,42 +0,0 @@
# -*- 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

@@ -1,23 +0,0 @@
# -*- 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

@@ -1,29 +0,0 @@
# -*- 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

@@ -1,28 +0,0 @@
# -*- 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

@@ -1,22 +0,0 @@
# -*- 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

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

View File

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

View File

@@ -71,4 +71,4 @@ class LoggedModel(models.Model, LoggingMixin):
:return: A QuerySet of LogEntry objects
"""
return self.logentries.all().select_related('user', 'event')
return self.logentries.all().select_related('user')

View File

@@ -1,10 +0,0 @@
from django.db import models
from django.utils.timezone import now
class Checkin(models.Model):
"""
A checkin object is created when a person enters the event.
"""
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins')
datetime = models.DateTimeField(default=now)

View File

@@ -1,18 +1,14 @@
import string
import uuid
from datetime import date, datetime, time
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.email import CustomSMTPBackend
@@ -20,7 +16,6 @@ from pretix.base.i18n import I18nCharField
from pretix.base.models.base import LoggedModel
from pretix.base.settings import SettingsProxy
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.daterange import daterange
from .auth import User
from .organizer import Organizer
@@ -51,8 +46,6 @@ 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
@@ -68,7 +61,7 @@ class Event(LoggedModel):
max_length=50, db_index=True,
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."),
"This is being used in addresses and bank transfer references."),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9.-]+$",
@@ -76,7 +69,7 @@ class Event(LoggedModel):
),
EventSlugBlacklistValidator()
],
verbose_name=_("Short form"),
verbose_name=_("Slug"),
)
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
permitted = models.ManyToManyField(User, through='EventPermission',
@@ -101,11 +94,6 @@ 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"),
@@ -164,12 +152,6 @@ class Event(LoggedModel):
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_range_display(self, tz=None) -> str:
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
@@ -226,85 +208,6 @@ class Event(LoggedModel):
else:
return get_connection(fail_silently=False)
@property
def payment_term_last(self):
tz = pytz.timezone(self.settings.timezone)
return make_aware(datetime.combine(
self.settings.get('payment_term_last', as_type=date),
time(hour=23, minute=59, second=59)
), tz)
def copy_data_from(self, other):
from . import ItemCategory, Item, Question, Quota
self.plugins = other.plugins
self.save()
category_map = {}
for c in ItemCategory.objects.filter(event=other):
category_map[c.pk] = c
c.pk = None
c.event = self
c.save()
item_map = {}
variation_map = {}
for i in Item.objects.filter(event=other).prefetch_related('variations'):
vars = list(i.variations.all())
item_map[i.pk] = i
i.pk = None
i.event = self
if i.picture:
i.picture.save(i.picture.name, i.picture)
if i.category_id:
i.category = category_map[i.category_id]
i.save()
for v in vars:
variation_map[v.pk] = v
v.pk = None
v.item = i
v.save()
for q in Quota.objects.filter(event=other).prefetch_related('items', 'variations'):
items = list(q.items.all())
vars = list(q.variations.all())
q.pk = None
q.event = self
q.save()
for i in items:
q.items.add(item_map[i.pk])
for v in vars:
q.variations.add(variation_map[v.pk])
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
items = list(q.items.all())
opts = list(q.options.all())
q.pk = None
q.event = self
q.save()
for i in items:
q.items.add(item_map[i.pk])
for o in opts:
o.pk = None
o.question = q
o.save()
for s in EventSetting.objects.filter(object=other):
s.object = self
s.pk = None
if s.value.startswith('file://'):
fi = default_storage.open(s.value[7:], 'rb')
nonce = get_random_string(length=8)
fname = '%s/%s/%s.%s.%s' % (
self.organizer.slug, self.slug, s.key, nonce, s.value.split('.')[-1]
)
newname = default_storage.save(fname, fi)
s.value = 'file://' + newname
s.save()
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
class EventPermission(models.Model):
"""
@@ -326,9 +229,7 @@ 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, 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)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE)
can_change_settings = models.BooleanField(
default=True,
verbose_name=_("Can change event settings")
@@ -373,42 +274,3 @@ class EventLock(models.Model):
event = models.CharField(max_length=36, primary_key=True)
date = models.DateTimeField(auto_now=True)
token = models.UUIDField(default=uuid.uuid4)
class RequiredAction(models.Model):
"""
Represents an action that is to be done by an admin. The admin will be
displayed a list of actions to do.
:param datatime: The timestamp of the required action
:type datetime: datetime
:param user: The user that performed the action
:type user: User
:param done: If this action has been completed or dismissed
:type done: bool
:param action_type: The type of action that has to be performed. This is
used to look up the renderer used to describe the action in a human-
readable way. This should be some namespaced value using dotted
notation to avoid duplicates, e.g.
``"pretix.plugins.banktransfer.incoming_transfer"``.
:type action_type: str
:param data: Arbitrary data that can be used by the log action renderer
:type data: str
"""
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
done = models.BooleanField(default=False)
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE)
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
class Meta:
ordering = ('datetime',)
def display(self, request):
from ..signals import requiredaction_display
for receiver, response in requiredaction_display.send(self.event, action=self, request=request):
if response:
return response
return self.action_type

View File

@@ -1,11 +1,6 @@
import json
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
class LogEntry(models.Model):
@@ -37,7 +32,7 @@ class LogEntry(models.Model):
data = models.TextField(default='{}')
class Meta:
ordering = ('-datetime',)
ordering = ('-datetime', )
def display(self):
from ..signals import logentry_display
@@ -46,87 +41,3 @@ class LogEntry(models.Model):
if response:
return response
return self.action_type
@cached_property
def display_object(self):
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event
if self.content_type.model_class() is Event:
return ''
co = self.content_object
a_map = None
a_text = None
if isinstance(co, Order):
a_text = _('Order {val}')
a_map = {
'href': reverse('control:event.order', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'code': co.code
}),
'val': co.code,
}
elif isinstance(co, Voucher):
a_text = _('Voucher {val}')
a_map = {
'href': reverse('control:event.voucher', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'voucher': co.id
}),
'val': co.code[:6],
}
elif isinstance(co, Item):
a_text = _('Product {val}')
a_map = {
'href': reverse('control:event.item', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'item': co.id
}),
'val': co.name,
}
elif isinstance(co, Quota):
a_text = _('Quota {val}')
a_map = {
'href': reverse('control:event.items.quotas.show', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'quota': co.id
}),
'val': co.name,
}
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
a_map = {
'href': reverse('control:event.items.categories.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'category': co.id
}),
'val': co.name,
}
elif isinstance(co, Question):
a_text = _('Question {val}')
a_map = {
'href': reverse('control:event.items.questions.show', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'question': co.id
}),
'val': co.question,
}
if a_text and a_map:
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
elif a_text:
return a_text
else:
return ''
@cached_property
def parsed_data(self):
return json.loads(self.data)

View File

@@ -1,21 +1,19 @@
import copy
import os
import string
from datetime import datetime
from datetime import date, datetime, time
from decimal import Decimal
from typing import List, Union
import pytz
from django.conf import settings
from django.db import models
from django.db.models import F
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from ..decimal import round_decimal
from .base import LoggedModel
from .base import CachedFile, LoggedModel
from .event import Event
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -229,7 +227,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__organizer=self.event.organizer, code=code).exists():
if not Order.objects.filter(event=self.event, code=code).exists():
self.code = code
return
@@ -269,21 +267,25 @@ class Order(LoggedModel):
def _can_be_paid(self) -> Union[bool, str]:
error_messages = {
'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."),
'late': _("The payment is too late to be accepted."),
}
if self.event.settings.get('payment_term_last'):
if now() > self.event.payment_term_last:
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
return True
tz = pytz.timezone(self.event.settings.timezone)
last_date = make_aware(datetime.combine(
self.event.settings.get('payment_term_last', as_type=date),
time(hour=23, minute=59, second=59)
), tz)
if now() > last_date:
return error_messages['late']
if not self.event.settings.get('payment_term_accept_late'):
return error_messages['late']
return self._is_still_available()
if self.status == self.STATUS_PENDING:
return True
else:
return self._is_still_available()
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
error_messages = {
@@ -437,7 +439,6 @@ class OrderPosition(AbstractPosition):
:param order: The order this position is a part of
:type order: Order
"""
positionid = models.PositiveIntegerField(default=1)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
@@ -463,12 +464,11 @@ class OrderPosition(AbstractPosition):
from . import Voucher
ops = []
for i, cartpos in enumerate(cp):
for cartpos in cp:
op = OrderPosition(order=order)
for f in AbstractPosition._meta.fields:
setattr(op, f.name, getattr(cartpos, f.name))
op._calculate_tax()
op.positionid = i + 1
op.save()
for answ in cartpos.answers.all():
answ.orderposition = op
@@ -476,10 +476,6 @@ class OrderPosition(AbstractPosition):
answ.save()
if cartpos.voucher:
Voucher.objects.filter(pk=cartpos.voucher.pk).update(redeemed=F('redeemed') + 1)
cartpos.voucher.log_action('pretix.voucher.redeemed', {
'order_code': order.code
})
cartpos.delete()
return ops
@@ -498,9 +494,6 @@ class OrderPosition(AbstractPosition):
def save(self, *args, **kwargs):
if self.tax_rate is None:
self._calculate_tax()
if self.pk is None:
while OrderPosition.objects.filter(secret=self.secret).exists():
self.secret = generate_position_secret()
return super().save(*args, **kwargs)
@@ -567,50 +560,7 @@ class InvoiceAddress(models.Model):
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'))
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}.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
)
class CachedTicket(models.Model):
order_position = models.ForeignKey(OrderPosition, on_delete=models.CASCADE)
cachedfile = models.ForeignKey(CachedFile, on_delete=models.CASCADE, null=True)
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=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)
def cachedticket_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)

View File

@@ -1,8 +1,5 @@
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 _
@@ -41,7 +38,7 @@ class Organizer(LoggedModel):
),
OrganizerSlugBlacklistValidator()
],
verbose_name=_("Short form"),
verbose_name=_("Slug"),
)
permitted = models.ManyToManyField(User, through='OrganizerPermission',
related_name="organizers")
@@ -79,10 +76,6 @@ 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
@@ -98,17 +91,11 @@ class OrganizerPermission(models.Model):
"""
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="organizer_perms", on_delete=models.CASCADE, null=True, blank=True)
invite_email = models.EmailField(null=True, blank=True)
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
user = models.ForeignKey(User, related_name="organizer_perms")
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

@@ -1,5 +1,3 @@
from decimal import Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
@@ -7,7 +5,6 @@ from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from ..decimal import round_decimal
from .base import LoggedModel
from .event import Event
from .items import Item, ItemVariation, Quota
@@ -43,11 +40,8 @@ class Voucher(LoggedModel):
:type block_quota: bool
:param allow_ignore_quota: If set to true, this voucher can be redeemed even if the event is sold out
:type allow_ignore_quota: bool
:param price_mode: Sets how this voucher affects a product's price. Can be ``none``, ``set``, ``subtract``
or ``percent``.
:type price_mode: str
:param value: The value by which the price should be modified in the way specified by ``price_mode``.
:type value: decimal.Decimal
:param price: If set, the voucher will allow the sale of associated items for this price
:type price: decimal.Decimal
:param item: If set, the item to sell
:type item: Item
:param variation: If set, the variation to sell
@@ -65,13 +59,6 @@ class Voucher(LoggedModel):
* You need to either select a quota or an item
* If you select an item that has variations but do not select a variation, you cannot set block_quota
"""
PRICE_MODES = (
('none', _('No effect')),
('set', _('Set product price to')),
('subtract', _('Subtract from product price')),
('percent', _('Reduce product price by (%)')),
)
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
@@ -111,15 +98,10 @@ class Voucher(LoggedModel):
"If activated, a holder of this voucher code can buy tickets, even if there are none left."
)
)
price_mode = models.CharField(
verbose_name=_("Price mode"),
max_length=100,
choices=PRICE_MODES,
default='none'
)
value = models.DecimalField(
verbose_name=_("Voucher value"),
price = models.DecimalField(
verbose_name=_("Set product price to"),
decimal_places=2, max_digits=10, null=True, blank=True,
help_text=_('If empty, the product will cost its normal price.')
)
item = models.ForeignKey(
Item, related_name='vouchers',
@@ -226,19 +208,3 @@ class Voucher(LoggedModel):
if self.valid_until and self.valid_until < now():
return False
return True
def calculate_price(self, original_price: Decimal) -> Decimal:
"""
Returns how the price given in original_price would be modified if this
voucher is applied, i.e. replaced by a different price or reduced by a
certain percentage. If the voucher does not modify the price, the
original price will be returned.
"""
if self.value is not None:
if self.price_mode == 'set':
return self.value
elif self.price_mode == 'subtract':
return original_price - self.value
elif self.price_mode == 'percent':
return round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
return original_price

View File

@@ -21,15 +21,6 @@ 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.
@@ -196,12 +187,8 @@ 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 = PaymentProviderForm(
form = Form(
data=(request.POST if request.method == 'POST' else None),
prefix='payment_%s' % self.identifier,
initial={
@@ -211,12 +198,6 @@ 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):
@@ -384,12 +365,8 @@ class BasePaymentProvider:
whether the user should be presented with an option to retry the payment. The default
implementation always returns False.
If you want to enable retrials for your payment method, the best is to just return
``self._is_still_available()`` from this method to disable it as soon as the method
gets disabled or the methods end date is reached.
The retry workflow is also used if a user switches to this payment method for an existing
order!
order! Therefore, they can only switch to your p
:param order: The order object
"""

View File

@@ -15,10 +15,11 @@ import time
from django.conf import settings
from django.db import transaction
from pretix.celery_app import app
from pretix.celery import app
class ProfiledTask(app.Task):
abstract = True
def __call__(self, *args, **kwargs):
@@ -42,6 +43,7 @@ class TransactionAwareTask(ProfiledTask):
Task class which is aware of django db transactions and only executes tasks
after transaction has been committed
"""
abstract = True
def apply_async(self, *args, **kwargs):
"""

View File

@@ -1,9 +1,11 @@
from collections import Counter
from datetime import datetime, timedelta
from decimal import Decimal
from typing import List, Optional
from typing import List
from celery.exceptions import MaxRetriesExceededError
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.i18n import LazyLocaleException
@@ -12,7 +14,7 @@ from pretix.base.models import (
)
from pretix.base.services.async import ProfiledTask
from pretix.base.services.locking import LockTimeoutException
from pretix.celery_app import app
from pretix.celery import app
class CartError(LazyLocaleException):
@@ -35,8 +37,6 @@ error_messages = {
'voucher_invalid': _('This voucher code is not known in our database.'),
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
'voucher_double': _('You already used this voucher code. Remove the associated line from your '
'cart if you want to use it for a different product.'),
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_required': _('You need a valid voucher code to order this product.'),
@@ -65,7 +65,7 @@ def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str, now
'variation': cp.variation_id,
'count': 1,
'price': cp.price,
'cp': cp,
'_cp': cp,
'voucher': cp.voucher.code if cp.voucher else None
})
positions.add(cp)
@@ -74,7 +74,7 @@ def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str, now
def _delete_expired(expired: List[CartPosition], now_dt: datetime) -> None:
for cp in expired:
if cp.expires <= now_dt:
if cp.expires <= now_dt: # Has not been extended
cp.delete()
@@ -85,8 +85,17 @@ def _check_date(event: Event, now_dt: datetime) -> None:
raise CartError(error_messages['ended'])
def _add_new_items(event: Event, items: List[dict],
cart_id: str, expiry: datetime, now_dt: datetime) -> Optional[str]:
def _parse_items_and_check_constraints(event: Event, items: List[dict], cart_id: str,
now_dt: datetime) -> Counter:
"""
This method does three things:
* Extend the item list with the database objects for the item, variation, etc.
* Check all constraints that are placed on the items, vouchers etc. to be valid and calculates the correct prices
* Return a counter object that contains the quota changes that are required to perform the operation
"""
err = None
# Fetch items from the database
@@ -99,6 +108,9 @@ def _add_new_items(event: Event, items: List[dict],
).select_related("item", "item__event").prefetch_related("quotas")
variations_cache = {v.id: v for v in variations_query}
quotadiff = Counter()
vouchers = Counter()
for i in items:
# Check whether the specified items are part of what we just fetched from the database
# If they are not, the user supplied item IDs which either do not exist or belong to
@@ -116,109 +128,162 @@ def _add_new_items(event: Event, items: List[dict],
try:
voucher = Voucher.objects.get(code=i.get('voucher').strip(), event=event)
if voucher.redeemed >= voucher.max_usages:
return error_messages['voucher_redeemed']
raise CartError(error_messages['voucher_redeemed'])
if voucher.valid_until is not None and voucher.valid_until < now_dt:
return error_messages['voucher_expired']
raise CartError(error_messages['voucher_expired'])
if not voucher.applies_to(item, variation):
return error_messages['voucher_invalid_item']
raise CartError(error_messages['voucher_invalid_item'])
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=voucher) & Q(event=event) &
(Q(expires__gte=now_dt) | Q(cart_id=cart_id))
Q(voucher=voucher) & Q(event=event) & Q(expires__gte=now_dt)
)
if 'cp' in i:
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['cp'].pk)
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['_cp'].pk)
v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count()
if v_avail < 1:
return error_messages['voucher_redeemed']
if i['count'] > v_avail:
return error_messages['voucher_redeemed_partial'] % v_avail
raise CartError(error_messages['voucher_redeemed'])
if i['count'] > v_avail - vouchers[voucher]:
raise CartError(error_messages['voucher_redeemed_partial'] % v_avail)
vouchers[voucher] += i['count']
except Voucher.DoesNotExist:
return error_messages['voucher_invalid']
raise CartError(error_messages['voucher_invalid'])
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
if voucher and voucher.quota and voucher.quota.pk not in [q.pk for q in quotas]:
return error_messages['voucher_invalid_item']
raise CartError(error_messages['voucher_invalid_item'])
if item.require_voucher and voucher is None:
return error_messages['voucher_required']
raise CartError(error_messages['voucher_required'])
if item.hide_without_voucher and (voucher is None or voucher.item is None or voucher.item.pk != item.pk):
return error_messages['voucher_required']
raise CartError(error_messages['voucher_required'])
if len(quotas) == 0 or not item.is_available() or (variation and not variation.active):
err = err or error_messages['unavailable']
continue
# Check that all quotas allow us to buy i['count'] instances of the object
quota_ok = i['count']
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
for quota in quotas:
avail = quota.availability()
if avail[1] is not None and avail[1] < i['count']:
# This quota is not available or less than i['count'] items are left, so we have to
# reduce the number of bought items
if avail[0] != Quota.AVAILABILITY_OK:
err = err or error_messages['unavailable']
else:
err = err or error_messages['in_part']
quota_ok = min(quota_ok, avail[1])
price = item.default_price if variation is None else (
variation.default_price if variation.default_price is not None else item.default_price)
if voucher:
price = voucher.calculate_price(price)
if voucher and voucher.price is not None:
price = voucher.price
else:
price = item.default_price if variation is None else (
variation.default_price if variation.default_price is not None else item.default_price)
if item.free_price and 'price' in i and i['price'] is not None and i['price'] != "":
custom_price = i['price']
if not isinstance(custom_price, Decimal):
custom_price = Decimal(custom_price.replace(",", "."))
if custom_price > 100000000:
return error_messages['price_too_high']
raise CartError(error_messages['price_too_high'])
price = max(custom_price, price)
# Create a CartPosition for as much items as we can
for k in range(quota_ok):
if 'cp' in i and i['count'] == 1:
# Recreating
cp = i['cp']
cp.expires = expiry
cp.price = price
cp.save()
# Check that all quotas allow us to buy i['count'] instances of the object
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
for quota in quotas:
quotadiff[quota] += i['count']
i['_quotas'] = quotas
else:
i['_quotas'] = []
i['_price'] = price
i['_item'] = item
i['_variation'] = variation
i['_voucher'] = voucher
if err:
raise CartError(err)
return quotadiff
def _check_quota_and_create_positions(event: Event, items: List[dict], cart_id: str, now_dt: datetime,
expiry: datetime, quotadiff: Counter):
"""
This method takes the modified items and the quotadiff from _parse_items_and_check_constraints
and then
* checks that the given quotas are available
* creates as many cart positions as possible
"""
err = None
quotas_ok = {}
cartpositions = []
with event.lock():
for quota, count in quotadiff.items():
avail = quota.availability(now_dt)
if avail[1] is not None and avail[1] < count:
# This quota is not available or less than i['count'] items are left, so we have to
# reduce the number of bought items
if avail[0] != Quota.AVAILABILITY_OK:
err = err or error_messages['unavailable']
else:
err = err or error_messages['in_part']
quotas_ok[quota] = min(count, avail[1])
else:
CartPosition.objects.create(
event=event, item=item, variation=variation,
price=price,
expires=expiry,
cart_id=cart_id, voucher=voucher
)
return err
quotas_ok[quota] = count
for i in items:
# Create a CartPosition for as much items as we can
requested_count = i['count']
available_count = requested_count
if i['_quotas']:
available_count = min(requested_count, min(quotas_ok[q] for q in i['_quotas']))
for q in i['_quotas']:
quotas_ok[q] -= available_count
for k in range(available_count):
if '_cp' in i and i['count'] == 1:
# Recreating an existing position
cp = i['_cp']
cp.expires = expiry
cp.price = i['_price']
cp.save()
else:
cartpositions.append(CartPosition(
event=event, item=i['_item'], variation=i['_variation'],
price=i['_price'],
expires=expiry,
cart_id=cart_id, voucher=i['_voucher']
))
CartPosition.objects.bulk_create(cartpositions)
if err:
raise CartError(err)
def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> None:
with event.lock() as now_dt:
_check_date(event, now_dt)
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
if sum(i['count'] for i in items) + existing > int(event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(error_messages['max_items'], (event.settings.max_items_per_order,))
now_dt = now()
_check_date(event, now_dt)
expiry = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
_extend_existing(event, cart_id, expiry, now_dt)
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
if sum(i['count'] for i in items) + existing > int(event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(error_messages['max_items'], (event.settings.max_items_per_order,))
expired = _re_add_expired_positions(items, event, cart_id, now_dt)
expiry = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
_extend_existing(event, cart_id, expiry, now_dt)
expired = _re_add_expired_positions(items, event, cart_id, now_dt)
try:
if items:
err = _add_new_items(event, items, cart_id, expiry, now_dt)
_delete_expired(expired, now_dt)
if err:
raise CartError(err)
quotadiff = _parse_items_and_check_constraints(event, items, cart_id, now_dt)
_check_quota_and_create_positions(event, items, cart_id, now_dt, expiry, quotadiff)
except CartError as e:
_delete_expired(expired, now_dt)
raise e
else:
_delete_expired(expired, now_dt)
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None) -> None:
"""
Adds a list of items to a user's cart.
@@ -259,7 +324,7 @@ def _remove_items_from_cart(event: Event, items: List[dict], cart_id: str) -> No
cp.delete()
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
def remove_items_from_cart(self, event: int, items: List[dict], cart_id: str=None) -> None:
"""
Removes a list of items from a user's cart.

View File

@@ -6,7 +6,7 @@ from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.async import ProfiledTask
from pretix.base.signals import register_data_exporters
from pretix.celery_app import app
from pretix.celery import app
@app.task(base=ProfiledTask)

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, localize
from django.utils.formats import date_format
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 (
@@ -25,7 +25,7 @@ from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
from pretix.base.services.async import TransactionAwareTask
from pretix.base.signals import register_payment_providers
from pretix.celery_app import app
from pretix.celery import app
from pretix.helpers.database import rolledback_transaction
@@ -252,13 +252,6 @@ 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())
@@ -313,7 +306,6 @@ 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),
@@ -328,16 +320,16 @@ def _invoice_generate_german(invoice, f):
total = Decimal('0.00')
for line in invoice.lines.all():
tdata.append((
Paragraph(line.description, styles['Normal']),
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + invoice.event.currency,
localize(line.gross_value) + " " + invoice.event.currency,
line.description,
lformat("%.2f", line.tax_rate) + " %",
lformat("%.2f", line.net_value) + " " + invoice.event.currency,
lformat("%.2f", 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'), '', '', localize(total) + " " + invoice.event.currency])
tdata.append([pgettext('invoice', 'Invoice total'), '', '', lformat("%.2f", 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))
@@ -369,10 +361,10 @@ def _invoice_generate_german(invoice, f):
tax = taxvalue_map[rate]
tdata.append((
'',
localize(rate) + " %",
localize((gross - tax)) + " " + invoice.event.currency,
localize(gross) + " " + invoice.event.currency,
localize(tax) + " " + invoice.event.currency,
lformat("%.2f", rate) + " %",
lformat("%.2f", (gross - tax)) + " " + invoice.event.currency,
lformat("%.2f", gross) + " " + invoice.event.currency,
lformat("%.2f", tax) + " " + invoice.event.currency,
))
if len(tdata) > 2:

View File

@@ -1,23 +1,18 @@
import logging
from typing import Any, Dict, Union
from typing import Any, Dict
import bleach
import cssutils
import markdown
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail import EmailMessage, get_connection
from django.template.loader import get_template
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, InvoiceAddress, Order
from pretix.celery_app import app
from pretix.base.models import Event, Order
from pretix.celery import app
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger('pretix.base.mail')
INVALID_ADDRESS = 'invalid-pretix-mail-address'
cssutils.log.setLevel(logging.CRITICAL)
class TolerantDict(dict):
@@ -30,7 +25,7 @@ class SendMailException(Exception):
pass
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
def mail(email: str, subject: str, template: str,
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None):
"""
@@ -43,7 +38,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
: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_map()`` call on the template.
``context`` will be used as the argument to a Python ``.format()`` call on the template.
:param context: The context for rendering the template (see ``template`` parameter)
@@ -63,84 +58,42 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
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:
body = body.format_map(TolerantDict(context))
body_md = bleach.linkify(bleach.clean(markdown.markdown(body), tags=bleach.ALLOWED_TAGS + [
'p',
]))
else:
tpl = get_template(template)
body = tpl.render(context)
body_md = bleach.linkify(markdown.markdown(body))
sender = event.settings.get('mail_from') if event else settings.MAIL_FROM
subject = str(subject)
body_plain = body
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
'body': body_md,
'color': '#8E44B3'
}
if event:
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)
body_plain += "\r\n\r\n-- \r\n"
body_plain += _(
body += "\r\n\r\n-- \r\n"
body += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)
if order:
htmlctx['order'] = order
body_plain += "\r\n"
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}
)
)
body_plain += "\r\n"
tpl = get_template('pretixbase/email/plainwrapper.html')
body_html = tpl.render(htmlctx)
return mail_send([email], subject, body_plain, body_html, sender, event.id if event else None, headers)
body += "\r\n"
body += _(
"You can view your order details at the following URL:\r\n{orderurl}."
).format(event=event.name, orderurl=build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}))
body += "\r\n"
return mail_send([email], subject, body, sender, event.id if event else None, headers)
@app.task
def mail_send_task(to: str, subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, headers=headers)
email.attach_alternative(inline_css(html), "text/html")
def mail_send_task(to: str, subject: str, body: str, sender: str, event: int=None, headers: dict=None) -> bool:
email = EmailMessage(subject, body, sender, to=to, headers=headers)
if event:
event = Event.objects.get(id=event)
backend = event.get_mail_backend()

View File

@@ -1,3 +1,4 @@
import contextlib
import json
import logging
from collections import Counter, namedtuple
@@ -7,7 +8,6 @@ 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
@@ -22,7 +22,7 @@ from pretix.base.models import (
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
User, Voucher,
)
from pretix.base.models.orders import CachedTicket, InvoiceAddress
from pretix.base.models.orders import InvoiceAddress
from pretix.base.payment import BasePaymentProvider
from pretix.base.services.async import ProfiledTask
from pretix.base.services.invoices import (
@@ -33,7 +33,7 @@ from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import (
order_paid, order_placed, periodic_task, register_payment_providers,
)
from pretix.celery_app import app
from pretix.celery import app
from pretix.multidomain.urlreverse import build_absolute_uri
error_messages = {
@@ -44,7 +44,6 @@ error_messages = {
'price_changed': _('The price of some of the items in your cart has changed in the '
'meantime. Please see below for details.'),
'internal': _("An internal error occured, please try again."),
'empty': _("Your cart is empty."),
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'not_started': _('The presale period for this event has not yet started.'),
@@ -52,6 +51,8 @@ error_messages = {
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
'number of times allowed. We removed this item from your cart.'),
'voucher_redeemed_partial': _('The voucher code used for one of the items in your cart can only be redeemed %d '
'more times. We removed this item from your cart.'),
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
'from your cart.'),
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
@@ -64,7 +65,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, mail_text='') -> Order:
force: bool=False, send_mail: bool=True, user: User=None) -> Order:
"""
Marks an order as paid. This sets the payment provider, info and date and returns
the order object.
@@ -82,14 +83,17 @@ 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
lock_func = order.event.lock
if order.status == order.STATUS_PENDING and order.expires > now() + timedelta(minutes=10):
# No lock necessary in this case. The 10 minute offset is just to be safe and prevent
# collisions with the cronjob.
@contextlib.contextmanager
def lock_func():
yield now()
with order.event.lock() as now_dt:
with lock_func() as now_dt:
can_be_paid = order._can_be_paid()
if not force and can_be_paid is not True:
raise Quota.QuotaExceededException(can_be_paid)
@@ -104,24 +108,14 @@ 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 or now_dt,
'date': date,
'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,
@@ -131,10 +125,7 @@ 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),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
'downloads': order.event.settings.get('ticket_download', as_type=bool)
},
order.event, locale=order.locale
)
@@ -190,7 +181,7 @@ def _cancel_order(order, user=None):
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
return order.pk
return order
class OrderError(LazyLocaleException):
@@ -205,6 +196,9 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition]):
"""
Checks constraints on all positions except quota
"""
err = None
_check_date(event, now_dt)
@@ -213,17 +207,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
err = err or error_messages['unavailable']
cp.delete()
continue
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
if cp.voucher:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
).exclude(pk=cp.pk)
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
if v_avail < 1:
err = err or error_messages['voucher_redeemed']
cp.delete() # Sorry!
continue
cp._quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
if cp.item.require_voucher and cp.voucher is None:
cp.delete()
@@ -243,7 +227,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
price = cp.item.default_price if cp.variation is None else (
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
if price is False or len(quotas) == 0:
if price is False or len(cp._quotas) == 0:
err = err or error_messages['unavailable']
cp.delete()
continue
@@ -253,7 +237,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
err = err or error_messages['voucher_expired']
cp.delete()
continue
price = cp.voucher.calculate_price(price)
if cp.voucher.price is not None:
price = cp.voucher.price
if price != cp.price and not (cp.item.free_price and cp.price > price):
positions[i] = cp
@@ -261,53 +246,126 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.save()
err = err or error_messages['price_changed']
continue
quota_ok = True
ignore_all_quotas = cp.expires >= now_dt or (
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
if not ignore_all_quotas:
for quota in quotas:
if cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id == quota.pk:
continue
avail = quota.availability(now_dt)
if avail[0] != Quota.AVAILABILITY_OK:
# This quota is sold out/currently unavailable, so do not sell this at all
err = err or error_messages['unavailable']
quota_ok = False
break
if quota_ok:
positions[i] = cp
cp.expires = now_dt + timedelta(
minutes=event.settings.get('reservation_time', as_type=int))
cp.save()
else:
cp.delete() # Sorry!
if err:
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,
payment_provider: BasePaymentProvider, expires: datetime, locale: str=None, address: int=None,
meta_info: dict=None):
from datetime import date, time
total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total)
total += payment_fee
tz = pytz.timezone(event.settings.timezone)
exp_by_date = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
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)
expires = exp_by_date
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_placed.send(event, order=order)
return order
def _check_quota_on_expired_positions(event: Event, positions: List[CartPosition], now_dt: datetime):
err = None
quotadiff = Counter()
vouchers = Counter()
for cp in positions:
if not cp.id:
continue
ignore_all_quotas = cp.expires >= now_dt or (
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
if ignore_all_quotas:
cp._quotas = []
elif cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id:
cp._quotas = [q for q in cp._quotas if cp.voucher.quota_id != q.pk]
for quota in cp._quotas:
quotadiff[quota] += 1
quotas_ok = {}
for quota, count in quotadiff.items():
avail = quota.availability(now_dt)
if avail[1] is not None and avail[1] < count:
# This quota is not available or less than items are than requested left, so we have to
# reduce the number of bought items
if avail[0] != Quota.AVAILABILITY_OK:
err = err or error_messages['unavailable']
else:
err = err or error_messages['in_part']
quotas_ok[quota] = min(count, avail[1])
else:
quotas_ok[quota] = count
for cp in positions:
if not cp.id:
continue
if cp.voucher:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp2.pk for cp2 in positions])
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
if v_avail < 1:
err = err or error_messages['voucher_redeemed']
cp.delete() # Sorry!
continue
if v_avail - vouchers[cp.voucher] < 1:
err = err or (error_messages['voucher_redeemed_partial'] % v_avail)
cp.delete() # Sorry!
continue
vouchers[cp.voucher] += 1
if cp._quotas:
if min(quotas_ok[q] for q in cp._quotas) > 0:
cp.expires = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
cp.save()
for q in cp._quotas:
quotas_ok[q] -= 1
else:
cp.delete()
if err:
raise OrderError(err)
def _calculate_expiry(event: Event, now_dt: datetime):
from datetime import date, time
tz = pytz.timezone(event.settings.timezone)
expires = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
expires = expires.replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if expires.weekday() == 5:
expires += timedelta(days=2)
elif expires.weekday() == 6:
expires += timedelta(days=1)
if event.settings.get('payment_term_last'):
last_date = make_aware(datetime.combine(
@@ -317,37 +375,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
if last_date < expires:
expires = last_date
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
order.log_action('pretix.event.order.placed')
order_placed.send(event, order=order)
return order
return expires
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
@@ -363,15 +391,18 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
if not pprov:
raise OrderError(error_messages['internal'])
now_dt = now()
positions = list(CartPosition.objects.filter(id__in=position_ids).select_related('item', 'variation'))
if set(str(p) for p in position_ids) != set(str(p.id) for p in positions):
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions)
expires = _calculate_expiry(event, now_dt)
with event.lock() as now_dt:
positions = list(CartPosition.objects.filter(
id__in=position_ids).select_related('item', 'variation'))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions)
order = _create_order(event, email, positions, now_dt, pprov,
_check_quota_on_expired_positions(event, positions, now_dt)
order = _create_order(event, email, positions, now_dt, pprov, expires,
locale=locale, address=address, meta_info=meta_info)
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -383,14 +414,6 @@ 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,
@@ -403,9 +426,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
'order': order.code,
'secret': order.secret
}),
'paymentinfo': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'paymentinfo': str(pprov.order_pending_mail_render(order))
},
event, locale=order.locale
)
@@ -434,39 +455,29 @@ 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'):
eventsettings = eventcache.get(o.event.pk, None)
if eventsettings is None:
eventsettings = o.event.settings
eventcache[o.event.pk] = eventsettings
settings = eventcache.get(o.event.pk, None)
if settings is None:
settings = o.event.settings
eventcache[o.event.pk] = settings
days = eventsettings.get('mail_days_order_expire_warning', as_type=int)
tz = pytz.timezone(eventsettings.get('timezone', settings.TIME_ZONE))
days = settings.get('mail_days_order_expire_warning', as_type=int)
if days and (o.expires - today).days <= days:
o.expiry_reminder_sent = True
o.save()
try:
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
)
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
)
except SendMailException:
logger.exception('Reminder email could not be sent')
else:
@@ -540,7 +551,6 @@ class OrderChangeManager:
if isinstance(op, self.ItemOperation):
self.order.log_action('pretix.event.order.changed.item', user=self.user, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'new_item': op.item.pk,
@@ -556,7 +566,6 @@ class OrderChangeManager:
elif isinstance(op, self.PriceOperation):
self.order.log_action('pretix.event.order.changed.price', user=self.user, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_price': op.position.price,
'new_price': op.price
})
@@ -566,7 +575,6 @@ class OrderChangeManager:
elif isinstance(op, self.CancelOperation):
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'old_price': op.position.price,
@@ -597,12 +605,6 @@ 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,
@@ -612,8 +614,6 @@ 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
)
@@ -623,22 +623,18 @@ class OrderChangeManager:
# Do nothing
return
with transaction.atomic():
self._check_free_to_paid()
self._check_complete_cancel()
with self.order.event.lock():
if self.order.status != Order.STATUS_PENDING:
raise OrderError(self.error_messages['not_pending'])
self._check_free_to_paid()
self._check_quotas()
self._check_complete_cancel()
self._perform_operations()
self._recalculate_total_and_payment_fee()
self._reissue_invoice()
self._clear_tickets_cache()
self._check_paid_to_free()
self._notify_user()
def _clear_tickets_cache(self):
CachedTicket.objects.filter(order_position__order=self.order).delete()
def _get_payment_provider(self):
responses = register_payment_providers.send(self.order.event)
pprov = None
@@ -650,7 +646,7 @@ class OrderChangeManager:
raise OrderError(error_messages['internal'])
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
def perform_order(self, event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None):
try:
@@ -662,7 +658,7 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
return OrderError(error_messages['busy'])
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
def cancel_order(self, order: int, user: int=None):
try:
try:

View File

@@ -1,4 +1,4 @@
import os
from datetime import timedelta
from django.core.files.base import ContentFile
from django.utils.timezone import now
@@ -6,62 +6,34 @@ from django.utils.translation import ugettext as _
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, Order, OrderPosition,
CachedFile, CachedTicket, Event, Order, OrderPosition, cachedfile_name,
)
from pretix.base.services.async import ProfiledTask
from pretix.base.signals import register_ticket_outputs
from pretix.celery_app import app
from pretix.celery import app
from pretix.helpers.database import rolledback_transaction
@app.task(base=ProfiledTask)
def generate(order_position: str, provider: str):
order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position)
try:
ct = CachedTicket.objects.get(order_position=order_position, provider=provider)
except CachedTicket.MultipleObjectsReturned:
CachedTicket.objects.filter(order_position=order_position, provider=provider).delete()
ct = CachedTicket.objects.create(order_position=order_position, provider=provider, extension='',
type='', file=None)
except CachedTicket.DoesNotExist:
ct = CachedTicket.objects.create(order_position=order_position, provider=provider, extension='',
type='', file=None)
ct = CachedTicket.objects.get_or_create(order_position=order_position, provider=provider)[0]
if not ct.cachedfile:
cf = CachedFile()
cf.date = now()
cf.expires = order_position.order.event.date_from + timedelta(days=30)
cf.save()
ct.cachedfile = cf
ct.save()
with language(order_position.order.locale):
responses = register_ticket_outputs.send(order_position.order.event)
for receiver, response in responses:
prov = response(order_position.order.event)
if prov.identifier == provider:
filename, ct.type, data = prov.generate(order_position)
path, ext = os.path.splitext(filename)
ct.extension = ext
ct.save()
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))
ct.cachedfile.filename, ct.cachedfile.type, data = prov.generate(order_position)
ct.cachedfile.file.save(cachedfile_name(ct.cachedfile, ct.cachedfile.filename), ContentFile(data))
ct.cachedfile.save()
class DummyRollbackException(Exception):

View File

@@ -128,10 +128,6 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'show_variations_expanded': {
'default': 'False',
'type': bool
},
'ticket_download': {
'default': 'False',
'type': bool
@@ -235,8 +231,6 @@ 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}
@@ -305,10 +299,6 @@ Your {event} team"""))
'default': None,
'type': File
},
'invoice_logo_image': {
'default': None,
'type': File
},
'frontpage_text': {
'default': '',
'type': LazyI18nString
@@ -369,7 +359,7 @@ class SettingsProxy:
settings[key] = self.get(key)
return settings
def _unserialize(self, value: str, as_type: type, binary_file=False) -> Any:
def _unserialize(self, value: str, as_type: type) -> Any:
if as_type is None and value is not None and value.startswith('file://'):
as_type = File
@@ -385,7 +375,7 @@ class SettingsProxy:
return value == 'True'
elif as_type == File:
try:
fi = default_storage.open(value[7:], 'rb' if binary_file else 'r')
fi = default_storage.open(value[7:], 'r')
fi.url = default_storage.url(value[7:])
return fi
except OSError:
@@ -424,7 +414,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, binary_file=False):
def get(self, key: str, default=None, as_type: type=None):
"""
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
@@ -450,7 +440,7 @@ class SettingsProxy:
if value is None and default is not None:
value = default
return self._unserialize(value, as_type, binary_file=binary_file)
return self._unserialize(value, as_type)
def __getitem__(self, key: str) -> Any:
return self.get(key)

View File

@@ -52,18 +52,6 @@ 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=[]
)
@@ -94,18 +82,6 @@ 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"]
)
@@ -134,21 +110,7 @@ To display an instance of the ``LogEntry`` model to a human user,
``pretix.base.signals.logentry_display`` will be sent out with a ``logentry`` argument.
The first received response that is not ``None`` will be used to display the log entry
to the user. The receivers are expected to return plain text.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
requiredaction_display = EventPluginSignal(
providing_args=["action", "request"]
)
"""
To display an instance of the ``RequiredAction`` model to a human user,
``pretix.base.signals.requiredaction_display`` will be sent out with a ``action`` argument.
You will also get the current ``request`` in a different argument.
The first received response that is not ``None`` will be used to display the log entry
to the user. The receivers are expected to return HTML code.
to the user.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""

View File

@@ -1,166 +0,0 @@
{% load eventurl %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=false">
</head>
<style type="text/css">
body {
background-color: #e8e8e8;
background-position: top;
background-repeat: repeat-x;
font-family: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333;
margin: 0;
}
.header h1 {
margin-top: 20px;
margin-bottom: 5px;
}
.header h1 a {
text-decoration: none;
}
a {
color: {{ color }};
font-weight: bold;
}
a:hover, a:focus {
color: {{ color }};
text-decoration: underline;
}
a:hover, a:active {
outline: 0;
}
p {
margin: 0 0 10px;
/* These are technically the same, but use both */
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
.footer {
padding: 10px;
text-align: center;
font-size: 12px;
}
.content {
padding: 8px 18px 8px;
}
::selection {
background: {{ color }};
color: #FFF;
}
table {
width: 90%;
max-width: 900px;
border-spacing: 0px;
border-collapse: separate;
margin: auto;
}
@media (max-width: 480px) {
.header h1 {
font-size: 19px;
line-height: 24px;
margin: 0 9px 3px 0;
border-radius: 5px 5px;
-webkit-border-radius: 5px 5px;
-moz-border-radius: 5px 5px;
}
.header h1 a {
padding: 3px 9px;
display: block;
}
.header {
margin: 0;
padding: 12px 0 8px;
}
}
td.containertd {
background-color: #FFFFFF;
border: 1px solid #cccccc;
}
{% block addcss %}{% endblock %}
</style>
<body>
<table>
<tr>
<td class="header" background="">
{% if event %}
<h1><a href="{% eventurl event "presale:event.index" %}" target="_blank">{{ event.name }}</a></h1>
{% else %}
<h1><a href="{{ site_url }}" target="_blank">{{ site }}</a></h1>
{% endif %}
</td>
</tr>
<tr>
<td class="containertd">
<div class="content">
{{ body|safe }}
</div>
</td>
</tr>
{% if order %}
<tr>
<td class="gap"></td>
</tr>
<tr>
<td class="order containertd">
<div class="content">
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% eventurl event "presale:event.order" order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
</div>
</td>
</tr>
{% endif %}
<tr>
<td class="footer">
<div>
{% with 'target="blank" href="https://pretix.eu"'|safe as a_attr %}
{% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a>
{% endblocktrans %}
{% endwith %}
</div>
</td>
</tr>
</table>
<br/>
<br/>
</body>
</html>

View File

@@ -1,14 +1,11 @@
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, Order, OrderPosition
from pretix.base.models import Event, OrderPosition
from pretix.base.settings import SettingsSandbox
@@ -32,37 +29,13 @@ class BaseTicketOutput:
"""
return self.settings.get('_enabled', as_type=bool)
def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
def generate(self, order: 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
which is otherwise ignored.
filename, a file type and file content.
"""
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

@@ -8,7 +8,7 @@ from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils.translation import ugettext as _
from pretix.celery_app import app
from pretix.celery import app
logger = logging.getLogger('pretix.base.async')
@@ -17,7 +17,6 @@ class AsyncAction:
task = None
success_url = None
error_url = None
known_errortypes = []
def do(self, *args):
if not isinstance(self.task, app.Task):
@@ -54,7 +53,7 @@ class AsyncAction:
def _return_ajax_result(self, res, timeout=.5):
if not res.ready():
try:
res.get(timeout=timeout, propagate=False)
res.get(timeout=timeout)
except celery.exceptions.TimeoutError:
pass
@@ -119,13 +118,8 @@ class AsyncAction:
return redirect(self.get_error_url())
def get_error_message(self, exception):
if isinstance(exception, dict) and exception['exc_type'] in self.known_errortypes:
return exception['exc_message']
elif exception.__class__.__name__ in self.known_errortypes:
return str(exception)
else:
logger.error('Unexpected exception: %r' % exception)
return _('An unexpected error has occured.')
logger.error('Unexpected exception: %r' % exception)
return _('An unexpected error has occured.')
def get_success_message(self, value):
return _('The task has been completed.')

View File

@@ -1,4 +1,6 @@
from django.http import FileResponse, Http404, HttpRequest, HttpResponse
import os
from django.http import FileResponse, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.views.generic import TemplateView
@@ -11,17 +13,15 @@ class DownloadView(TemplateView):
@cached_property
def object(self) -> CachedFile:
try:
return get_object_or_404(CachedFile, id=self.kwargs['id'])
except ValueError: # Invalid URLs
raise Http404()
return get_object_or_404(CachedFile, id=self.kwargs['id'])
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if 'ajax' in request.GET:
return HttpResponse('1' if self.object.file else '0')
elif self.object.file:
resp = FileResponse(self.object.file.file, content_type=self.object.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename)
_, ext = os.path.splitext(self.object.filename)
resp['Content-Disposition'] = 'attachment; filename="{}{}"'.format(self.object.id, ext)
return resp
else:
return super().get(request, *args, **kwargs)

View File

@@ -1,7 +1,9 @@
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
from django.views.i18n import (
get_javascript_catalog, render_javascript_catalog, to_locale,
)
# Yes, we want to regenerate this every time the module has been imported to
# refresh the cache at least at every code deployment
@@ -19,5 +21,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(lang, 'djangojs', packages)
catalog, plural = get_javascript_catalog(to_locale(lang), 'djangojs', packages)
return render_javascript_catalog(catalog, plural)

View File

@@ -1,5 +1,3 @@
import hmac
from django.conf import settings
from django.http import HttpResponse
@@ -28,9 +26,9 @@ def serve_metrics(request):
user, passphrase = credentials.strip().decode("base64").split(":", 1)
if not hmac.compare_digest(user, settings.METRICS_USER):
if user != settings.METRICS_USER:
return unauthed_response()
if not hmac.compare_digest(passphrase, settings.METRICS_PASSPHRASE):
if passphrase != settings.METRICS_PASSPHRASE:
return unauthed_response()
# ok, the request passed the authentication-barrier, let's hand out the metrics:

29
src/pretix/celery.py Normal file
View File

@@ -0,0 +1,29 @@
import os
from celery import Celery
from celery.utils.mail import ErrorMail
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
from django.conf import settings
app = Celery('pretix')
class MyErrorMail(ErrorMail):
def should_send(self, context, exc):
from pretix.base.services.orders import OrderError
from pretix.base.services.cart import CartError
blacklist = (OrderError, CartError)
return not isinstance(exc, blacklist)
app.config_from_object('django.conf:settings')
app.conf.CELERY_ANNOTATIONS = {
'*': {
'ErrorMail': MyErrorMail,
}
}
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

View File

@@ -1,18 +0,0 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
from django.conf import settings
app = Celery('pretix')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
if hasattr(settings, 'RAVEN_CONFIG'):
# Celery signal registration
from raven.contrib.celery import register_signal
from raven.contrib.django.models import client
register_signal(client, ignore_expected=True)

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, nav_topbar
from .signals import html_head, nav_event
from .utils.i18n import get_javascript_format, get_moment_locale
@@ -37,11 +37,6 @@ 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

@@ -2,50 +2,19 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import ugettext_lazy as _
from pytz import common_timezones
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.i18n import I18nFormField, I18nTextarea
from pretix.base.models import Event, Organizer
from pretix.base.models import Event
from pretix.control.forms import ExtFileField
class EventWizardFoundationForm(forms.Form):
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=forms.CheckboxSelectMultiple,
help_text=_('Choose all languages that your event should be available in.')
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"),
queryset=Organizer.objects.filter(
id__in=self.user.organizer_perms.filter(can_create_events=True).values_list('organizer', flat=True)
),
widget=forms.RadioSelect,
empty_label=None,
required=True
)
class EventWizardBasicsForm(I18nModelForm):
class EventCreateForm(I18nModelForm):
error_messages = {
'duplicate_slug': _("You already used this slug for a different event. Please choose a new one."),
}
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
label=_("Default language"),
)
class Meta:
model = Event
@@ -56,8 +25,7 @@ class EventWizardBasicsForm(I18nModelForm):
'date_from',
'date_to',
'presale_start',
'presale_end',
'location',
'presale_end'
]
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -68,19 +36,7 @@ class EventWizardBasicsForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
self.locales = kwargs.get('locales')
kwargs.pop('user')
super().__init__(*args, **kwargs)
self.initial['timezone'] = get_current_timezone_name()
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
def clean(self):
data = super().clean()
if data['locale'] not in self.locales:
raise ValidationError({
'locale': _('Your default locale must also be enabled for your event (see box above).')
})
return data
def clean_slug(self):
slug = self.cleaned_data['slug']
@@ -92,24 +48,27 @@ class EventWizardBasicsForm(I18nModelForm):
return slug
class EventWizardCopyForm(forms.Form):
class EventCreateSettingsForm(SettingsForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Available langauges"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
label=_("Default language"),
)
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
kwargs.pop('locales')
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['copy_from_event'] = forms.ModelChoiceField(
label=_("Copy configuration from"),
queryset=Event.objects.filter(
id__in=self.user.event_perms.filter(
can_change_items=True, can_change_settings=True
).values_list('event', flat=True)
),
widget=forms.RadioSelect,
empty_label=_('Do not copy'),
required=False
)
def clean(self):
data = super().clean()
if data['locale'] not in data['locales']:
raise ValidationError({
'locale': _('Your default locale must also be enabled for your event (see box above).')
})
return data
class EventUpdateForm(I18nModelForm):
@@ -132,7 +91,6 @@ class EventUpdateForm(I18nModelForm):
'is_public',
'presale_start',
'presale_end',
'location',
]
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -331,8 +289,7 @@ class InvoiceSettingsForm(SettingsForm):
('False', _('No')),
('admin', _('Manually in admin panel')),
('user', _('Automatically on user request')),
('True', _('Automatically for all created orders')),
('paid', _('Automatically on payment')),
('True', _('Automatically for all created orders'))
)
)
invoice_address_from = forms.CharField(
@@ -364,12 +321,6 @@ 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):
@@ -387,32 +338,31 @@ class MailSettingsForm(SettingsForm):
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}, "
"{invoice_name}, {invoice_company}")
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}")
)
mail_text_order_paid = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}")
help_text=_("Available placeholders: {event}, {url}")
)
mail_text_order_free = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
help_text=_("Available placeholders: {event}, {url}")
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
help_text=_("Available placeholders: {event}, {url}")
)
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
help_text=_("Available placeholders: {event}, {url}")
)
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
@@ -431,7 +381,7 @@ class MailSettingsForm(SettingsForm):
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}")
help_text=_("Available placeholders: {event}, {url}, {expire_date}")
)
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
@@ -501,10 +451,6 @@ 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

@@ -1,12 +1,10 @@
import copy
from django import forms
from django.core.exceptions import ValidationError
from django.forms import BooleanField, ModelMultipleChoiceField
from django.forms.formsets import DELETION_FIELD_NAME
from django.utils.translation import ugettext as __, ugettext_lazy as _
from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.forms import I18nModelForm
from pretix.base.i18n import I18nFormField, I18nTextarea
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
@@ -155,28 +153,6 @@ class ItemUpdateForm(I18nModelForm):
}
class ItemVariationsFormSet(I18nFormSet):
def clean(self):
super().clean()
for f in self.forms:
if hasattr(f, '_delete_fail'):
f.fields['DELETE'].initial = False
f.fields['DELETE'].disabled = True
raise ValidationError(
message=_('The variation "%s" cannot be deleted because it has already been ordered by a user or '
'currently is in a users\'s cart. Please set the variation as "inactive" instead.'),
params=(str(f.instance),)
)
def _should_delete_form(self, form):
should_delete = super()._should_delete_form(form)
if should_delete and (form.instance.orderposition_set.exists() or form.instance.cartposition_set.exists()):
form._delete_fail = True
return False
return form.cleaned_data.get(DELETION_FIELD_NAME, False)
class ItemVariationForm(I18nModelForm):
class Meta:
model = ItemVariation

View File

@@ -21,9 +21,9 @@ class ExtendForm(I18nModelForm):
def clean(self):
data = super().clean()
data['expires'] = data['expires'].replace(hour=23, minute=59, second=59)
if data['expires'] < now():
raise ValidationError(_('The new expiry date needs to be in the future.'))
data['expires'] = data['expires'].replace(hour=23, minute=59, second=59)
return data
@@ -105,12 +105,6 @@ class OrderPositionChangeForm(forms.Form):
class OrderContactForm(forms.ModelForm):
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
help_text=_('Regenerates the order and ticket secrets. You will '
'need to re-send the link to the order page to the user and '
'the user will need to download his tickets again. The old '
'versions will be invalid.'))
class Meta:
model = Order
fields = ['email']

View File

@@ -22,8 +22,8 @@ class VoucherForm(I18nModelForm):
model = Voucher
localized_fields = '__all__'
fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
'comment', 'max_usages', 'price_mode'
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag',
'comment', 'max_usages'
]
widgets = {
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -187,8 +187,8 @@ class VoucherBulkForm(VoucherForm):
model = Voucher
localized_fields = '__all__'
fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
'max_usages', 'price_mode'
'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag', 'comment',
'max_usages'
]
widgets = {
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),

View File

@@ -5,14 +5,9 @@ from django.dispatch import receiver
from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from pretix.base.i18n import LazyI18nString
from pretix.base.models import Event, ItemVariation, LogEntry
from pretix.base.signals import logentry_display
OVERVIEW_BLACKLIST = [
'pretix.plugins.sendmail.order.email.sent'
]
def _display_order_changed(event: Event, logentry: LogEntry):
data = json.loads(logentry.data)
@@ -21,22 +16,18 @@ 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(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
old_item += ' - ' + str(event.itemvariations.get(pk=data['old_variation']))
new_item = str(event.items.get(pk=data['new_item']))
if 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', '?'),
new_item += ' - ' + str(event.itemvariations.get(pk=data['new_variation']))
return text + ' ' + _('{old_item} ({old_price} {currency}) changed to {new_item} ({new_price} {currency}).').format(
old_item=old_item, new_item=new_item,
old_price=formats.localize(Decimal(data['old_price'])),
new_price=formats.localize(Decimal(data['new_price'])),
currency=event.currency
)
elif logentry.action_type == 'pretix.event.order.changed.price':
return text + ' ' + _('Price of position #{posid} changed from {old_price} {currency} '
'to {new_price} {currency}.').format(
posid=data.get('positionid', '?'),
return text + ' ' + _('Price of a position changed from {old_price} {currency} to {new_price} {currency}.').format(
old_price=formats.localize(Decimal(data['old_price'])),
new_price=formats.localize(Decimal(data['new_price'])),
currency=event.currency
@@ -45,8 +36,7 @@ def _display_order_changed(event: Event, logentry: LogEntry):
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
return text + ' ' + _('Position #{posid} ({old_item}, {old_price} {currency}) removed.').format(
posid=data.get('positionid', '?'),
return text + ' ' + _('{old_item} ({old_price} {currency}) removed.').format(
old_item=old_item,
old_price=formats.localize(Decimal(data['old_price'])),
currency=event.currency
@@ -59,92 +49,44 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.modified': _('The order details have been modified.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.resend': _('The link to the order detail page has been resent to the user.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
'pretix.event.order.expirychanged': _('The order\'s expiry date has been changed.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.canceled': _('The order has been canceled.'),
'pretix.event.order.placed': _('The order has been created.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
'pretix.event.order.contact.changed': _('The email address has been changed.'),
'pretix.event.order.payment.changed': _('The payment method has been changed.'),
'pretix.event.order.expire_warning_sent': _('An email has been sent with a warning that the order is about '
'to expire.'),
'pretix.event.order.expire_warning_sent': _('An email has been sent with a warning that the order is about to expire.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
'from your account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.changed': _('The voucher has been modified.'),
'pretix.voucher.deleted': _('The voucher has been deleted.'),
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been modified.'),
'pretix.event.item.deleted': _('The product has been deleted.'),
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
'pretix.event.item.variation.changed': _('The variation "{value}" has been modified.'),
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been modified.'),
'pretix.event.category.added': _('The category has been added.'),
'pretix.event.category.deleted': _('The category has been deleted.'),
'pretix.event.category.changed': _('The category has been modified.'),
'pretix.event.question.added': _('The question has been added.'),
'pretix.event.question.deleted': _('The question has been deleted.'),
'pretix.event.question.changed': _('The question has been modified.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.plugins.enabled': _('A plugin has been enabled.'),
'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.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.')
}
data = json.loads(logentry.data)
if logentry.action_type.startswith('pretix.event.item.variation'):
if 'value' not in data:
# Backwards compatibility
var = ItemVariation.objects.filter(id=data['id']).first()
if var:
data['value'] = str(var.value)
else:
data['value'] = '?'
else:
data['value'] = LazyI18nString(data['value'])
if logentry.action_type in plains:
return plains[logentry.action_type].format_map(data)
return plains[logentry.action_type]
if logentry.action_type.startswith('pretix.event.order.changed'):
return _display_order_changed(sender, logentry)
if logentry.action_type.startswith('pretix.event.payment.provider.'):
return _('The settings of a payment provider have been changed.')
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
return _('The settings of a ticket output provider have been changed.')
if logentry.action_type == 'pretix.user.settings.2fa.device.added':
data = json.loads(logentry.data)
return _('A new two-factor authentication device "{name}" has been added to your account.').format(
name=data['name']
)
if logentry.action_type == 'pretix.user.settings.2fa.device.deleted':
data = json.loads(logentry.data)
return _('The two-factor authentication device "{name}" has been removed from your account.').format(
name=data['name']
)
if logentry.action_type == 'pretix.user.settings.changed':
data = json.loads(logentry.data)
text = str(_('Your account settings have been changed.'))
if 'email' in data:
text = text + ' ' + str(_('Your email address has been changed to {email}.').format(email=data['email']))

View File

@@ -9,9 +9,7 @@ from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_str
from django.utils.translation import ugettext as _
from pretix.base.models import (
Event, EventPermission, Organizer, OrganizerPermission,
)
from pretix.base.models import Event, EventPermission, Organizer
class PermissionMiddleware(MiddlewareMixin):
@@ -26,18 +24,15 @@ class PermissionMiddleware(MiddlewareMixin):
"auth.login.2fa",
"auth.register",
"auth.forgot",
"auth.forgot.recover",
"auth.invite",
"auth.forgot.recover"
)
def process_request(self, request):
url = resolve(request.path_info)
url_name = url.url_name
if not request.path.startswith(get_script_prefix() + 'control'):
# This middleware should only touch the /control subpath
return
if hasattr(request, 'organizer'):
# If the user is on a organizer's subdomain, he should be redirected to pretix
return redirect(urljoin(settings.SITE_URL, request.get_full_path()))
@@ -61,53 +56,29 @@ class PermissionMiddleware(MiddlewareMixin):
return redirect_to_login(
path, resolved_login_url, REDIRECT_FIELD_NAME)
events = Event.objects.all() if request.user.is_superuser else request.user.events
request.user.events_cache = events.order_by(
request.user.events_cache = request.user.events.order_by(
"organizer", "date_from").prefetch_related("organizer")
if 'event' in url.kwargs and 'organizer' in url.kwargs:
try:
if request.user.is_superuser:
request.event = Event.objects.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
request.eventperm = EventPermission(
event=request.event,
user=request.user
)
else:
request.event = Event.objects.filter(
slug=url.kwargs['event'],
permitted__id__exact=request.user.id,
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
request.eventperm = EventPermission.objects.get(
event=request.event,
user=request.user
)
request.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:
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
)
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
permitted__id__exact=request.user.id,
)[0]
except IndexError:
raise Http404(_("The selected organizer was not found or you "
"have no permission to administrate it."))

View File

@@ -14,8 +14,6 @@ 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,
@@ -30,7 +28,7 @@ def event_permission_required(permission):
allowed = getattr(perm, permission)
except AttributeError:
pass
if allowed or request.user.is_superuser:
if allowed:
return function(request, *args, **kw)
raise PermissionDenied(_('You do not have permission to view this content.'))
return wrapper
@@ -60,8 +58,6 @@ 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 ``request``.
navigation. 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
@@ -42,24 +42,6 @@ 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=[]
)
@@ -68,8 +50,8 @@ This signal is sent out to include widgets in the event dashboard. Receivers
should return a list of dictionaries, where each dictionary can have the keys:
* content (str, containing HTML)
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
(quarter of a row). May be ignored on small displays, default is "small")
* minimal width (int, widget width in 1/12ths of the page, default ist 3, can be
ignored on small displays)
* priority (int, used for ordering, higher comes first, default is 1)
* link (str, optional, if the full widget should be a link)
@@ -84,8 +66,8 @@ This signal is sent out to include widgets in the personal user dashboard. Recei
should return a list of dictionaries, where each dictionary can have the keys:
* content (str, containing HTML)
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
(quarter of a row). May be ignored on small displays, default is "small")
* minimal width (int, widget width in 1/12ths of the page, default ist 3, can be
ignored on small displays)
* priority (int, used for ordering, higher comes first, default is 1)
* link (str, optional, if the full widget should be a link)
@@ -122,15 +104,3 @@ 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

@@ -1,31 +0,0 @@
{% 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

@@ -24,9 +24,7 @@
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
@@ -74,35 +72,6 @@
</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

@@ -5,7 +5,7 @@
<h1>{% trans "Dashboard" %}</h1>
<div class="row dashboard">
{% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
<div class="col-xs-12 col-sm-{% if w.width > 6 %}12{% else %}6{% endif %} col-md-{{ w.width }}">
{% if w.url %}
<a href="{{ w.url }}" class="widget">
{{ w.content|safe }}

View File

@@ -1,16 +0,0 @@
{% 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

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

View File

@@ -1,25 +0,0 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Current issues" %}{% endblock %}
{% block inside %}
<h1>{% trans "Current issues" %}</h1>
<ul class="list-group">
{% for action in actions %}
<li class="list-group-item logentry">
<p>
<a href="{% url "control:event.requiredaction.discard" event=request.event.slug organizer=request.event.organizer.slug id=action.id %}"
class="btn btn-default btn-xs pull-right">
{% trans "Hide message" %}
</a>
<small><span class="fa fa-clock-o"></span> {{ action.datetime|date:"SHORT_DATETIME_FORMAT" }}</small>
</p>
{{ action.display|safe }}
</li>
{% empty %}
<div class="list-group-item">
<em>{% trans "No issues. Awesome!" %}</em>
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -98,28 +98,12 @@
{% endif %}
{% for nav in nav_event %}
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="has-children"{% endif %}>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}>
{% if nav.icon %}
<i class="fa fa-{{ nav.icon }} fa-fw"></i>
{% endif %}
{{ nav.label }}
</a>
{% if nav.children %}
<a href="#" class="arrow">
<span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
{% for item in nav.children %}
<li>
<a href="{{ item.url }}"
{% if item.active %}class="active"{% endif %}>
{{ item.label }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% endblock %}

View File

@@ -10,7 +10,6 @@
{% 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,62 +3,19 @@
{% load eventurl %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<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">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Your attention is required to resolve the following issues" %}
</h3>
</div>
<ul class="list-group">
{% for action in actions %}
<li class="list-group-item logentry">
<p>
<a href="{% url "control:event.requiredaction.discard" event=request.event.slug organizer=request.event.organizer.slug id=action.id %}"
class="btn btn-default btn-xs pull-right">
{% trans "Hide message" %}
</a>
<small><span class="fa fa-clock-o"></span> {{ action.datetime|date:"SHORT_DATETIME_FORMAT" }}</small>
</p>
{{ action.display|safe }}
</li>
{% endfor %}
</ul>
<div class="panel-footer">
<a href="{% url "control:event.requiredactions" event=request.event.slug organizer=request.event.organizer.slug %}">
{% trans "Show more" %}
</a>
</div>
</div>
{% endif %}
<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>
<div class="row dashboard">
{% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
{% if w.url %}{# backwards compatibility #}
<div class="col-xs-12 col-sm-{% if w.width > 6 %}12{% else %}6{% endif %} col-md-{{ w.width }}">
{% if w.url %}
<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 }}
@@ -67,43 +24,4 @@
</div>
{% endfor %}
</div>
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Event logs" %}
</h3>
</div>
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item logentry">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-12">
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
<span class="fa fa-user"></span> {{ log.user.get_full_name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">
{% if log.display_object %}
<span class="fa fa-flag"></span> {{ log.display_object|safe }}
{% endif %}
</div>
<div class="col-lg-6 col-sm-12 col-xs-12">
{{ log.display }}
</div>
</div>
</li>
{% endfor %}
</ul>
<div class="panel-footer">
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
{% trans "Show more logs" %}
</a>
</div>
</div>
{% endblock %}

View File

@@ -17,7 +17,6 @@
{% 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|safe }}</li>
<li>{{ issue }}</li>
{% endfor %}
</ul>
</div>

View File

@@ -1,54 +0,0 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Event logs" %}{% endblock %}
{% block inside %}
<h1>{% trans "Event logs" %}</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<p>
<select name="user" class="form-control">
<option value="">{% trans "All actions" %}</option>
<option value="yes" {% if request.GET.user == "yes" %}selected="selected"{% endif %}>
{% trans "Team actions" %}
</option>
<option value="no" {% if request.GET.user == "no" %}selected="selected"{% endif %}>
{% trans "Customer actions" %}
</option>
{% for up in userlist %}
<option value="{{ up.user_id }}" {% if request.GET.user == up.user_id %}selected="selected"{% endif %}>
{{ up.user }}
</option>
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item logentry">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-12">
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
<span class="fa fa-user"></span> {{ log.user.get_full_name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">
{% if log.display_object %}
<span class="fa fa-flag"></span> {{ log.display_object|safe }}
{% endif %}
</div>
<div class="col-lg-6 col-sm-12 col-xs-12">
{{ log.display }}
</div>
</div>
</li>
{% empty %}
<div class="list-group-item">
<em>{% trans "No results" %}</em>
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -26,16 +26,7 @@
<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.id }}{{ form.instance.user }}</td>
<td>{{ form.can_change_settings }}</td>
<td>{{ form.can_change_items }}</td>
<td>{{ form.can_view_orders }}</td>
@@ -46,19 +37,6 @@
<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">
@@ -75,7 +53,7 @@
<td>{{ add_form.can_change_vouchers }}</td>
<td>{{ add_form.can_view_vouchers }}</td>
</tr>
</tfoot>
</tbody>
</table>
</div>
</fieldset>

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