mirror of
https://github.com/pretix/pretix.git
synced 2026-08-19 12:16:26 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
673df7df80 | ||
|
|
9e9fc36b50 |
@@ -35,7 +35,6 @@ WORKDIR /pretix/src
|
||||
ADD deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
ENV DJANGO_SETTINGS_MODULE production_settings
|
||||
|
||||
RUN pip3 install -U pip wheel setuptools
|
||||
RUN pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
|
||||
-r requirements/memcached.txt -r requirements/redis.txt \
|
||||
-r requirements/py34.txt gunicorn
|
||||
|
||||
+7
-15
@@ -1,14 +1,9 @@
|
||||
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
|
||||
[](http://docs.pretix.eu/en/latest/)
|
||||
[](https://travis-ci.org/pretix/pretix)
|
||||
[](https://coveralls.io/r/pretix/pretix)
|
||||
|
||||
|
||||
Reinventing ticket presales, one bit at a time.
|
||||
@@ -25,11 +20,12 @@ like, but we try to keep the changes to documented APIs as small as possible. If
|
||||
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`_ in our documentation.
|
||||
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`_
|
||||
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
|
||||
@@ -42,8 +38,4 @@ AUTHORS file for a list of all the awesome folks who contributed to this project
|
||||
|
||||
This project is 100 percent free and open source software. If you are interested in
|
||||
commercial support, hosting services or supporting this project financially, please
|
||||
go to `pretix.eu`_ or contact Raphael directly.
|
||||
|
||||
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
|
||||
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html
|
||||
.. _pretix.eu: https://pretix.eu
|
||||
go to [pretix.eu](https://pretix.eu) or contact Raphael directly.
|
||||
+2
-16
@@ -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
|
||||
---------------
|
||||
@@ -207,6 +205,7 @@ If no memcached is configured, pretix will use Django's built-in local-memory ca
|
||||
shared memcached instance, not multiple ones, because cache invalidations would not be
|
||||
propagated otherwise.
|
||||
|
||||
|
||||
Redis
|
||||
-----
|
||||
|
||||
@@ -243,19 +242,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
|
||||
-------------
|
||||
|
||||
|
||||
@@ -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::
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -51,7 +51,7 @@ Backend
|
||||
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, requiredaction_display
|
||||
:members: logentry_display
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
@@ -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 bracket’s line
|
||||
* F403: ``from module import *`` used; unable to detect undefined names
|
||||
* F401: module imported but unused
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,5 +8,4 @@ Contents:
|
||||
|
||||
admin/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,30 +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:
|
||||
|
||||
* `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`_
|
||||
|
||||
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
|
||||
.. _Cartshare: https://github.com/pretix/pretix-cartshare
|
||||
.. _esPass ticket output: https://github.com/esPass/pretix-espass
|
||||
@@ -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
|
||||
@@ -1,4 +1,3 @@
|
||||
-r ../src/requirements.txt
|
||||
sphinx
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.bak
|
||||
pretix/static/jsi18n/
|
||||
static/jsi18n/
|
||||
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.0.0b1"
|
||||
__version__ = "0.0.0"
|
||||
|
||||
@@ -12,7 +12,7 @@ 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
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
+6
-13
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -1,9 +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,
|
||||
)
|
||||
from .event import Event, EventLock, EventPermission, EventSetting
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
@@ -1,17 +1,14 @@
|
||||
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
|
||||
@@ -19,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
|
||||
@@ -65,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.-]+$",
|
||||
@@ -73,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',
|
||||
@@ -156,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
|
||||
@@ -218,81 +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()
|
||||
|
||||
|
||||
class EventPermission(models.Model):
|
||||
"""
|
||||
@@ -359,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
|
||||
|
||||
@@ -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.show', 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)
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import copy
|
||||
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
|
||||
|
||||
@@ -272,7 +273,13 @@ class Order(LoggedModel):
|
||||
}
|
||||
|
||||
if self.event.settings.get('payment_term_last'):
|
||||
if now() > self.event.payment_term_last:
|
||||
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 self.status == self.STATUS_PENDING:
|
||||
return True
|
||||
@@ -433,7 +440,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"),
|
||||
@@ -459,12 +465,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
|
||||
@@ -472,10 +477,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
|
||||
|
||||
@@ -494,9 +495,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)
|
||||
|
||||
|
||||
@@ -563,28 +561,13 @@ 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}.pdf'.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
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
def cached_file_delete(sender, instance, **kwargs):
|
||||
if instance.cachedfile:
|
||||
instance.cachedfile.delete()
|
||||
|
||||
@@ -115,7 +115,7 @@ class Voucher(LoggedModel):
|
||||
verbose_name=_("Price mode"),
|
||||
max_length=100,
|
||||
choices=PRICE_MODES,
|
||||
default='none'
|
||||
default='set'
|
||||
)
|
||||
value = models.DecimalField(
|
||||
verbose_name=_("Voucher value"),
|
||||
@@ -234,7 +234,7 @@ class Voucher(LoggedModel):
|
||||
certain percentage. If the voucher does not modify the price, the
|
||||
original price will be returned.
|
||||
"""
|
||||
if self.value is not None:
|
||||
if self.value:
|
||||
if self.price_mode == 'set':
|
||||
return self.value
|
||||
elif self.price_mode == 'subtract':
|
||||
|
||||
@@ -365,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
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,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):
|
||||
@@ -218,7 +218,7 @@ def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> No
|
||||
raise CartError(err)
|
||||
|
||||
|
||||
@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 +259,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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import logging
|
||||
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, Order
|
||||
from pretix.celery_app import app
|
||||
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):
|
||||
@@ -68,42 +63,25 @@ def mail(email: str, subject: str, template: str,
|
||||
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
|
||||
|
||||
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 += _(
|
||||
body += "\r\n"
|
||||
body += _(
|
||||
"You can view your order details at the following URL:\n{orderurl}."
|
||||
).replace("\n", "\r\n").format(
|
||||
event=event.name, orderurl=build_absolute_uri(
|
||||
@@ -113,18 +91,13 @@ def mail(email: str, subject: str, template: str,
|
||||
}
|
||||
)
|
||||
)
|
||||
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"
|
||||
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()
|
||||
|
||||
@@ -32,7 +32,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 = {
|
||||
@@ -43,7 +43,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.'),
|
||||
@@ -346,8 +345,6 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
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)
|
||||
@@ -500,7 +497,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,
|
||||
@@ -516,7 +512,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
|
||||
})
|
||||
@@ -526,7 +521,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,
|
||||
@@ -602,7 +596,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:
|
||||
@@ -614,7 +608,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:
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedTicket, Event, Order, OrderPosition
|
||||
from pretix.base.models import (
|
||||
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))
|
||||
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):
|
||||
|
||||
@@ -110,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.
|
||||
"""
|
||||
|
||||
@@ -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>
|
||||
@@ -32,8 +32,7 @@ class BaseTicketOutput:
|
||||
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()
|
||||
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -67,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']
|
||||
@@ -91,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=True
|
||||
)
|
||||
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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
||||
from pretix.base.models.vouchers import _generate_random_code
|
||||
|
||||
|
||||
class VoucherForm(I18nModelForm):
|
||||
@@ -90,9 +91,8 @@ class VoucherForm(I18nModelForm):
|
||||
}
|
||||
)
|
||||
|
||||
if 'codes' in data:
|
||||
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||
cnt = len(data['codes']) * data['max_usages']
|
||||
if 'number' in data:
|
||||
cnt = data['number'] * data['max_usages']
|
||||
else:
|
||||
cnt = data['max_usages']
|
||||
|
||||
@@ -174,14 +174,30 @@ class VoucherForm(I18nModelForm):
|
||||
|
||||
|
||||
class VoucherBulkForm(VoucherForm):
|
||||
codes = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
label=_("Codes"),
|
||||
help_text=_(
|
||||
"Add one voucher code per line. We suggest that you copy this list and save it into a file."
|
||||
),
|
||||
number = forms.IntegerField(
|
||||
label=_("Number"),
|
||||
required=True
|
||||
)
|
||||
itemvar = forms.ChoiceField(
|
||||
label=_("Product"),
|
||||
widget=forms.RadioSelect
|
||||
)
|
||||
price_mode = forms.ChoiceField(
|
||||
choices=Voucher.PRICE_MODES,
|
||||
)
|
||||
has_valid_until = forms.BooleanField()
|
||||
value_percent = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2
|
||||
)
|
||||
value_subtract = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2
|
||||
)
|
||||
value_set = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
@@ -203,21 +219,35 @@ class VoucherBulkForm(VoucherForm):
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
|
||||
if Voucher.objects.filter(code__in=data['codes'], event=self.instance.event).exists():
|
||||
raise ValidationError(_('A voucher with one of this codes already exists.'))
|
||||
if data.get('has_valid_until', False) and not data.get('valid_until'):
|
||||
raise ValidationError(_('You did not specify an expiration date for the vouchers.'))
|
||||
|
||||
if data.get('price_mode', 'none') != 'none':
|
||||
if data.get('value_%s' % data['price_mode']) is None:
|
||||
raise ValidationError(_('You specified that the vouchers should modify the products price '
|
||||
'but did not specify a value.'))
|
||||
|
||||
return data
|
||||
|
||||
def save(self, event, *args, **kwargs):
|
||||
objs = []
|
||||
for code in self.cleaned_data['codes']:
|
||||
|
||||
codes = set()
|
||||
while len(codes) < self.cleaned_data['number']:
|
||||
new_codes = set()
|
||||
for i in range(min(self.cleaned_data['number'] - len(codes), 500)):
|
||||
# Work around SQLite's SQLITE_MAX_VARIABLE_NUMBER
|
||||
new_codes.add(_generate_random_code())
|
||||
new_codes -= set([v['code'] for v in Voucher.objects.filter(code__in=new_codes).values('code')])
|
||||
codes |= new_codes
|
||||
|
||||
for code in codes:
|
||||
obj = copy.copy(self.instance)
|
||||
obj.event = event
|
||||
obj.code = code
|
||||
data = dict(self.cleaned_data)
|
||||
data['code'] = code
|
||||
data['bulk'] = True
|
||||
del data['codes']
|
||||
obj.save()
|
||||
objs.append(obj)
|
||||
return objs
|
||||
|
||||
@@ -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)
|
||||
@@ -25,18 +20,14 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
new_item = str(event.items.get(pk=data['new_item']))
|
||||
if data['new_variation']:
|
||||
new_item += ' - ' + str(event.itemvariations.get(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', '?'),
|
||||
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,84 +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.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']))
|
||||
|
||||
@@ -30,11 +30,9 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
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()))
|
||||
|
||||
@@ -50,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)
|
||||
|
||||
@@ -66,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)
|
||||
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
<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>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/voucher.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{{ html_head|safe }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -9,39 +9,9 @@
|
||||
{% trans "Go to shop" %}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
<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 }}
|
||||
@@ -54,43 +24,4 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<p> </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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -13,8 +13,7 @@
|
||||
<div class="panel panel-default ticketoutput-panel">
|
||||
<div class="panel-heading">
|
||||
<a href="{% url "control:event.settings.tickets.preview" event=request.event.slug organizer=request.organizer.slug output=provider.identifier %}"
|
||||
class="btn btn-default btn-sm pull-right {% if not provider.preview_allowed %}disabled{% endif %}"
|
||||
target="_blank">
|
||||
class="btn btn-default btn-sm pull-right" target="_blank">
|
||||
{% trans "Preview" %}
|
||||
</a>
|
||||
<h3 class="panel-title">{{ provider.verbose_name }}</h3>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Create a new event" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Create a new event" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_field sform.locales layout="horizontal" %}
|
||||
{% bootstrap_field sform.locale layout="horizontal" %}
|
||||
{% bootstrap_field sform.timezone layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
{% bootstrap_field form.presale_start layout="horizontal" %}
|
||||
{% bootstrap_field form.presale_end layout="horizontal" %}
|
||||
</fieldset>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You will be able to adjust further settings in the next step.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Create a new event" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Create a new event" %} <small>{% blocktrans trimmed with step=wizard.steps.step1 %}
|
||||
Step {{ step }}
|
||||
{% endblocktrans %}</small></h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{{ wizard.management_form }}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% block form %}
|
||||
{% endblock %}
|
||||
<div class="form-group submit-group">
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
|
||||
class="btn btn-default btn-lg pull-left">
|
||||
{% trans "Back" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends "pretixcontrol/events/create_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_field form.locale layout="horizontal" %}
|
||||
{% bootstrap_field form.timezone layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
{% bootstrap_field form.presale_start layout="horizontal" %}
|
||||
{% bootstrap_field form.presale_end layout="horizontal" %}
|
||||
</fieldset>
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends "pretixcontrol/events/create_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block form %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Do you want to copy over your configuration from a different event? We will copy all
|
||||
products, categories, quotas, and questions as well as general event settings.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="alert alert-info">
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
Please make sure to review all settings extensively. You will probably still need to change some
|
||||
settings manually, e.g. date and time settings and texts that contain the event name.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
</div>
|
||||
{% bootstrap_field form.copy_from_event layout="horizontal" %}
|
||||
{% endblock %}
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends "pretixcontrol/events/create_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block form %}
|
||||
{% bootstrap_field form.organizer layout="horizontal" %}
|
||||
{% bootstrap_field form.locales layout="horizontal" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create a new event" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Create a new event" %}</h1>
|
||||
{% if organizers|length == 0 %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "You are not permitted to create new events in the name of any organizer." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{% trans "Please choose the organizer of this event from the list below." %}</p>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Organizer name" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in organizers %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:events.create" organizer=o.slug %}">{{ o.name }}</a>
|
||||
</strong></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -4,43 +4,29 @@
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.active layout="horizontal" %}
|
||||
{% bootstrap_field form.category layout="horizontal" %}
|
||||
{% bootstrap_field form.admission layout="horizontal" %}
|
||||
{% bootstrap_field form.description layout="horizontal" %}
|
||||
{% bootstrap_field form.picture layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Price settings" %}</legend>
|
||||
{% bootstrap_field form.default_price layout="horizontal" %}
|
||||
{% bootstrap_field form.tax_rate layout="horizontal" %}
|
||||
{% bootstrap_field form.free_price layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Availability" %}</legend>
|
||||
{% bootstrap_field form.available_from layout="horizontal" %}
|
||||
{% bootstrap_field form.available_until layout="horizontal" %}
|
||||
{% bootstrap_field form.require_voucher layout="horizontal" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="horizontal" %}
|
||||
{% bootstrap_field form.allow_cancel layout="horizontal" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Product history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=item %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.active layout="horizontal" %}
|
||||
{% bootstrap_field form.category layout="horizontal" %}
|
||||
{% bootstrap_field form.admission layout="horizontal" %}
|
||||
{% bootstrap_field form.description layout="horizontal" %}
|
||||
{% bootstrap_field form.picture layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Price settings" %}</legend>
|
||||
{% bootstrap_field form.default_price layout="horizontal" %}
|
||||
{% bootstrap_field form.tax_rate layout="horizontal" %}
|
||||
{% bootstrap_field form.free_price layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Availability" %}</legend>
|
||||
{% bootstrap_field form.available_from layout="horizontal" %}
|
||||
{% bootstrap_field form.available_until layout="horizontal" %}
|
||||
{% bootstrap_field form.require_voucher layout="horizontal" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="horizontal" %}
|
||||
{% bootstrap_field form.allow_cancel layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -7,25 +7,11 @@
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.description layout="horizontal" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Category history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=category %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.description layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -36,31 +36,31 @@
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<div class="row" id="question-stats">
|
||||
{% if not stats %}
|
||||
<div class="empty-collection col-md-10 col-xs-12">
|
||||
{% if not stats %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No matching answers found.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if not items %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No matching answers found.
|
||||
{% endblocktrans %}
|
||||
{% trans "You need to assign the question to a product to collect answers." %}
|
||||
</p>
|
||||
{% if not items %}
|
||||
<p>
|
||||
{% trans "You need to assign the question to a product to collect answers." %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-edit"></i> {% trans "Edit question" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-edit"></i> {% trans "Edit question" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row" id="question-stats">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<div class="chart" id="question_chart" data-type="{{ question.type }}">
|
||||
|
||||
</div>
|
||||
<script type="application/json" id="question-chart-data">{{ stats_json|safe }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -80,16 +80,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Question history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=question %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
<div class="row" id="quota-stats">
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<legend>{% trans "Usage overview" %}</legend>
|
||||
<div class="chart" id="quota_chart">
|
||||
|
||||
</div>
|
||||
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|safe }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<legend>{% trans "Availability calculation" %}</legend>
|
||||
|
||||
<div class="row">
|
||||
@@ -43,31 +43,6 @@
|
||||
{% if quota.size == None %}{% trans "Infinite" %}{% else %}{{ avail.1 }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if quota_overbooked > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed with num=quota_overbooked %}
|
||||
This quota is currently overbooked by {{ num }} tickets.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_ignore_vouchers %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
Your event contains vouchers that affect products covered by this quota and that
|
||||
allow a user to buy products even if this quota is sold out.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Quota history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=quota %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% eventsignal request.event "pretix.control.signals.quota_detail_html" quota=quota %}
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
#{{ position.positionid }} –
|
||||
<strong>{{ position.item.name }}</strong>
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
|
||||
@@ -158,15 +158,11 @@
|
||||
<div class="panel-body">
|
||||
{% for line in items.positions %}
|
||||
<div class="row-fluid product-row">
|
||||
<div class="col-md-9 col-xs-6">
|
||||
#{{ line.positionid }} –
|
||||
<div class="col-md-4 col-xs-6">
|
||||
<strong>{{ line.item.name }}</strong>
|
||||
{% if line.variation %}
|
||||
– {{ line.variation }}
|
||||
{% endif %}
|
||||
{% if line.checkins.all %}
|
||||
<span class="fa fa-check" data-toggle="tooltip" title="{% blocktrans trimmed with date=line.checkins.all.0.datetime|date:'d.m.Y H:i' %}First scanned: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}
|
||||
<a href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
@@ -188,8 +184,14 @@
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 count">
|
||||
{{ line.count }}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 price">
|
||||
<strong>{{ event.currency }} {{ line.price|floatformat:2 }}</strong>
|
||||
{{ event.currency }} {{ line.price|floatformat:2 }}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 price">
|
||||
<strong>{{ event.currency }} {{ line.total|floatformat:2 }}</strong>
|
||||
{% if line.tax_rate %}
|
||||
<br/>
|
||||
<small>{% blocktrans trimmed with rate=line.tax_rate %}
|
||||
|
||||
@@ -2,72 +2,139 @@
|
||||
{% load i18n %}
|
||||
{% load eventsignal %}
|
||||
{% load bootstrap3 %}
|
||||
{% load capture_tags %}
|
||||
{% block title %}{% trans "Voucher" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Create multiple voucher" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<h1>{% trans "Create new vouchers" %}</h1>
|
||||
<form action="" method="post" class="form-inline" id="voucher-create">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher codes" %}</legend>
|
||||
<div class="form-group">
|
||||
<div class="col-md-7 col-sm-12 col-md-offset-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-xs"
|
||||
id="voucher-bulk-codes-num"
|
||||
placeholder="{% trans "Number" %}">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="voucher-bulk-codes-generate"
|
||||
data-rng-url="{% url 'control:event.vouchers.rng' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Generate random codes" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-clipboard" data-clipboard-target="#id_codes">
|
||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
{% trans "Copy codes" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.codes layout="horizontal" %}
|
||||
{% capture as number_field silent %} {% bootstrap_field form.number layout="inline" %} {% endcapture %}
|
||||
{% capture as max_usages_field silent %} {% bootstrap_field form.max_usages layout="inline" %} {% endcapture %}
|
||||
{% capture as valid_until_field silent %} {% bootstrap_field form.valid_until layout="inline" %} {% endcapture %}
|
||||
{% capture as value_field_percent silent %} {% bootstrap_field form.value_percent layout="inline" %} {% endcapture %}
|
||||
{% capture as value_field_subtract silent %} {% bootstrap_field form.value_subtract layout="inline" %} {% endcapture %}
|
||||
{% capture as value_field_set silent %} {% bootstrap_field form.value_set layout="inline" %} {% endcapture %}
|
||||
|
||||
{% bootstrap_field form.max_usages layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.valid_until layout="horizontal" %}
|
||||
{% bootstrap_field form.block_quota layout="horizontal" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
<div class="wizard-step" id="step-number">
|
||||
<div class="wizard-step-inner">
|
||||
<h4>{% trans "How many vouchers do you want to create?" %}</h4>
|
||||
<div class="form-line">
|
||||
{% blocktrans trimmed %}
|
||||
Create {{ number_field }} voucher codes. Each of them can be redeemed {{ max_usages_field }}
|
||||
times.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.itemvar layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="controls">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
If you choose "any product" for a specific quota and choose to reserve quota for this
|
||||
voucher above, the product can still be unavailable to the voucher holder if another quota
|
||||
associated with the product is sold out!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-step" id="step-valid">
|
||||
<div class="wizard-step-inner">
|
||||
<h4>{% trans "How long should the vouchers be valid?" %}</h4>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="has_valid_until" value="" {% if request.POST and not "has_valid_until" in request.POST %}checked="checked"{% endif %}>
|
||||
{% trans "The whole presale period" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="has_valid_until" value="on" {% if "on" == request.POST.has_valid_until %}checked="checked"{% endif %}>
|
||||
{% blocktrans trimmed %}
|
||||
Only valid until {{ valid_until_field }}
|
||||
{% endblocktrans %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-step" id="step-products">
|
||||
<div class="wizard-step-inner">
|
||||
<h4>{% trans "For which products should the vouchers be applicable?" %}</h4>
|
||||
{% bootstrap_field form.itemvar layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-step" id="step-price">
|
||||
<div class="wizard-step-inner">
|
||||
<h4>{% trans "Should the vouchers modify the product's price?" %}</h4>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="price_mode" value="none" {% if "none" == request.POST.price_mode %}checked="checked"{% endif %}>
|
||||
{% trans "No, just allow to buy this product" %}
|
||||
<span class="help-block">
|
||||
{% trans "This is useful if you have products that can only be bought using vouchers. It also allows you to just block quota for someone (see next step)." %}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="price_mode" value="percent" {% if "percent" == request.POST.price_mode %}checked="checked"{% endif %}>
|
||||
{% blocktrans trimmed %}
|
||||
Reduce price by {{ value_field_percent }} %
|
||||
{% endblocktrans %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="price_mode" value="subtract" {% if "subtract" == request.POST.price_mode %}checked="checked"{% endif %}>
|
||||
{% blocktrans trimmed with currency=request.event.currency %}
|
||||
Reduce price by {{ value_field_subtract }} {{ currency }}
|
||||
{% endblocktrans %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="price_mode" value="set" {% if "set" == request.POST.price_mode %}checked="checked"{% endif %}>
|
||||
{% blocktrans trimmed with currency=request.event.currency %}
|
||||
Change price to {{ value_field_set }} {{ currency }}
|
||||
{% endblocktrans %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-step" id="step-block">
|
||||
<div class="wizard-step-inner">
|
||||
<h4>{% trans "Should the vouchers block quota?" %}</h4>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="block_quota" value="" {% if request.POST and not "block_quota" in request.POST %}checked="checked"{% endif %}>
|
||||
{% trans "No" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio radio-alt">
|
||||
<label>
|
||||
<input type="radio" name="block_quota" value="on" {% if "on" == request.POST.block_quota %}checked="checked"{% endif %}>
|
||||
{% trans "Yes" %}
|
||||
<span class="help-block">
|
||||
{% trans "If you select this option, these vouchers will be guaranteed, i.e. the applicable quotas will be reduced in a way that these vouchers can be redeemed as long as they are valid, even if your event sells out otherwise." %}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-step" id="step-advanced">
|
||||
<div class="wizard-step-inner">
|
||||
<a href="#" id="wizard-advanced-show">Show advanced options</a>
|
||||
<div class="wizard-advanced">
|
||||
<h4>{% trans "Advanced options" %}</h4>
|
||||
{% bootstrap_field form.allow_ignore_quota %}
|
||||
<p><strong>{% trans "Comment" %}</strong></p>
|
||||
{% bootstrap_field form.comment layout="inline" form_group_class="comment" %}
|
||||
<div class="help-block">
|
||||
{% trans "The text entered in this field will not be visible to the user and is available for your convenience." %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.tag layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
<div class="form-group submit-group">
|
||||
<div class="submit-group" id="step-save">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
{% trans "Create" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -20,54 +20,40 @@
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.code layout="horizontal" %}
|
||||
{% bootstrap_field form.max_usages layout="horizontal" %}
|
||||
{% bootstrap_field form.valid_until layout="horizontal" %}
|
||||
{% bootstrap_field form.block_quota layout="horizontal" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.itemvar layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="controls">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
If you choose "any product" for a specific quota and choose to reserve quota for this
|
||||
voucher above, the product can still be unavailable to the voucher holder if another quota
|
||||
associated with the product is sold out!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.tag layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
</fieldset>
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Voucher history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=voucher %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.code layout="horizontal" %}
|
||||
{% bootstrap_field form.max_usages layout="horizontal" %}
|
||||
{% bootstrap_field form.valid_until layout="horizontal" %}
|
||||
{% bootstrap_field form.block_quota layout="horizontal" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.itemvar layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="controls">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
If you choose "any product" for a specific quota and choose to reserve quota for this
|
||||
voucher above, the product can still be unavailable to the voucher holder if another quota
|
||||
associated with the product is sold out!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.tag layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
</fieldset>
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -37,17 +37,12 @@
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
||||
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create multiple new vouchers" %}</a>
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create new vouchers" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
||||
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||
{% trans "Create multiple new vouchers" %}</a>
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new vouchers" %}</a>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
|
||||
@@ -33,14 +33,11 @@ urlpatterns = [
|
||||
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
url(r'^events/$', main.EventList.as_view(), name='events'),
|
||||
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
|
||||
url(r'^events/add$', main.EventCreateStart.as_view(), name='events.add'),
|
||||
url(r'^event/(?P<organizer>[^/]+)/add', main.EventCreate.as_view(), name='events.create'),
|
||||
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
|
||||
url(r'^$', dashboards.event_index, name='event.index'),
|
||||
url(r'^live/$', event.EventLive.as_view(), name='event.live'),
|
||||
url(r'^logs/$', event.EventLog.as_view(), name='event.log'),
|
||||
url(r'^requiredactions/$', event.EventActions.as_view(), name='event.requiredactions'),
|
||||
url(r'^requiredactions/(?P<id>\d+)/discard$', event.EventActionDiscard.as_view(),
|
||||
name='event.requiredaction.discard'),
|
||||
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
|
||||
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
|
||||
url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'),
|
||||
@@ -93,7 +90,6 @@ urlpatterns = [
|
||||
url(r'^vouchers/(?P<voucher>\d+)/delete$', vouchers.VoucherDelete.as_view(),
|
||||
name='event.voucher.delete'),
|
||||
url(r'^vouchers/add$', vouchers.VoucherCreate.as_view(), name='event.vouchers.add'),
|
||||
url(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
||||
name='event.order.transition'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/resend$', orders.OrderResendLink.as_view(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
@@ -10,13 +9,11 @@ from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Item, Order, OrderPosition, Voucher
|
||||
from pretix.base.models import Event, Item, Order, OrderPosition
|
||||
from pretix.control.signals import (
|
||||
event_dashboard_widgets, user_dashboard_widgets,
|
||||
)
|
||||
|
||||
from ..logdisplay import OVERVIEW_BLACKLIST
|
||||
|
||||
NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
|
||||
|
||||
|
||||
@@ -44,7 +41,7 @@ def base_widgets(sender, **kwargs):
|
||||
return [
|
||||
{
|
||||
'content': NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.orders', kwargs={
|
||||
'event': sender.slug,
|
||||
@@ -53,7 +50,7 @@ def base_widgets(sender, **kwargs):
|
||||
},
|
||||
{
|
||||
'content': NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.orders.overview', kwargs={
|
||||
'event': sender.slug,
|
||||
@@ -63,7 +60,7 @@ def base_widgets(sender, **kwargs):
|
||||
{
|
||||
'content': NUM_WIDGET.format(
|
||||
num=formats.localize(rev), text=_('Total revenue ({currency})').format(currency=sender.currency)),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.orders.overview', kwargs={
|
||||
'event': sender.slug,
|
||||
@@ -72,7 +69,7 @@ def base_widgets(sender, **kwargs):
|
||||
},
|
||||
{
|
||||
'content': NUM_WIDGET.format(num=prodc, text=_('Active products')),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.items', kwargs={
|
||||
'event': sender.slug,
|
||||
@@ -90,7 +87,7 @@ def quota_widgets(sender, **kwargs):
|
||||
widgets.append({
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
|
||||
text=_('{quota} left').format(quota=q.name)),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.items.quotas.show', kwargs={
|
||||
'event': sender.slug,
|
||||
@@ -104,7 +101,7 @@ def quota_widgets(sender, **kwargs):
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def shop_state_widget(sender, **kwargs):
|
||||
return [{
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 1000,
|
||||
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
|
||||
t1=_('Your ticket shop is'), t2=_('Click here to change'),
|
||||
@@ -147,7 +144,7 @@ def welcome_wizard_widget(sender, **kwargs):
|
||||
else:
|
||||
return []
|
||||
return [{
|
||||
'display_size': 'full',
|
||||
'width': 12,
|
||||
'priority': 2000,
|
||||
'content': template.render(ctx)
|
||||
}]
|
||||
@@ -157,24 +154,9 @@ def event_index(request, organizer, event):
|
||||
widgets = []
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event):
|
||||
widgets.extend(result)
|
||||
|
||||
qs = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not request.eventperm.can_view_orders:
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
if not request.eventperm.can_view_vouchers:
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||
|
||||
a_qs = request.event.requiredaction_set.filter(done=False)
|
||||
|
||||
ctx = {
|
||||
'widgets': rearrange(widgets),
|
||||
'logs': qs[:5],
|
||||
'actions': a_qs[:5] if request.eventperm.can_change_orders else []
|
||||
}
|
||||
for a in ctx['actions']:
|
||||
a.display = a.display(request)
|
||||
|
||||
return render(request, 'pretixcontrol/event/index.html', ctx)
|
||||
|
||||
|
||||
@@ -189,7 +171,7 @@ def user_event_widgets(**kwargs):
|
||||
event=event.name, df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
|
||||
dt=date_format(event.date_to, 'SHORT_DATE_FORMAT') if event.date_to else ''
|
||||
),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
@@ -206,7 +188,7 @@ def new_event_widgets(**kwargs):
|
||||
'content': '<div class="newevent"><span class="fa fa-plus-circle"></span>{t}</div>'.format(
|
||||
t=_('Create a new event')
|
||||
),
|
||||
'display_size': 'small',
|
||||
'width': 3,
|
||||
'priority': 50,
|
||||
'url': reverse('control:events.add')
|
||||
}
|
||||
@@ -225,18 +207,22 @@ def user_index(request):
|
||||
|
||||
def rearrange(widgets: list):
|
||||
"""
|
||||
Sort widget boxes according to priority.
|
||||
Small and stupid algorithm to arrange widget boxes without too many gaps while respecting
|
||||
priority. Doing this siginificantly better might be *really* hard.
|
||||
"""
|
||||
mapping = {
|
||||
'small': 1,
|
||||
'big': 2,
|
||||
'full': 3,
|
||||
}
|
||||
oldlist = sorted(widgets, key=lambda w: -1 * w.get('priority', 1))
|
||||
newlist = []
|
||||
cpos = 0
|
||||
while len(oldlist) > 0:
|
||||
max_prio = max([w.get('priority', 1) for w in oldlist])
|
||||
try:
|
||||
best = max([w for w in oldlist if w.get('priority', 1) == max_prio and cpos + w.get('width', 3) <= 12],
|
||||
key=lambda w: w.get('width', 3))
|
||||
cpos = (cpos + best.get('width', 3)) % 12
|
||||
except ValueError: # max() arg is an empty sequence
|
||||
best = [w for w in oldlist if w.get('priority', 1) == max_prio][0]
|
||||
cpos = best.get('width', 3)
|
||||
oldlist.remove(best)
|
||||
newlist.append(best)
|
||||
|
||||
def sort_key(element):
|
||||
return (
|
||||
element.get('priority', 1),
|
||||
mapping.get(element.get('display_size', 'small'), 1),
|
||||
)
|
||||
|
||||
return sorted(widgets, key=sort_key, reverse=True)
|
||||
return newlist
|
||||
|
||||
@@ -2,23 +2,21 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.forms import modelformset_factory
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import FormView, ListView
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Event, EventPermission, Item, ItemVariation, LogEntry, Order,
|
||||
RequiredAction, User, Voucher,
|
||||
CachedTicket, Event, EventPermission, Item, ItemVariation, User,
|
||||
)
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||
@@ -34,7 +32,6 @@ from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
from . import UpdateView
|
||||
from ..logdisplay import OVERVIEW_BLACKLIST
|
||||
|
||||
|
||||
class EventUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
@@ -492,13 +489,6 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
||||
)
|
||||
provider.settings_content = provider.settings_content_render(self.request)
|
||||
provider.form.prepare_fields()
|
||||
|
||||
provider.preview_allowed = True
|
||||
for k, v in provider.settings_form_fields.items():
|
||||
if v.required and not self.request.event.settings.get('ticketoutput_%s_%s' % (provider.identifier, k)):
|
||||
provider.preview_allowed = False
|
||||
break
|
||||
|
||||
providers.append(provider)
|
||||
return providers
|
||||
|
||||
@@ -634,16 +624,10 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
if request.POST.get("live") == "true" and not self.issues:
|
||||
request.event.live = True
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.live.activated', user=self.request.user, data={}
|
||||
)
|
||||
messages.success(self.request, _('Your shop is live now!'))
|
||||
elif request.POST.get("live") == "false":
|
||||
request.event.live = False
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.live.deactivated', user=self.request.user, data={}
|
||||
)
|
||||
messages.success(self.request, _('We\'ve taken your shop down. You can re-enable it whenever you want!'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
@@ -652,68 +636,3 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug
|
||||
})
|
||||
|
||||
|
||||
class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
template_name = 'pretixcontrol/event/logs.html'
|
||||
model = LogEntry
|
||||
context_object_name = 'logs'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not self.request.eventperm.can_view_orders:
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
if not self.request.eventperm.can_view_vouchers:
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||
|
||||
if self.request.GET.get('user') == 'yes':
|
||||
qs = qs.filter(user__isnull=False)
|
||||
elif self.request.GET.get('user') == 'no':
|
||||
qs = qs.filter(user__isnull=True)
|
||||
elif self.request.GET.get('user'):
|
||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['userlist'] = self.request.event.user_perms.select_related('user')
|
||||
return ctx
|
||||
|
||||
|
||||
class EventActions(EventPermissionRequiredMixin, ListView):
|
||||
template_name = 'pretixcontrol/event/actions.html'
|
||||
model = RequiredAction
|
||||
context_object_name = 'actions'
|
||||
paginate_by = 20
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.requiredaction_set.filter(done=False)
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
for a in ctx['actions']:
|
||||
a.display = a.display(self.request)
|
||||
return ctx
|
||||
|
||||
|
||||
class EventActionDiscard(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
action = get_object_or_404(RequiredAction, event=request.event, pk=kwargs.get('id'))
|
||||
action.done = True
|
||||
action.user = request.user
|
||||
action.save()
|
||||
messages.success(self.request, _('The issue has been marked as resolved!'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.index', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, Q
|
||||
from django.db.models import Count
|
||||
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
@@ -19,11 +19,11 @@ from django.views.generic.edit import DeleteView
|
||||
from pretix.base.forms import I18nFormSet
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Item, ItemCategory, ItemVariation, Order, Question,
|
||||
QuestionAnswer, QuestionOption, Quota, Voucher,
|
||||
QuestionAnswer, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.control.forms.item import (
|
||||
CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
|
||||
QuestionForm, QuestionOptionForm, QuotaForm,
|
||||
)
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required,
|
||||
@@ -624,24 +624,12 @@ class QuotaView(ChartContainingView, DetailView):
|
||||
]
|
||||
ctx['quota_table_rows'] = list(data)
|
||||
|
||||
sum_values = sum([d['value'] for d in data])
|
||||
|
||||
if self.object.size is not None:
|
||||
data.append({
|
||||
'label': ugettext('Current availability'),
|
||||
'value': avail[1]
|
||||
})
|
||||
|
||||
ctx['quota_chart_data'] = json.dumps(data)
|
||||
ctx['quota_overbooked'] = sum_values - self.object.size if self.object.size is not None else 0
|
||||
|
||||
ctx['has_ignore_vouchers'] = Voucher.objects.filter(
|
||||
Q(allow_ignore_quota=True) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now())) &
|
||||
Q(Q(self.object._position_lookup) | Q(quota=self.object)) &
|
||||
Q(redeemed__lt=F('max_usages'))
|
||||
).exists()
|
||||
|
||||
return ctx
|
||||
|
||||
def get_object(self, queryset=None) -> Quota:
|
||||
@@ -684,10 +672,9 @@ class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.quotas.show', kwargs={
|
||||
return reverse('control:event.items.quotas', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'quota': self.object.pk
|
||||
})
|
||||
|
||||
|
||||
@@ -816,7 +803,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
||||
def formset(self):
|
||||
formsetclass = inlineformset_factory(
|
||||
Item, ItemVariation,
|
||||
form=ItemVariationForm, formset=ItemVariationsFormSet,
|
||||
form=ItemVariationForm, formset=I18nFormSet,
|
||||
can_order=True, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
@@ -831,7 +818,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
||||
continue
|
||||
self.get_object().log_action(
|
||||
'pretix.event.item.variation.deleted', user=self.request.user, data={
|
||||
'value': form.instance.value,
|
||||
'id': form.instance.pk
|
||||
}
|
||||
)
|
||||
@@ -849,7 +835,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['value'] = form.instance.value
|
||||
change_data['id'] = form.instance.pk
|
||||
self.get_object().log_action(
|
||||
'pretix.event.item.variation.changed' if not created else
|
||||
|
||||
@@ -2,15 +2,13 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import ListView
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from django.views.generic import CreateView, ListView, TemplateView
|
||||
|
||||
from pretix.base.models import Event, EventPermission
|
||||
from pretix.control.forms.event import (
|
||||
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
|
||||
)
|
||||
from pretix.base.models import Event, EventPermission, OrganizerPermission
|
||||
from pretix.control.forms.event import EventCreateForm, EventCreateSettingsForm
|
||||
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
||||
|
||||
|
||||
class EventList(ListView):
|
||||
@@ -27,69 +25,76 @@ class EventList(ListView):
|
||||
)
|
||||
|
||||
|
||||
def condition_copy(wizard):
|
||||
return EventPermission.objects.filter(
|
||||
user=wizard.request.user, can_change_settings=True, can_change_items=True
|
||||
).exists()
|
||||
class EventCreateStart(TemplateView):
|
||||
template_name = 'pretixcontrol/events/start.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['organizers'] = [
|
||||
p.organizer for p in OrganizerPermission.objects.filter(
|
||||
user=self.request.user, can_create_events=True
|
||||
).select_related("organizer")
|
||||
]
|
||||
return ctx
|
||||
|
||||
|
||||
class EventWizard(SessionWizardView):
|
||||
form_list = [
|
||||
('foundation', EventWizardFoundationForm),
|
||||
('basics', EventWizardBasicsForm),
|
||||
('copy', EventWizardCopyForm),
|
||||
]
|
||||
templates = {
|
||||
'foundation': 'pretixcontrol/events/create_foundation.html',
|
||||
'basics': 'pretixcontrol/events/create_basics.html',
|
||||
'copy': 'pretixcontrol/events/create_copy.html',
|
||||
}
|
||||
condition_dict = {
|
||||
'copy': condition_copy
|
||||
}
|
||||
class EventCreate(OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = Event
|
||||
form_class = EventCreateForm
|
||||
template_name = 'pretixcontrol/events/create.html'
|
||||
context_object_name = 'event'
|
||||
permission = 'can_create_events'
|
||||
|
||||
def get_form_kwargs(self, step=None):
|
||||
kwargs = {
|
||||
'user': self.request.user
|
||||
}
|
||||
if step != 'foundation':
|
||||
fdata = self.get_cleaned_data_for_step('foundation')
|
||||
kwargs.update(fdata)
|
||||
@cached_property
|
||||
def sform(self):
|
||||
return EventCreateSettingsForm(
|
||||
obj=Event(),
|
||||
prefix='settings',
|
||||
data=self.request.POST if self.request.method == 'POST' else None
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.sform.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['sform'] = self.sform
|
||||
return context
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object = Event()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_template_names(self):
|
||||
return [self.templates[self.steps.current]]
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The new event has been created.'))
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
EventPermission.objects.create(
|
||||
event=form.instance, user=self.request.user,
|
||||
)
|
||||
self.object = form.instance
|
||||
self.object.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||
self.object.save()
|
||||
|
||||
def done(self, form_list, form_dict, **kwargs):
|
||||
foundation_data = self.get_cleaned_data_for_step('foundation')
|
||||
basics_data = self.get_cleaned_data_for_step('basics')
|
||||
copy_data = self.get_cleaned_data_for_step('copy')
|
||||
self.sform.obj = form.instance
|
||||
self.sform.save()
|
||||
form.instance.log_action('pretix.event.settings', user=self.request.user, data={
|
||||
k: form.instance.settings.get(k) for k in self.sform.changed_data
|
||||
})
|
||||
return ret
|
||||
|
||||
with transaction.atomic():
|
||||
event = form_dict['basics'].instance
|
||||
event.organizer = foundation_data['organizer']
|
||||
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||
form_dict['basics'].save()
|
||||
EventPermission.objects.create(event=event, user=self.request.user)
|
||||
|
||||
logdata = {}
|
||||
for f in form_list:
|
||||
logdata.update({
|
||||
k: v for k, v in f.cleaned_data.items()
|
||||
})
|
||||
event.log_action('pretix.event.settings', user=self.request.user, data=logdata)
|
||||
|
||||
if copy_data and copy_data['copy_from_event']:
|
||||
from_event = copy_data['copy_from_event']
|
||||
event.copy_data_from(from_event)
|
||||
|
||||
event.settings.set('timezone', basics_data['timezone'])
|
||||
event.settings.set('locale', basics_data['locale'])
|
||||
event.settings.set('locales', foundation_data['locales'])
|
||||
|
||||
messages.success(self.request, _('The new event has been created. You can now adjust the event settings in '
|
||||
'detail.'))
|
||||
return redirect(reverse('control:event.settings', kwargs={
|
||||
'organizer': event.organizer.slug,
|
||||
'event': event.slug,
|
||||
}))
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.object.slug,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -99,13 +100,10 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
|
||||
model = Order
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
try:
|
||||
return Order.objects.get(
|
||||
event=self.request.event,
|
||||
code=self.kwargs['code'].upper()
|
||||
)
|
||||
except Order.DoesNotExist:
|
||||
raise Http404()
|
||||
return Order.objects.get(
|
||||
event=self.request.event,
|
||||
code=self.kwargs['code'].upper()
|
||||
)
|
||||
|
||||
def _redirect_back(self):
|
||||
return redirect('control:event.order',
|
||||
@@ -162,17 +160,27 @@ class OrderDetail(OrderView):
|
||||
).select_related(
|
||||
'item', 'variation'
|
||||
).prefetch_related(
|
||||
'item__questions', 'answers', 'answers__question', 'checkins'
|
||||
).order_by('positionid')
|
||||
'item__questions', 'answers', 'answers__question'
|
||||
)
|
||||
|
||||
# Group items of the same variation
|
||||
# We do this by list manipulations instead of a GROUP BY query, as
|
||||
# Django is unable to join related models in a .values() query
|
||||
def keyfunc(pos):
|
||||
if (pos.item.admission and self.request.event.settings.attendee_names_asked) \
|
||||
or pos.item.questions.all():
|
||||
return pos.id, 0, 0, 0, 0, 0
|
||||
return 0, pos.item_id, pos.variation_id, pos.price, pos.tax_rate, (pos.voucher_id or 0)
|
||||
|
||||
positions = []
|
||||
for p in cartpos:
|
||||
p.has_questions = (
|
||||
(p.item.admission and self.request.event.settings.attendee_names_asked) or
|
||||
p.item.questions.all()
|
||||
)
|
||||
p.cache_answers()
|
||||
positions.append(p)
|
||||
for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc):
|
||||
g = list(g)
|
||||
group = g[0]
|
||||
group.count = len(g)
|
||||
group.total = group.count * group.price
|
||||
group.has_questions = k[0] != ""
|
||||
group.cache_answers()
|
||||
positions.append(group)
|
||||
|
||||
return {
|
||||
'positions': positions,
|
||||
@@ -518,23 +526,18 @@ class OrderContactChange(OrderView):
|
||||
)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
old_email = self.order.email
|
||||
if self.form.is_valid():
|
||||
self.order.log_action(
|
||||
'pretix.event.order.contact.changed',
|
||||
data={
|
||||
'old_email': old_email,
|
||||
'new_email': self.form.cleaned_data['email'],
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
self.order.log_action('pretix.event.order.contact.changed', {
|
||||
'old_email': self.order.email,
|
||||
'new_email': self.form.cleaned_data['email'],
|
||||
'regenerate_secrets': self.form.cleaned_data['regenerate_secrets']
|
||||
})
|
||||
if self.form.cleaned_data['regenerate_secrets']:
|
||||
self.order.secret = generate_secret()
|
||||
for op in self.order.positions.all():
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||
self.order.log_action('pretix.event.order.secret.changed', user=self.request.user)
|
||||
|
||||
self.form.save()
|
||||
messages.success(self.request, _('The order has been changed.'))
|
||||
|
||||
@@ -33,7 +33,7 @@ class VoucherList(EventPermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.vouchers.all().select_related('item', 'variation')
|
||||
if self.request.GET.get("search", "") != "":
|
||||
s = self.request.GET.get("search", "").strip()
|
||||
s = self.request.GET.get("search", "")
|
||||
qs = qs.filter(Q(code__icontains=s) | Q(tag__icontains=s) | Q(comment__icontains=s))
|
||||
if self.request.GET.get("tag", "") != "":
|
||||
s = self.request.GET.get("tag", "")
|
||||
@@ -78,7 +78,7 @@ class VoucherList(EventPermissionRequiredMixin, ListView):
|
||||
_("Yes") if v.block_quota else _("No"),
|
||||
_("Yes") if v.allow_ignore_quota else _("No"),
|
||||
v.get_price_mode_display(),
|
||||
str(v.value) if v.value is not None else "",
|
||||
str(v.value) if v.value else "",
|
||||
v.tag,
|
||||
str(v.redeemed),
|
||||
str(v.max_usages)
|
||||
@@ -189,44 +189,6 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
|
||||
|
||||
class VoucherCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = Voucher
|
||||
template_name = 'pretixcontrol/vouchers/detail.html'
|
||||
permission = 'can_change_vouchers'
|
||||
context_object_name = 'voucher'
|
||||
|
||||
def get_form_class(self):
|
||||
form_class = VoucherForm
|
||||
for receiver, response in voucher_form_class.send(self.request.event, cls=form_class):
|
||||
if response:
|
||||
form_class = response
|
||||
return form_class
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.vouchers', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = Voucher(event=self.request.event)
|
||||
return kwargs
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
messages.success(self.request, _('The new voucher has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.voucher.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
return ret
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# TODO: Transform this into an asynchronous call?
|
||||
with request.event.lock():
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = Voucher
|
||||
template_name = 'pretixcontrol/vouchers/bulk.html'
|
||||
permission = 'can_change_vouchers'
|
||||
@@ -241,6 +203,11 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = Voucher(event=self.request.event)
|
||||
initial = {
|
||||
}
|
||||
if 'initial' in kwargs:
|
||||
initial.update(kwargs['initial'])
|
||||
kwargs['initial'] = initial
|
||||
return kwargs
|
||||
|
||||
@transaction.atomic
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
|
||||
|
||||
def daterange(df, dt):
|
||||
lng = get_language()
|
||||
|
||||
if lng.startswith("de"):
|
||||
if df.year == dt.year and df.month == dt.month:
|
||||
return "{}.–{}".format(_date(df, "j"), _date(dt, "j. F Y"))
|
||||
elif df.year == dt.year:
|
||||
return "{} – {}".format(_date(df, "j. F"), _date(dt, "j. F Y"))
|
||||
elif lng.startswith("en"):
|
||||
if df.year == dt.year and df.month == dt.month:
|
||||
return "{} – {}".format(_date(df, "N jS"), _date(dt, "jS, Y"))
|
||||
elif df.year == dt.year:
|
||||
return "{} – {}".format(_date(df, "N jS"), _date(dt, "N jS, Y"))
|
||||
|
||||
return _("{date_from} – {date_to}").format(
|
||||
date_from=_date(df, "DATE_FORMAT"), date_to=_date(dt, "DATE_FORMAT")
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-01-03 23:46+0000\n"
|
||||
"PO-Revision-Date: 2017-01-01 20:40+0100\n"
|
||||
"POT-Creation-Date: 2016-11-27 18:45+0000\n"
|
||||
"PO-Revision-Date: 2016-11-27 19:46+0100\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -46,14 +46,6 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: static/pretixcontrol/js/clipboard.js:23
|
||||
msgid "Copied!"
|
||||
msgstr "Kopiert!"
|
||||
|
||||
#: static/pretixcontrol/js/clipboard.js:29
|
||||
msgid "Press Ctrl-C to copy!"
|
||||
msgstr "Drücken Sie Strg+C zum Kopieren!"
|
||||
|
||||
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
|
||||
msgid "Close message"
|
||||
msgstr "Schließen"
|
||||
@@ -70,9 +62,8 @@ msgstr "Sonstige"
|
||||
msgid "Count"
|
||||
msgstr "Anzahl"
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:27
|
||||
#: static/pretixpresale/js/ui/asynctask.js:27
|
||||
#: static/pretixpresale/js/ui/asynctask.js:64
|
||||
#: static/pretixpresale/js/ui/asynctask.js:62
|
||||
msgid ""
|
||||
"Your request has been queued on the server and will now be processed. If "
|
||||
"this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -82,19 +73,35 @@ msgstr ""
|
||||
"dies länger als zwei Minuten dauert, kontaktieren Sie uns bitte oder gehen "
|
||||
"Sie in Ihrem Browser einen Schritt zurück und versuchen es erneut."
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:40
|
||||
#: static/pretixpresale/js/ui/asynctask.js:43
|
||||
#: static/pretixpresale/js/ui/asynctask.js:85
|
||||
#: static/pretixpresale/js/ui/asynctask.js:41
|
||||
#: static/pretixpresale/js/ui/asynctask.js:83
|
||||
msgid "An error of type {code} occured."
|
||||
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:53
|
||||
#: static/pretixpresale/js/ui/asynctask.js:103
|
||||
#: static/pretixpresale/js/ui/asynctask.js:44
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||
"Letzter Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:74
|
||||
msgid "The request took to long. Please try again."
|
||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:86
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
|
||||
"einmal. Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:101
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Wir verarbeiten deine Anfrage …"
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:54
|
||||
#: static/pretixpresale/js/ui/asynctask.js:104
|
||||
#: static/pretixpresale/js/ui/asynctask.js:102
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -104,25 +111,6 @@ msgstr ""
|
||||
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
|
||||
"Seite neu laden und es erneut versuchen."
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:46
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||
"Letzter Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:76
|
||||
msgid "The request took to long. Please try again."
|
||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:88
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
|
||||
"einmal. Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/cart.js:10
|
||||
msgid "The items in your cart are no longer reserved for you."
|
||||
msgstr "Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-01-03 23:46+0000\n"
|
||||
"PO-Revision-Date: 2017-01-01 20:41+0100\n"
|
||||
"POT-Creation-Date: 2016-11-27 18:45+0000\n"
|
||||
"PO-Revision-Date: 2016-11-27 19:52+0100\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -46,14 +46,6 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: static/pretixcontrol/js/clipboard.js:23
|
||||
msgid "Copied!"
|
||||
msgstr "Kopiert!"
|
||||
|
||||
#: static/pretixcontrol/js/clipboard.js:29
|
||||
msgid "Press Ctrl-C to copy!"
|
||||
msgstr "Drücke Strg+C zum kopieren!"
|
||||
|
||||
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
|
||||
msgid "Close message"
|
||||
msgstr "Schließen"
|
||||
@@ -70,9 +62,8 @@ msgstr "Sonstige"
|
||||
msgid "Count"
|
||||
msgstr "Anzahl"
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:27
|
||||
#: static/pretixpresale/js/ui/asynctask.js:27
|
||||
#: static/pretixpresale/js/ui/asynctask.js:64
|
||||
#: static/pretixpresale/js/ui/asynctask.js:62
|
||||
msgid ""
|
||||
"Your request has been queued on the server and will now be processed. If "
|
||||
"this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -82,19 +73,35 @@ msgstr ""
|
||||
"dies länger als zwei Minuten dauert, kontaktiere uns bitte oder gehe in "
|
||||
"deinem Browser einen Schritt zurück und versuche es erneut."
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:40
|
||||
#: static/pretixpresale/js/ui/asynctask.js:43
|
||||
#: static/pretixpresale/js/ui/asynctask.js:85
|
||||
#: static/pretixpresale/js/ui/asynctask.js:41
|
||||
#: static/pretixpresale/js/ui/asynctask.js:83
|
||||
msgid "An error of type {code} occured."
|
||||
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:53
|
||||
#: static/pretixpresale/js/ui/asynctask.js:103
|
||||
#: static/pretixpresale/js/ui/asynctask.js:44
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||
"Letzter Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:74
|
||||
msgid "The request took to long. Please try again."
|
||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:86
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
|
||||
"einmal. Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:101
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Wir verarbeiten deine Anfrage …"
|
||||
|
||||
#: static/pretixpresale/js/ui/asyncdownload.js:54
|
||||
#: static/pretixpresale/js/ui/asynctask.js:104
|
||||
#: static/pretixpresale/js/ui/asynctask.js:102
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -104,25 +111,6 @@ msgstr ""
|
||||
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
|
||||
"neu laden und es erneut versuchen."
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:46
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||
"Letzter Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:76
|
||||
msgid "The request took to long. Please try again."
|
||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
||||
|
||||
#: static/pretixpresale/js/ui/asynctask.js:88
|
||||
msgid ""
|
||||
"We currenctly cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
|
||||
"einmal. Fehlercode: {code}"
|
||||
|
||||
#: static/pretixpresale/js/ui/cart.js:10
|
||||
msgid "The items in your cart are no longer reserved for you."
|
||||
msgstr "Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert."
|
||||
|
||||
@@ -14,7 +14,7 @@ from pretix.base.services.async import TransactionAwareTask
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid
|
||||
from pretix.celery_app import app
|
||||
from pretix.celery import app
|
||||
|
||||
from .models import BankImportJob, BankTransaction
|
||||
|
||||
|
||||
@@ -215,8 +215,6 @@ class ImportView(EventPermissionRequiredMixin, ListView):
|
||||
q = self.request.GET.get('search')
|
||||
qs = qs.filter(
|
||||
Q(payer__icontains=q) | Q(reference__icontains=q) | Q(comment__icontains=q)
|
||||
).order_by(
|
||||
'-import_job__created'
|
||||
)
|
||||
|
||||
return qs
|
||||
@@ -230,22 +228,17 @@ class ImportView(EventPermissionRequiredMixin, ListView):
|
||||
self.discard_all()
|
||||
return self.redirect_back()
|
||||
|
||||
elif ('file' in self.request.FILES and 'csv' in self.request.FILES.get('file').name.lower()) \
|
||||
if ('file' in self.request.FILES and 'csv' in self.request.FILES.get('file').name.lower()) \
|
||||
or 'amount' in self.request.POST:
|
||||
# Process CSV
|
||||
return self.process_csv()
|
||||
|
||||
elif 'file' in self.request.FILES and 'txt' in self.request.FILES.get('file').name.lower():
|
||||
if 'file' in self.request.FILES and 'txt' in self.request.FILES.get('file').name.lower():
|
||||
return self.process_mt940()
|
||||
|
||||
elif self.request.FILES.get('file') is None:
|
||||
messages.error(self.request, _('You must choose a file to import.'))
|
||||
return self.redirect_back()
|
||||
|
||||
else:
|
||||
messages.error(self.request, _('We were unable to detect the file type of this import. Please '
|
||||
'contact support for help.'))
|
||||
return self.redirect_back()
|
||||
messages.error(self.request, _('We were unable to detect the file type of this import. Please '
|
||||
'contact support for help.'))
|
||||
return self.redirect_back()
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.contrib import messages
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext as __, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Quota, RequiredAction
|
||||
from pretix.base.models import Quota
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
@@ -47,13 +47,6 @@ class Paypal(BasePaymentProvider):
|
||||
]
|
||||
)
|
||||
|
||||
def settings_content_render(self, request):
|
||||
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
|
||||
_('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
|
||||
'when payments are refunded externally.'),
|
||||
build_absolute_uri(self.event, 'plugins:paypal:webhook')
|
||||
)
|
||||
|
||||
def init_api(self):
|
||||
paypalrestsdk.set_config(
|
||||
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
|
||||
@@ -203,12 +196,6 @@ class Paypal(BasePaymentProvider):
|
||||
mark_order_paid(order, 'paypal', json.dumps(payment.to_dict()))
|
||||
except Quota.QuotaExceededException as e:
|
||||
messages.error(request, str(e))
|
||||
RequiredAction.objects.create(
|
||||
event=request.event, action_type='pretix.plugins.paypal.overpaid', data=json.dumps({
|
||||
'order': order.code,
|
||||
'payment': payment.id
|
||||
})
|
||||
)
|
||||
except SendMailException:
|
||||
messages.warning(request, _('There was an error sending the confirmation mail.'))
|
||||
return None
|
||||
@@ -270,7 +257,7 @@ class Paypal(BasePaymentProvider):
|
||||
order.save()
|
||||
|
||||
def order_can_retry(self, order):
|
||||
return self._is_still_available()
|
||||
return True
|
||||
|
||||
def order_prepare(self, request, order):
|
||||
self.init_api()
|
||||
|
||||
@@ -1,53 +1,9 @@
|
||||
import json
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_payment_providers, requiredaction_display,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
from .payment import Paypal
|
||||
return Paypal
|
||||
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="paypal_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
if logentry.action_type != 'pretix.plugins.paypal.event':
|
||||
return
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
event_type = data.get('event_type')
|
||||
text = None
|
||||
plains = {
|
||||
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
|
||||
'PAYMENT.SALE.DENIED': _('Payment denied.'),
|
||||
'PAYMENT.SALE.REFUNDED': _('Payment refunded.'),
|
||||
'PAYMENT.SALE.REVERSED': _('Payment reversed.'),
|
||||
}
|
||||
|
||||
if event_type in plains:
|
||||
text = plains[event_type]
|
||||
|
||||
if text:
|
||||
return _('PayPal reported an event: {}').format(text)
|
||||
|
||||
|
||||
@receiver(signal=requiredaction_display, dispatch_uid="paypal_requiredaction_display")
|
||||
def pretixcontrol_action_display(sender, action, request, **kwargs):
|
||||
if not action.action_type.startswith('pretix.plugins.paypal'):
|
||||
return
|
||||
|
||||
data = json.loads(action.data)
|
||||
|
||||
if action.action_type == 'pretix.plugins.paypal.refund':
|
||||
template = get_template('pretixplugins/paypal/action_refund.html')
|
||||
elif action.action_type == 'pretix.plugins.paypal.overpaid':
|
||||
template = get_template('pretixplugins/paypal/action_overpaid.html')
|
||||
|
||||
ctx = {'data': data, 'event': sender, 'action': action}
|
||||
return template.render(ctx, request)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{% url "control:event.order" organizer=event.organizer.slug event=event.slug code=data.order as ourl %}
|
||||
{% blocktrans trimmed with payment=data.payment order="<a href='"|add:ourl|add:"'>"|add:data.order|add:"</a>"|safe %}
|
||||
The PayPal transaction {{ payment }} has succeeded, but the order {{ order }} is expired and the product
|
||||
was sold out in the meantime. Therefore, the payment could not be acceped. Please contact the user and refund
|
||||
the money via PayPal's interface.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
@@ -1,20 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{% url "control:event.order" organizer=event.organizer.slug event=event.slug code=data.order as ourl %}
|
||||
{% blocktrans trimmed with payment=data.resource.id order="<a href='"|add:ourl|add:"'>"|add:data.order|add:"</a>"|safe %}
|
||||
PayPal reported that the payment {{ payment }} has been refunded or reversed.
|
||||
Do you want to mark the matching order ({{ order }}) as refunded?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form class="form-inline" method="post" action="{% url "plugins:paypal:refund" event=event.slug organizer=event.organizer.slug id=action.id %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.requiredaction.discard" event=event.slug organizer=event.organizer.slug id=action.id %}"
|
||||
class="btn btn-default">
|
||||
{% trans "No" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-default btn-danger">
|
||||
{% trans "Yes, mark order as refunded" %}
|
||||
</button>
|
||||
{% trans "This action cannot be undone." %}
|
||||
</form>
|
||||
@@ -1,17 +1,10 @@
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from .views import abort, refund, success, webhook
|
||||
from .views import abort, success
|
||||
|
||||
event_patterns = [
|
||||
url(r'^paypal/', include([
|
||||
url(r'^abort/$', abort, name='abort'),
|
||||
url(r'^return/$', success, name='return'),
|
||||
url(r'^webhook/$', webhook, name='webhook'),
|
||||
])),
|
||||
]
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/refund/(?P<id>\d+)/',
|
||||
refund, name='refund'),
|
||||
]
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import paypalrestsdk
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from pretix.base.models import Order, Quota, RequiredAction
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.base.models import Order
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.plugins.paypal.payment import Paypal
|
||||
from pretix.presale.utils import event_view
|
||||
@@ -58,88 +49,3 @@ def success(request, *args, **kwargs):
|
||||
def abort(request, *args, **kwargs):
|
||||
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
@event_view(require_live=False)
|
||||
def webhook(request, *args, **kwargs):
|
||||
event_body = request.body.decode('utf-8').strip()
|
||||
event_json = json.loads(event_body)
|
||||
|
||||
prov = Paypal(request.event)
|
||||
prov.init_api()
|
||||
|
||||
# We do not check the signature, we just use it as a trigger to look the charge up.
|
||||
if event_json['resource_type'] != 'sale':
|
||||
return HttpResponse("Not interested in this resource type", status=200)
|
||||
|
||||
try:
|
||||
sale = paypalrestsdk.Sale.find(event_json['resource']['id'])
|
||||
except:
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Sale not found', status=500)
|
||||
|
||||
orders = Order.objects.filter(event=request.event, payment_provider='paypal',
|
||||
payment_info__icontains=sale['id'])
|
||||
order = None
|
||||
for o in orders:
|
||||
payment_info = json.loads(o.payment_info)
|
||||
for res in payment_info['transactions'][0]['related_resources']:
|
||||
for k, v in res.items():
|
||||
if k == 'sale' and v['id'] == sale['id']:
|
||||
order = o
|
||||
break
|
||||
|
||||
if not order:
|
||||
return HttpResponse('Order not found', status=200)
|
||||
|
||||
order.log_action('pretix.plugins.paypal.event', data=event_json)
|
||||
|
||||
if order.status == Order.STATUS_PAID and sale['state'] in ('partially_refunded', 'refunded'):
|
||||
RequiredAction.objects.create(
|
||||
event=request.event, action_type='pretix.plugins.paypal.refund', data=json.dumps({
|
||||
'order': order.code,
|
||||
'sale': sale['id']
|
||||
})
|
||||
)
|
||||
elif order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and sale['state'] == 'completed':
|
||||
try:
|
||||
mark_order_paid(order, user=None)
|
||||
except Quota.QuotaExceededException:
|
||||
if not RequiredAction.objects.filter(event=request.event, action_type='pretix.plugins.paypal.overpaid',
|
||||
data__icontains=order.code).exists():
|
||||
RequiredAction.objects.create(
|
||||
event=request.event, action_type='pretix.plugins.paypal.overpaid', data=json.dumps({
|
||||
'order': order.code,
|
||||
'payment': sale['parent_payment']
|
||||
})
|
||||
)
|
||||
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@event_permission_required('can_view_orders')
|
||||
@require_POST
|
||||
def refund(request, **kwargs):
|
||||
with transaction.atomic():
|
||||
action = get_object_or_404(RequiredAction, event=request.event, pk=kwargs.get('id'),
|
||||
action_type='pretix.plugins.paypal.refund', done=False)
|
||||
data = json.loads(action.data)
|
||||
action.done = True
|
||||
action.user = request.user
|
||||
action.save()
|
||||
order = get_object_or_404(Order, event=request.event, code=data['order'])
|
||||
if order.status != Order.STATUS_PAID:
|
||||
messages.error(request, _('The order cannot be marked as refunded as it is not marked as paid!'))
|
||||
else:
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.success(
|
||||
request, _('The order has been marked as refunded and the issue has been marked as resolved!')
|
||||
)
|
||||
|
||||
return redirect(reverse('control:event.order', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
'code': data['order']
|
||||
}))
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2016-12-08 16:44
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixdroid', '0001_initial'),
|
||||
]
|
||||
|
||||
database_operations = [
|
||||
migrations.AlterModelTable('Checkin', 'pretixbase_checkin')
|
||||
]
|
||||
|
||||
state_operations = [
|
||||
migrations.DeleteModel('Checkin')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
database_operations=database_operations,
|
||||
state_operations=state_operations)
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Checkin(models.Model):
|
||||
position = models.ForeignKey('pretixbase.OrderPosition', related_name='pretixdroid_checkins')
|
||||
datetime = models.DateTimeField(auto_now_add=True)
|
||||
@@ -1,10 +1,7 @@
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.signals import logentry_display
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
@@ -24,19 +21,3 @@ def control_nav_import(sender, request=None, **kwargs):
|
||||
'icon': 'android',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="pretixdroid_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
if logentry.action_type != 'pretix.plugins.pretixdroid.scan':
|
||||
return
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
if data.get('first'):
|
||||
return _('Position #{posid} has been scanned.'.format(
|
||||
posid=data.get('positionid')
|
||||
))
|
||||
else:
|
||||
return _('Position #{posid} has been scanned and rejected because it has already been scanned before.'.format(
|
||||
posid=data.get('positionid')
|
||||
))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user