forked from CGM_Public/pretix_original
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680e7cb0da | ||
|
|
498ac97bdd | ||
|
|
3c75072868 | ||
|
|
8e09aa9b54 | ||
|
|
c4fe2322a1 | ||
|
|
43abe14293 | ||
|
|
7146d984d0 | ||
|
|
5495cd749a | ||
|
|
ef93b8ae38 | ||
|
|
2d370aaf5a | ||
|
|
3037309711 | ||
|
|
01c3200258 | ||
|
|
bf9987e05f | ||
|
|
cd61c0833d | ||
|
|
90bdb30449 | ||
|
|
92c2dcaf25 | ||
|
|
a3cd8d151d | ||
|
|
ba455a3630 | ||
|
|
123f47ab39 | ||
|
|
d6503e3e48 | ||
|
|
4f4e5854f2 | ||
|
|
48461122f8 | ||
|
|
aba7652aee | ||
|
|
78fc58cc93 | ||
|
|
3a2ca8d3d6 | ||
|
|
fecc5ec307 | ||
|
|
0665bd443b | ||
|
|
221526c979 | ||
|
|
04369ff4f1 | ||
|
|
e71691d4a5 | ||
|
|
598e7c5637 | ||
|
|
ad73c0e05b | ||
|
|
d4573e8c25 | ||
|
|
1d0def19b1 | ||
|
|
80a1bcf033 | ||
|
|
2f0cbcc565 | ||
|
|
48a2090e01 | ||
|
|
520b978ab0 | ||
|
|
ec42557305 | ||
|
|
bbb71ef891 | ||
|
|
7300c60e73 | ||
|
|
9f90ac80a0 | ||
|
|
f2260212ee | ||
|
|
b94459c761 | ||
|
|
3e26a4d9cc | ||
|
|
6bcfa4980f | ||
|
|
829b6a7d56 | ||
|
|
56bf3fe459 | ||
|
|
63bdb397e7 | ||
|
|
d4b3bf4370 | ||
|
|
7120e95d2a | ||
|
|
f285390f46 | ||
|
|
559864dd01 | ||
|
|
8021e1f269 | ||
|
|
53713acd9a | ||
|
|
5212f6b035 | ||
|
|
ea807239b1 | ||
|
|
2ec534e32d | ||
|
|
ec90efbf4a | ||
|
|
ffa35a9b9b | ||
|
|
2a6629e075 | ||
|
|
59d440b213 | ||
|
|
e6a6043a7a | ||
|
|
ecb1eedcba | ||
|
|
41c8ed2400 |
1
AUTHORS
1
AUTHORS
@@ -21,3 +21,4 @@ an awesome project. Thank you all!
|
||||
Tobias Kunze <rixx@cutebit.de>
|
||||
Oliver Knapp <github@oliverknapp.de>
|
||||
Vishal Sodani <vishalsodani@rediffmail.com>
|
||||
Jan Felix Wiebe <git@jfwie.be>
|
||||
|
||||
65
Dockerfile
65
Dockerfile
@@ -1,50 +1,45 @@
|
||||
FROM debian:jessie
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 git python3-pip \
|
||||
libxml2-dev libxslt1-dev python-dev python-virtualenv locales libffi-dev \
|
||||
build-essential python3-dev zlib1g-dev libssl-dev gettext \
|
||||
libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \
|
||||
aqbanking-tools supervisor nginx sudo \
|
||||
--no-install-recommends
|
||||
|
||||
WORKDIR /
|
||||
|
||||
RUN dpkg-reconfigure locales && \
|
||||
RUN apt-get update && \
|
||||
apt-get install -y python3 git python3-pip \
|
||||
libxml2-dev libxslt1-dev python-dev python-virtualenv locales libffi-dev \
|
||||
build-essential python3-dev zlib1g-dev libssl-dev gettext \
|
||||
libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \
|
||||
aqbanking-tools supervisor nginx sudo \
|
||||
--no-install-recommends && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
dpkg-reconfigure locales && \
|
||||
locale-gen C.UTF-8 && \
|
||||
/usr/sbin/update-locale LANG=C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
/usr/sbin/update-locale LANG=C.UTF-8 && \
|
||||
mkdir /etc/pretix && \
|
||||
mkdir /data && \
|
||||
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
|
||||
echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||
mkdir /static
|
||||
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd -ms /bin/bash -d /pretix -u 15371 pretixuser
|
||||
RUN echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers
|
||||
|
||||
RUN mkdir /etc/pretix
|
||||
RUN mkdir /data
|
||||
VOLUME /etc/pretix
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
DJANGO_SETTINGS_MODULE=production_settings
|
||||
|
||||
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
|
||||
RUN chmod +x /usr/local/bin/pretix
|
||||
COPY deployment/docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
RUN rm /etc/nginx/sites-enabled/default
|
||||
|
||||
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
COPY src /pretix/src
|
||||
WORKDIR /pretix/src
|
||||
ADD deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
ENV DJANGO_SETTINGS_MODULE production_settings
|
||||
|
||||
RUN pip3 install -U pip wheel setuptools
|
||||
RUN pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
|
||||
-r requirements/memcached.txt -r requirements/redis.txt \
|
||||
-r requirements/py34.txt gunicorn
|
||||
RUN chmod +x /usr/local/bin/pretix && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
pip3 install -U pip wheel setuptools && \
|
||||
cd /pretix/src && \
|
||||
pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
|
||||
-r requirements/memcached.txt -r requirements/redis.txt \
|
||||
-r requirements/py34.txt gunicorn && \
|
||||
mkdir -p data && \
|
||||
chown -R pretixuser:pretixuser /static /pretix /data data && \
|
||||
sudo -u pretixuser make production
|
||||
|
||||
RUN mkdir /static && chown -R pretixuser:pretixuser /static /pretix /data
|
||||
USER pretixuser
|
||||
RUN make production
|
||||
|
||||
VOLUME ["/etc/pretix", "/data"]
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["pretix"]
|
||||
CMD ["all"]
|
||||
|
||||
31
README.rst
31
README.rst
@@ -16,19 +16,23 @@ pretix
|
||||
|
||||
Reinventing ticket presales, one bit at a time.
|
||||
|
||||
Project status
|
||||
--------------
|
||||
Most features are present and sufficiently stable. pretix has been in use for multiple event and
|
||||
sold a few thousand tickets so far. There is still a bunch of features to come and there surely is
|
||||
still a bunch of bugs in there, but we consider it stable enough that we use it in production ourselves.
|
||||
Project status & release cycle
|
||||
------------------------------
|
||||
|
||||
If you deploy and use pretix, there will be a safe upgrade path for all changes to come. We're planning
|
||||
on an 1.0 release in late 2016 or early 2017. Until then, we take the liberty of changing the code as we
|
||||
like, but we try to keep the changes to documented APIs as small as possible. If you want to use pretix
|
||||
in production or develop a plugin now, I invite you to send me an email so that I can notify you of changes
|
||||
and bugs that require your attention.
|
||||
While there is always a lot to do and improve on, pretix by now has been in use for more than a dozen
|
||||
conferences that sold over ten thousand tickets combined without major problems. We therefore think of
|
||||
pretix as being stable and ready to use.
|
||||
|
||||
Since very recently we now have an `installation guide`_ in our documentation.
|
||||
If you want to use or extend pretix, we strongly recommend to follow our `blog`_. We will announce all
|
||||
releases there. You can always find the latest stable version on PyPI or in the ``release/X.Y`` branch of
|
||||
this repository. The ``master`` branch contains a development version that we also try to keep stable in
|
||||
the sense that it does not break your data, but its APIs might change without prior notice.
|
||||
|
||||
To get started using pretix on your own server, look at the `installation guide`_ in our documentation.
|
||||
|
||||
This project is 100 percent free and open source software. If you are interested in commercial support,
|
||||
hosting services or supporting this project financially, please go to `pretix.eu`_ or contact us at
|
||||
support@pretix.eu.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
@@ -43,10 +47,7 @@ See the LICENSE file for the complete license text.
|
||||
This project is maintained by Raphael Michel <mail@raphaelmichel.de>. See the
|
||||
AUTHORS file for a list of all the awesome folks who contributed to this project.
|
||||
|
||||
This project is 100 percent free and open source software. If you are interested in
|
||||
commercial support, hosting services or supporting this project financially, please
|
||||
go to `pretix.eu`_ or contact Raphael directly.
|
||||
|
||||
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
|
||||
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html
|
||||
.. _pretix.eu: https://pretix.eu
|
||||
.. _blog: https://pretix.eu/about/en/blog/
|
||||
|
||||
@@ -48,6 +48,14 @@ http {
|
||||
expires 7d;
|
||||
access_log off;
|
||||
}
|
||||
location ^~ /media/cachedfiles {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location ^~ /media/invoices {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location /static/ {
|
||||
alias /static/;
|
||||
access_log off;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from pretix.settings import *
|
||||
|
||||
|
||||
LOGGING['handlers']['mail_admins']['include_html'] = True
|
||||
STATIC_ROOT = '/static'
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
|
||||
@@ -247,7 +247,7 @@ Sentry
|
||||
------
|
||||
|
||||
pretix has native support for sentry, a tool that you can use to track errors in the
|
||||
application. If you want to use sentry, you need to set a DSN in the configuration file.
|
||||
application. If you want to use sentry, you need to set a DSN in the configuration file::
|
||||
|
||||
[sentry]
|
||||
dsn=https://<key>:<secret>@sentry.io/<project>
|
||||
|
||||
@@ -111,7 +111,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
datadir=/data
|
||||
|
||||
[database]
|
||||
; Replace mysql with psycopg2 for PostgreSQL
|
||||
; Replace mysql with postgresql_psycopg2 for PostgreSQL
|
||||
backend=mysql
|
||||
name=pretix
|
||||
user=pretix
|
||||
|
||||
@@ -38,10 +38,10 @@ Unix user
|
||||
|
||||
As we do not want to run pretix as root, we first create a new unprivileged user::
|
||||
|
||||
# sudo adduser pretix --disabled-password --home /var/pretix
|
||||
# adduser pretix --disabled-password --home /var/pretix
|
||||
|
||||
In this guide, all code lines prepended with a ``#`` symbol are commands that you need to execute on your server as
|
||||
``root`` user; all lines prepended with a ``$`` symbol should be run by the unprivileged user.
|
||||
``root`` user (e.g. using ``sudo``); all lines prepended with a ``$`` symbol should be run by the unprivileged user.
|
||||
|
||||
Database
|
||||
--------
|
||||
@@ -82,7 +82,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
datadir=/var/pretix/data
|
||||
|
||||
[database]
|
||||
; Replace mysql with psycopg2 for PostgreSQL
|
||||
; Replace mysql with postgresql_psycopg2 for PostgreSQL
|
||||
backend=mysql
|
||||
name=pretix
|
||||
user=pretix
|
||||
@@ -100,8 +100,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
sessions=true
|
||||
|
||||
[celery]
|
||||
backend=redis://127.0.0.1?virtual_host=1
|
||||
broker=redis://127.0.0.1?virtual_host=2
|
||||
backend=redis://127.0.0.1/1
|
||||
broker=redis://127.0.0.1/2
|
||||
|
||||
See :ref:`email configuration <mail-settings>` to learn more about configuring mail features.
|
||||
|
||||
@@ -119,7 +119,10 @@ python installation::
|
||||
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
|
||||
command if you're running PostgreSQL::
|
||||
|
||||
(venv)$ pip install "pretix[mysql]" gunicorn
|
||||
(venv)$ pip3 install "pretix[mysql]" gunicorn
|
||||
|
||||
If you are running Python 3.4, you also need to ``pip3 install typing``. This is not required on 3.5 or newer.
|
||||
You can find out your Python version using ``python -V``.
|
||||
|
||||
We also need to create a data directory::
|
||||
|
||||
@@ -127,8 +130,8 @@ We also need to create a data directory::
|
||||
|
||||
Finally, we compile static files and translation data and create the database structure::
|
||||
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
|
||||
|
||||
Start pretix as a service
|
||||
@@ -150,7 +153,7 @@ named ``/etc/systemd/system/pretix-web.service`` with the following content::
|
||||
--name pretix --workers 5 \
|
||||
--max-requests 1200 --max-requests-jitter 50 \
|
||||
--log-level=info --bind=127.0.0.1:8345
|
||||
WorkingDirectory=/var/pretix/source/src
|
||||
WorkingDirectory=/var/pretix
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
@@ -168,7 +171,7 @@ For background tasks we need a second service ``/etc/systemd/system/pretix-worke
|
||||
Environment="VIRTUAL_ENV=/var/pretix/venv"
|
||||
Environment="PATH=/var/pretix/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/var/pretix/venv/bin/celery -A pretix.celery_app worker -l info
|
||||
WorkingDirectory=/var/pretix/source/src
|
||||
WorkingDirectory=/var/pretix
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
@@ -223,6 +226,15 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ^~ /media/cachedfiles {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location ^~ /media/invoices {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /var/pretix/source/src/pretix/static.dist/;
|
||||
access_log off;
|
||||
|
||||
@@ -19,7 +19,7 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: order_paid, order_placed
|
||||
:members: validate_cart, order_paid, order_placed
|
||||
|
||||
Frontend
|
||||
--------
|
||||
@@ -47,7 +47,7 @@ Backend
|
||||
-------
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, quota_detail_html
|
||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, organizer_edit_tabs
|
||||
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
|
||||
@@ -42,6 +42,13 @@ configuration class. The metadata class must define the following attributes:
|
||||
``description`` (``str``):
|
||||
A more verbose description of what your plugin does.
|
||||
|
||||
``visible`` (``bool``):
|
||||
``True`` by default, can hide a plugin so it cannot be normally activated.
|
||||
|
||||
``restricted`` (``bool``):
|
||||
``False`` by default, restricts a plugin such that it can only be enabled for an event
|
||||
by system administrators / superusers.
|
||||
|
||||
A working example would be::
|
||||
|
||||
# file: pretix/plugins/timerestriction/__init__.py
|
||||
@@ -57,6 +64,8 @@ A working example would be::
|
||||
name = _("PayPal")
|
||||
author = _("the pretix team")
|
||||
version = '1.0.0'
|
||||
visible = True
|
||||
restricted = False
|
||||
description = _("This plugin allows you to receive payments via PayPal")
|
||||
|
||||
|
||||
|
||||
@@ -64,4 +64,6 @@ The output class
|
||||
|
||||
.. automethod:: generate
|
||||
|
||||
.. automethod:: generate_order
|
||||
|
||||
.. autoattribute:: download_button_text
|
||||
|
||||
@@ -17,6 +17,8 @@ ways that pretix itself is:
|
||||
The following plugins are not shipped with pretix but are maintained by the
|
||||
same team:
|
||||
|
||||
* `SEPA direct debit`_
|
||||
* `Pages`_
|
||||
* `Passbook/Wallet ticket output`_
|
||||
* `Cartshare`_
|
||||
|
||||
@@ -25,6 +27,8 @@ no statements about their stability:
|
||||
|
||||
* `esPass ticket output`_
|
||||
|
||||
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
|
||||
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
|
||||
.. _Cartshare: https://github.com/pretix/pretix-cartshare
|
||||
.. _Pages: https://github.com/pretix/pretix-pages
|
||||
.. _esPass ticket output: https://github.com/esPass/pretix-espass
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.0.0b2"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -19,6 +19,6 @@ class PretixBaseConfig(AppConfig):
|
||||
|
||||
default_app_config = 'pretix.base.PretixBaseConfig'
|
||||
try:
|
||||
import pretix.celery_app as celery # NOQA
|
||||
import pretix.celery_app as celery # NOQA
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytz
|
||||
from django import forms
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition
|
||||
@@ -50,7 +51,7 @@ class OrderListExporter(BaseExporter):
|
||||
tz = pytz.timezone(self.event.settings.timezone)
|
||||
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
||||
|
||||
qs = self.event.orders.all().select_related('invoice_address')
|
||||
qs = self.event.orders.all().select_related('invoice_address').prefetch_related('invoices')
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(status=Order.STATUS_PAID)
|
||||
tax_rates = self._get_all_tax_rates(qs)
|
||||
@@ -58,7 +59,7 @@ class OrderListExporter(BaseExporter):
|
||||
headers = [
|
||||
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
|
||||
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Payment date'), _('Payment type'), _('Payment method fee')
|
||||
_('Payment date'), _('Payment type'), _('Payment method fee'), _('Invoice numbers')
|
||||
]
|
||||
|
||||
for tr in tax_rates:
|
||||
@@ -86,7 +87,7 @@ class OrderListExporter(BaseExporter):
|
||||
for order in qs.order_by('datetime'):
|
||||
row = [
|
||||
order.code,
|
||||
str(order.total),
|
||||
localize(order.total),
|
||||
order.get_status_display(),
|
||||
order.email,
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -107,7 +108,7 @@ class OrderListExporter(BaseExporter):
|
||||
row += [
|
||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||
provider_names.get(order.payment_provider, order.payment_provider),
|
||||
str(order.payment_fee)
|
||||
localize(order.payment_fee)
|
||||
]
|
||||
|
||||
for tr in tax_rates:
|
||||
@@ -117,11 +118,12 @@ class OrderListExporter(BaseExporter):
|
||||
taxrate_values['taxsum'] += order.payment_fee_tax_value
|
||||
|
||||
row += [
|
||||
str(taxrate_values['grosssum']),
|
||||
str(taxrate_values['grosssum'] - taxrate_values['taxsum']),
|
||||
str(taxrate_values['taxsum']),
|
||||
localize(taxrate_values['grosssum']),
|
||||
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']),
|
||||
localize(taxrate_values['taxsum']),
|
||||
]
|
||||
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
writer.writerow(row)
|
||||
|
||||
return 'orders.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from ...signals import periodic_task
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Rebuild static files and language files"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
periodic_task.send(self)
|
||||
call_command('compilemessages', verbosity=1, interactive=False)
|
||||
call_command('compilejsi18n', verbosity=1, interactive=False)
|
||||
call_command('collectstatic', verbosity=1, interactive=False)
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-03 14:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.i18n
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.invoices
|
||||
import pretix.base.models.items
|
||||
import pretix.base.models.orders
|
||||
import pretix.base.models.vouchers
|
||||
|
||||
|
||||
def initial_user(apps, schema_editor):
|
||||
User = apps.get_model("pretixbase", "User")
|
||||
user = User(email='admin@localhost')
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.password = make_password('admin')
|
||||
user.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('pretixbase', '0001_initial'), ('pretixbase', '0002_auto_20160209_0940'), ('pretixbase', '0003_eventpermission_can_change_vouchers'), ('pretixbase', '0004_auto_20160209_1023'), ('pretixbase', '0005_auto_20160211_1459'), ('pretixbase', '0006_auto_20160211_1630'), ('pretixbase', '0007_auto_20160211_1710'), ('pretixbase', '0008_invoiceaddress'), ('pretixbase', '0009_auto_20160222_2002'), ('pretixbase', '0010_orderposition_secret'), ('pretixbase', '0011_auto_20160311_2052'), ('pretixbase', '0012_auto_20160312_1040'), ('pretixbase', '0013_invoice_locale'), ('pretixbase', '0014_invoice_additional_text'), ('pretixbase', '0015_auto_20160312_1924'), ('pretixbase', '0016_voucher_variation'), ('pretixbase', '0017_auto_20160324_1615'), ('pretixbase', '0018_auto_20160326_1104'), ('pretixbase', '0019_auto_20160326_1139'), ('pretixbase', '0020_auto_20160418_2106'), ('pretixbase', '0021_auto_20160418_2117'), ('pretixbase', '0020_auto_20160421_1943'), ('pretixbase', '0022_merge'), ('pretixbase', '0023_auto_20160601_1039'), ('pretixbase', '0024_auto_20160728_1725'), ('pretixbase', '0025_auto_20160802_2202'), ('pretixbase', '0026_order_comment'), ('pretixbase', '0027_auto_20160815_1254'), ('pretixbase', '0028_auto_20160816_1242')]
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0006_require_contenttypes_0002'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
|
||||
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
||||
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('is_staff', models.BooleanField(default=False, verbose_name='Is site admin')),
|
||||
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='Date joined')),
|
||||
('locale', models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)')], default='en', max_length=50, verbose_name='Language')),
|
||||
('timezone', models.CharField(default='UTC', max_length=100, verbose_name='Timezone')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User',
|
||||
'verbose_name_plural': 'Users',
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=initial_user,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CachedFile',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('expires', models.DateTimeField(blank=True, null=True)),
|
||||
('date', models.DateTimeField(blank=True, null=True)),
|
||||
('filename', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=255)),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.base.cachedfile_name)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CachedTicket',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('provider', models.CharField(max_length=255)),
|
||||
('cachedfile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.CachedFile')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CartPosition',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
|
||||
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
|
||||
('voucher_discount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('base_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('cart_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)')),
|
||||
('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiration date')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Cart position',
|
||||
'verbose_name_plural': 'Cart positions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', pretix.base.i18n.I18nCharField(max_length=200, verbose_name='Name')),
|
||||
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
|
||||
('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')),
|
||||
('date_from', models.DateTimeField(verbose_name='Event start time')),
|
||||
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
|
||||
('is_public', models.BooleanField(default=False, help_text="If selected, this event may show up on the ticket system's start page or an organization profile.", verbose_name='Visible in public lists')),
|
||||
('presale_end', models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True, verbose_name='End of presale')),
|
||||
('presale_start', models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True, verbose_name='Start of presale')),
|
||||
('plugins', models.TextField(blank=True, null=True, verbose_name='Plugins')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Event',
|
||||
'ordering': ('date_from', 'name'),
|
||||
'verbose_name_plural': 'Events',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventLock',
|
||||
fields=[
|
||||
('event', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('date', models.DateTimeField(auto_now=True)),
|
||||
('token', models.UUIDField(default=uuid.uuid4)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('can_change_settings', models.BooleanField(default=True, verbose_name='Can change event settings')),
|
||||
('can_change_items', models.BooleanField(default=True, verbose_name='Can change product settings')),
|
||||
('can_view_orders', models.BooleanField(default=True, verbose_name='Can view orders')),
|
||||
('can_change_permissions', models.BooleanField(default=True, verbose_name='Can change permissions')),
|
||||
('can_change_orders', models.BooleanField(default=True, verbose_name='Can change orders')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_perms', to='pretixbase.Event')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL)),
|
||||
('can_change_vouchers', models.BooleanField(default=True, verbose_name='Can change vouchers')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Event permission',
|
||||
'verbose_name_plural': 'Event permissions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=255)),
|
||||
('value', models.TextField()),
|
||||
('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='setting_objects', to='pretixbase.Event')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Item',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Item name')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('description', pretix.base.i18n.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')),
|
||||
('default_price', models.DecimalField(decimal_places=2, max_digits=7, null=True, verbose_name='Default price')),
|
||||
('tax_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Taxes included in percent')),
|
||||
('admission', models.BooleanField(default=False, help_text='Whether or not buying this product allows a person to enter your event', verbose_name='Is an admission ticket')),
|
||||
('position', models.IntegerField(default=0)),
|
||||
('picture', models.ImageField(blank=True, null=True, upload_to=pretix.base.models.items.itempicture_upload_to, verbose_name='Product picture')),
|
||||
('available_from', models.DateTimeField(blank=True, help_text='This product will not be sold before the given date.', null=True, verbose_name='Available from')),
|
||||
('available_until', models.DateTimeField(blank=True, help_text='This product will not be sold after the given date.', null=True, verbose_name='Available until')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Product',
|
||||
'ordering': ('category__position', 'category', 'position'),
|
||||
'verbose_name_plural': 'Products',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ItemCategory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Category name')),
|
||||
('position', models.IntegerField(default=0)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='pretixbase.Event')),
|
||||
('description', pretix.base.i18n.I18nTextField(blank=True, verbose_name='Category description')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Product category',
|
||||
'ordering': ('position', 'id'),
|
||||
'verbose_name_plural': 'Product categories',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ItemVariation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Description')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('position', models.PositiveIntegerField(default=0, verbose_name='Position')),
|
||||
('default_price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Default price')),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variations', to='pretixbase.Item')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Product variation',
|
||||
'ordering': ('position', 'id'),
|
||||
'verbose_name_plural': 'Product variations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LogEntry',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||
('action_type', models.CharField(max_length=255)),
|
||||
('data', models.TextField(default='{}')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiration date')),
|
||||
('payment_date', models.DateTimeField(blank=True, null=True, verbose_name='Payment date')),
|
||||
('payment_provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')),
|
||||
('payment_fee', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee')),
|
||||
('payment_info', models.TextField(blank=True, null=True, verbose_name='Payment information')),
|
||||
('payment_manual', models.BooleanField(default=False, verbose_name='Payment state was manually modified')),
|
||||
('total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Total amount')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='pretixbase.Event', verbose_name='Event')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Order',
|
||||
'ordering': ('-datetime',),
|
||||
'verbose_name_plural': 'Orders',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderPosition',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
|
||||
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
|
||||
('voucher_discount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('base_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item', verbose_name='Item')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='positions', to='pretixbase.Order', verbose_name='Order')),
|
||||
('variation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Order position',
|
||||
'verbose_name_plural': 'Order positions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Organizer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organizer',
|
||||
'ordering': ('name',),
|
||||
'verbose_name_plural': 'Organizers',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizerPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('can_create_events', models.BooleanField(default=True, verbose_name='Can create events')),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_perms', to='pretixbase.Organizer')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organizer permission',
|
||||
'verbose_name_plural': 'Organizer permissions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizerSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=255)),
|
||||
('value', models.TextField()),
|
||||
('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='setting_objects', to='pretixbase.Organizer')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Question',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('question', pretix.base.i18n.I18nTextField(verbose_name='Question')),
|
||||
('type', models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], max_length=5, verbose_name='Question type')),
|
||||
('required', models.BooleanField(default=False, verbose_name='Required question')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='pretixbase.Event')),
|
||||
('items', models.ManyToManyField(blank=True, help_text='This question will be asked to buyers of the selected products', related_name='questions', to='pretixbase.Item', verbose_name='Products')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Question',
|
||||
'verbose_name_plural': 'Questions',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='QuestionAnswer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('answer', models.TextField()),
|
||||
('cartposition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.CartPosition')),
|
||||
('orderposition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.OrderPosition')),
|
||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.Question')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Quota',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||
('size', models.PositiveIntegerField(blank=True, help_text='Leave empty for an unlimited number of tickets.', null=True, verbose_name='Total capacity')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.Event', verbose_name='Event')),
|
||||
('items', models.ManyToManyField(blank=True, related_name='quotas', to='pretixbase.Item', verbose_name='Item')),
|
||||
('variations', models.ManyToManyField(blank=True, related_name='quotas', to='pretixbase.ItemVariation', verbose_name='Variations')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Quota',
|
||||
'verbose_name_plural': 'Quotas',
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Voucher',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=255, verbose_name='Voucher code')),
|
||||
('valid_until', models.DateTimeField(blank=True, null=True, verbose_name='Valid until')),
|
||||
('block_quota', models.BooleanField(default=False, help_text="If activated, this voucher will be substracted from the affected product's quotas, such that it is guaranteed that anyone with this voucher code does receive a ticket.", verbose_name='Reserve ticket from quota')),
|
||||
('allow_ignore_quota', models.BooleanField(default=False, help_text='If activated, a holder of this voucher code can buy tickets, even if there are none left.', verbose_name='Allow to bypass quota')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Set product price to')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Event', verbose_name='Event')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Voucher',
|
||||
'verbose_name_plural': 'Vouchers',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizer',
|
||||
name='permitted',
|
||||
field=models.ManyToManyField(related_name='organizers', through='pretixbase.OrganizerPermission', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='voucher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.ItemCategory', verbose_name='Category'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.Event', verbose_name='Event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='organizer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='pretixbase.Organizer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='permitted',
|
||||
field=models.ManyToManyField(related_name='events', through='pretixbase.EventPermission', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event', verbose_name='Event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='item',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item', verbose_name='Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='variation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='voucher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='order',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='item',
|
||||
field=models.ForeignKey(blank=True, help_text="This product is added to the user's cart if the voucher is redeemed.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Item', verbose_name='Product'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='If empty, the product will cost its normal price.', max_digits=10, null=True, verbose_name='Set product price to'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='redeemed',
|
||||
field=models.BooleanField(default=False, verbose_name='Redeemed'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='voucher',
|
||||
unique_together=set([('event', 'code')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='code',
|
||||
field=models.CharField(default=pretix.base.models.vouchers.generate_code, max_length=255, verbose_name='Voucher code'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='tax_rate',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7, verbose_name='Tax rate'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='tax_value',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Tax value'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='tax_rate',
|
||||
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=7, verbose_name='Taxes included in percent'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceAddress',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_modified', models.DateTimeField(auto_now=True)),
|
||||
('company', models.CharField(blank=True, max_length=255, verbose_name='Company name')),
|
||||
('name', models.CharField(blank=True, max_length=255, verbose_name='Full name')),
|
||||
('street', models.TextField(verbose_name='Address')),
|
||||
('zipcode', models.CharField(max_length=30, verbose_name='ZIP code')),
|
||||
('city', models.CharField(max_length=255, verbose_name='City')),
|
||||
('country', models.CharField(max_length=255, verbose_name='Country')),
|
||||
('vat_id', models.CharField(blank=True, max_length=255, verbose_name='VAT ID')),
|
||||
('order', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoice_address', to='pretixbase.Order')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='live',
|
||||
field=models.BooleanField(default=False, verbose_name='Shop is live'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_rate',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee tax rate'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_value',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee tax'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invoice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('invoice_no', models.PositiveIntegerField(db_index=True)),
|
||||
('is_cancelled', models.BooleanField(default=False)),
|
||||
('invoice_from', models.TextField()),
|
||||
('invoice_to', models.TextField()),
|
||||
('date', models.DateField(default=datetime.date.today)),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.invoices.invoice_filename)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pretixbase.Event')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pretixbase.Order')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceLine',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.TextField()),
|
||||
('gross_value', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('tax_value', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=7)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='pretixbase.Invoice')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='locale',
|
||||
field=models.CharField(default='en', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='additional_text',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='is_cancelled',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='is_cancellation',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='refers',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='refered', to='pretixbase.Invoice'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='invoice',
|
||||
unique_together=set([('event', 'invoice_no')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='variation',
|
||||
field=models.ForeignKey(blank=True, help_text='This variation of the product select above is being used.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.ItemVariation', verbose_name='Product variation'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='logentry',
|
||||
options={'ordering': ('-datetime',)},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='free_price',
|
||||
field=models.BooleanField(default=False, help_text='If this option is active, your users can choose the price themselves. The price configured above is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect additional donations for your event.', verbose_name='Free price input'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='QuestionOption',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('answer', pretix.base.i18n.I18nCharField(verbose_name='Answer')),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list')], max_length=5, verbose_name='Question type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionoption',
|
||||
name='question',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='pretixbase.Question'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionanswer',
|
||||
name='options',
|
||||
field=models.ManyToManyField(blank=True, related_name='answers', to='pretixbase.QuestionOption'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='quota',
|
||||
field=models.ForeignKey(blank=True, help_text='If enabled, the voucher is valid for any product affected by this quota.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quota', to='pretixbase.Quota', verbose_name='Quota'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, help_text='The text entered in this field will not be visible to the user and is available for your convenience.', verbose_name='Comment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='tag',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='You can use this field to group multiple vouchers together. If you enter the same value for multiple vouchers, you can get statistics on how many of them have been redeemed etc.', max_length=255, verbose_name='Tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='hide_without_voucher',
|
||||
field=models.BooleanField(default=False, help_text='This product will be hidden from the event page until the user enters a voucher code that is specifically tied to this product (and not via a quota).', verbose_name='This product will only be shown if a voucher matching the product is redeemed.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='require_voucher',
|
||||
field=models.BooleanField(default=False, help_text='To buy this product, the user needs a voucher that applies to this product either directly or via a quota.', verbose_name='This product can only be bought using a voucher.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='logentry',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='logentry',
|
||||
name='object_id',
|
||||
field=models.PositiveIntegerField(db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='code',
|
||||
field=models.CharField(db_index=True, max_length=16, verbose_name='Order code'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], db_index=True, max_length=3, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='code',
|
||||
field=models.CharField(db_index=True, default=pretix.base.models.vouchers.generate_code, max_length=255, verbose_name='Voucher code'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, help_text='The text entered in this field will not be visible to the user and is available for your convenience.', verbose_name='Comment'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cartposition',
|
||||
name='base_price',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cartposition',
|
||||
name='voucher_discount',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='orderposition',
|
||||
name='base_price',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='orderposition',
|
||||
name='voucher_discount',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-03 14:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
import django.db.migrations.operations.special
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
def preserve_event_settings(apps, schema_editor):
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
EventSetting = apps.get_model('pretixbase', 'EventSetting')
|
||||
for e in Event.objects.all():
|
||||
EventSetting.objects.create(object=e, key='mail_days_order_expire_warning', value='0')
|
||||
|
||||
|
||||
def forwards42(apps, schema_editor):
|
||||
Order = apps.get_model('pretixbase', 'Order')
|
||||
EventSetting = apps.get_model('pretixbase', 'EventSetting')
|
||||
etz = {
|
||||
s['object_id']: s['value']
|
||||
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
|
||||
}
|
||||
for order in Order.objects.all():
|
||||
tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
|
||||
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
|
||||
order.save()
|
||||
|
||||
|
||||
def forwards44(apps, schema_editor):
|
||||
CachedTicket = apps.get_model('pretixbase', 'CachedTicket')
|
||||
CachedTicket.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('pretixbase', '0031_auto_20160816_0648'), ('pretixbase', '0032_question_position'), ('pretixbase', '0033_auto_20160821_2222'), ('pretixbase', '0034_auto_20160830_1952'), ('pretixbase', '0032_item_allow_cancel'), ('pretixbase', '0033_auto_20160822_1044'), ('pretixbase', '0035_merge'), ('pretixbase', '0036_auto_20160902_0755'), ('pretixbase', '0037_invoice_payment_provider_text'), ('pretixbase', '0038_auto_20160924_1448'), ('pretixbase', '0039_user_require_2fa'), ('pretixbase', '0040_u2fdevice'), ('pretixbase', '0041_auto_20161018_1654'), ('pretixbase', '0042_order_expires'), ('pretixbase', '0043_globalsetting'), ('pretixbase', '0044_auto_20161101_1610'), ('pretixbase', '0045_auto_20161108_1542'), ('pretixbase', '0046_order_meta_info'), ('pretixbase', '0047_auto_20161126_1300'), ('pretixbase', '0048_auto_20161129_1330')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0030_auto_20160816_0646'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='invoice',
|
||||
old_name='invoice_no_charfield',
|
||||
new_name='invoice_no',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='footer_text',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='introductory_text',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='invoice',
|
||||
unique_together=set([('event', 'invoice_no')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='position',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='question',
|
||||
options={'ordering': ('position', 'id'), 'verbose_name': 'Question', 'verbose_name_plural': 'Questions'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='allow_cancel',
|
||||
field=models.BooleanField(default=True, help_text='If you deactivate this, an order including this product might not be cancelled by the user. It may still be cancelled by you.', verbose_name='Allow product to be cancelled'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='expiry_reminder_sent',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=preserve_event_settings,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='payment_provider_text',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventpermission',
|
||||
name='can_view_vouchers',
|
||||
field=models.BooleanField(default=True, verbose_name='Can view vouchers'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='allow_cancel',
|
||||
field=models.BooleanField(default=True, help_text='If you deactivate this, an order including this product might not be canceled by the user. It may still be canceled by you.', verbose_name='Allow product to be canceled'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'canceled'), ('r', 'refunded')], db_index=True, max_length=3, verbose_name='Status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='require_2fa',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='U2FDevice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='The human-readable name of this device.', max_length=64)),
|
||||
('confirmed', models.BooleanField(default=True, help_text='Is this device ready for use?')),
|
||||
('json_data', models.TextField()),
|
||||
('user', models.ForeignKey(help_text='The user that this device belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cachedticket',
|
||||
name='cachedfile',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.CachedFile'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=forwards42,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalSetting',
|
||||
fields=[
|
||||
('key', models.CharField(max_length=255, primary_key=True, serialize=False)),
|
||||
('value', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=forwards44,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cachedticket',
|
||||
name='order',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='order_position',
|
||||
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.OrderPosition'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='expires',
|
||||
field=models.DateTimeField(db_index=True, verbose_name='Expiration date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='redeemed',
|
||||
field=models.BooleanField(db_index=True, default=False, verbose_name='Redeemed'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='valid_until',
|
||||
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Valid until'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='meta_info',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='max_usages',
|
||||
field=models.PositiveIntegerField(default=1, help_text='Number of times this voucher can be redeemed.', verbose_name='Maximum usages'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='redeemed',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Redeemed'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='price_mode',
|
||||
field=models.CharField(choices=[('none', 'No effect'), ('set', 'Set product price to'), ('subtract', 'Subtract from product price'), ('percent', 'Reduce product price by (%)')], default='set', max_length=100, verbose_name='Price mode'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='voucher',
|
||||
old_name='price',
|
||||
new_name='value',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='value',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Voucher value'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,219 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-03 14:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
import django.db.migrations.operations.special
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.i18n
|
||||
import pretix.base.models.event
|
||||
import pretix.base.models.orders
|
||||
import pretix.base.models.organizer
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
def forwards50(apps, schema_editor):
|
||||
Order = apps.get_model('pretixbase', 'Order')
|
||||
for o in Order.objects.all():
|
||||
for i, p in enumerate(o.positions.all()):
|
||||
p.positionid = i + 1
|
||||
p.save()
|
||||
|
||||
|
||||
def invalidate_ticket_cache(apps, schema_editor):
|
||||
CachedTicket = apps.get_model('pretixbase', 'CachedTicket')
|
||||
for ct in CachedTicket.objects.all():
|
||||
try:
|
||||
if ct.cachedfile:
|
||||
ct.cachedfile.delete()
|
||||
if ct.cachedfile.file:
|
||||
ct.cachedfile.file.delete(False)
|
||||
except models.Model.DoesNotExist:
|
||||
pass
|
||||
ct.delete()
|
||||
|
||||
|
||||
def merge_names(apps, schema_editor):
|
||||
User = apps.get_model('pretixbase', 'User')
|
||||
for u in User.objects.all():
|
||||
if u.givenname:
|
||||
if u.familyname:
|
||||
u.fullname = u.givenname + " " + u.familyname
|
||||
else:
|
||||
u.fullname = u.givenname
|
||||
elif u.familyname:
|
||||
u.fullname = u.familyname
|
||||
u.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('pretixbase', '0050_orderposition_positionid'), ('pretixbase', '0051_auto_20161221_1720'), ('pretixbase', '0052_auto_20161231_1533'), ('pretixbase', '0053_auto_20170104_1252'), ('pretixbase', '0054_auto_20170107_1058'), ('pretixbase', '0055_organizerpermission_can_change_permissions'), ('pretixbase', '0056_auto_20170107_1251'), ('pretixbase', '0057_auto_20170107_1531'), ('pretixbase', '0058_auto_20170107_1533'), ('pretixbase', '0059_cachedcombinedticket'), ('pretixbase', '0060_auto_20170113_1438'), ('pretixbase', '0061_event_location')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0049_checkin'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='positionid',
|
||||
field=models.PositiveIntegerField(default=1),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=forwards50,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=invalidate_ticket_cache,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cachedticket',
|
||||
name='cachedfile',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='extension',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedticket_name),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='type',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='checkin',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='checkin',
|
||||
name='position',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to='pretixbase.OrderPosition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='price_mode',
|
||||
field=models.CharField(choices=[('none', 'No effect'), ('set', 'Set product price to'), ('subtract', 'Subtract from product price'), ('percent', 'Reduce product price by (%)')], default='none', max_length=100, verbose_name='Price mode'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RequiredAction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('done', models.BooleanField(default=False)),
|
||||
('action_type', models.CharField(max_length=255)),
|
||||
('data', models.TextField(default='{}')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('datetime',),
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='requiredaction',
|
||||
name='event',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='requiredaction',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventpermission',
|
||||
name='invite_email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventpermission',
|
||||
name='invite_token',
|
||||
field=models.CharField(blank=True, default=pretix.base.models.event.generate_invite_token, max_length=64, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventpermission',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizerpermission',
|
||||
name='can_change_permissions',
|
||||
field=models.BooleanField(default=True, verbose_name='Can change permissions'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizerpermission',
|
||||
name='invite_email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizerpermission',
|
||||
name='invite_token',
|
||||
field=models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizerpermission',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='fullname',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Full name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=merge_names,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='familyname',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='givenname',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CachedCombinedTicket',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('provider', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=255)),
|
||||
('extension', models.CharField(max_length=255)),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')),
|
||||
('created', models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
field=pretix.base.i18n.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
|
||||
29
src/pretix/base/migrations/0059_cachedcombinedticket.py
Normal file
29
src/pretix/base/migrations/0059_cachedcombinedticket.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-13 14:07
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.orders
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0058_auto_20170107_1533'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CachedCombinedTicket',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('provider', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=255)),
|
||||
('extension', models.CharField(max_length=255)),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')),
|
||||
],
|
||||
),
|
||||
]
|
||||
28
src/pretix/base/migrations/0060_auto_20170113_1438.py
Normal file
28
src/pretix/base/migrations/0060_auto_20170113_1438.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-13 14:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0059_cachedcombinedticket'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cachedcombinedticket',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
22
src/pretix/base/migrations/0061_event_location.py
Normal file
22
src/pretix/base/migrations/0061_event_location.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-01 04:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import pretix.base.i18n
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0060_auto_20170113_1438'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
field=pretix.base.i18n.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'),
|
||||
),
|
||||
]
|
||||
@@ -12,8 +12,10 @@ from .items import (
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .orders import (
|
||||
AbstractPosition, CachedTicket, CartPosition, InvoiceAddress, Order,
|
||||
OrderPosition, QuestionAnswer, generate_position_secret, generate_secret,
|
||||
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
|
||||
InvoiceAddress, Order, OrderPosition, QuestionAnswer,
|
||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
|
||||
from .vouchers import Voucher
|
||||
|
||||
@@ -51,6 +51,8 @@ class Event(LoggedModel):
|
||||
:type presale_start: datetime
|
||||
:param presale_end: No tickets will be sold after this date.
|
||||
:type presale_end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this
|
||||
event.
|
||||
:type plugins: str
|
||||
@@ -99,6 +101,11 @@ class Event(LoggedModel):
|
||||
verbose_name=_("Start of presale"),
|
||||
help_text=_("No products will be sold before this date."),
|
||||
)
|
||||
location = I18nCharField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
plugins = models.TextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Plugins"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import os
|
||||
import string
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
@@ -228,7 +229,7 @@ class Order(LoggedModel):
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
|
||||
if not Order.objects.filter(event=self.event, code=code).exists():
|
||||
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
|
||||
self.code = code
|
||||
return
|
||||
|
||||
@@ -268,12 +269,15 @@ class Order(LoggedModel):
|
||||
|
||||
def _can_be_paid(self) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'late': _("The payment is too late to be accepted."),
|
||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||
"payment settings is over."),
|
||||
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
|
||||
"payments should be accepted in the payment settings."),
|
||||
}
|
||||
|
||||
if self.event.settings.get('payment_term_last'):
|
||||
if now() > self.event.payment_term_last:
|
||||
return error_messages['late']
|
||||
return error_messages['late_lastdate']
|
||||
if self.status == self.STATUS_PENDING:
|
||||
return True
|
||||
if not self.event.settings.get('payment_term_accept_late'):
|
||||
@@ -565,12 +569,24 @@ class InvoiceAddress(models.Model):
|
||||
|
||||
def cachedticket_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||
return 'tickets/{org}/{ev}/{code}-{no}-{prov}-{secret}.pdf'.format(
|
||||
return 'tickets/{org}/{ev}/{code}-{no}-{prov}-{secret}.dat'.format(
|
||||
org=instance.order_position.order.event.organizer.slug,
|
||||
ev=instance.order_position.order.event.slug,
|
||||
prov=instance.provider,
|
||||
no=instance.order_position.positionid,
|
||||
code=instance.order_position.order.code,
|
||||
secret=secret,
|
||||
ext=os.path.splitext(filename)[1]
|
||||
)
|
||||
|
||||
|
||||
def cachedcombinedticket_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||
return 'tickets/{org}/{ev}/{code}-{prov}-{secret}.dat'.format(
|
||||
org=instance.order.event.organizer.slug,
|
||||
ev=instance.order.event.slug,
|
||||
prov=instance.provider,
|
||||
code=instance.order.code,
|
||||
secret=secret
|
||||
)
|
||||
|
||||
@@ -581,6 +597,16 @@ class CachedTicket(models.Model):
|
||||
type = models.CharField(max_length=255)
|
||||
extension = models.CharField(max_length=255)
|
||||
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class CachedCombinedTicket(models.Model):
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE)
|
||||
provider = models.CharField(max_length=255)
|
||||
type = models.CharField(max_length=255)
|
||||
extension = models.CharField(max_length=255)
|
||||
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedTicket)
|
||||
|
||||
@@ -21,6 +21,15 @@ from pretix.base.signals import register_payment_providers
|
||||
from pretix.presale.views import get_cart_total
|
||||
|
||||
|
||||
class PaymentProviderForm(Form):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
for k, v in self.fields.items():
|
||||
val = cleaned_data.get(k)
|
||||
if v._required and not val:
|
||||
self.add_error(k, _('This field is required.'))
|
||||
|
||||
|
||||
class BasePaymentProvider:
|
||||
"""
|
||||
This is the base class for all payment providers.
|
||||
@@ -187,8 +196,12 @@ class BasePaymentProvider:
|
||||
process. The default implementation constructs the form using
|
||||
:py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
|
||||
and all fields and fills the form with data form the user's session.
|
||||
|
||||
If you overwrite this, we strongly suggest that you inherit from
|
||||
``PaymentProviderForm`` (from this module) that handles some nasty issues about
|
||||
required fields for you.
|
||||
"""
|
||||
form = Form(
|
||||
form = PaymentProviderForm(
|
||||
data=(request.POST if request.method == 'POST' else None),
|
||||
prefix='payment_%s' % self.identifier,
|
||||
initial={
|
||||
@@ -198,6 +211,12 @@ class BasePaymentProvider:
|
||||
}
|
||||
)
|
||||
form.fields = self.payment_form_fields
|
||||
|
||||
for k, v in form.fields.items():
|
||||
v._required = v.required
|
||||
v.required = False
|
||||
v.widget.is_required = False
|
||||
|
||||
return form
|
||||
|
||||
def _is_still_available(self, now_dt=None):
|
||||
|
||||
@@ -3,17 +3,17 @@ import tempfile
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from locale import format as lformat
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.platypus import (
|
||||
@@ -252,6 +252,13 @@ def _invoice_generate_german(invoice, f):
|
||||
textobject.textLine(date_format(invoice.order.datetime, "DATE_FORMAT"))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
if invoice.event.settings.invoice_logo_image:
|
||||
logo_file = invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
||||
canvas.drawImage(ImageReader(logo_file),
|
||||
95 * mm, (297 - 38) * mm,
|
||||
width=25 * mm, height=25 * mm,
|
||||
preserveAspectRatio=True, anchor='n')
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(_('Event').upper())
|
||||
@@ -306,6 +313,7 @@ def _invoice_generate_german(invoice, f):
|
||||
|
||||
tstyledata = [
|
||||
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
|
||||
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
|
||||
('LEFTPADDING', (0, 0), (0, -1), 0),
|
||||
@@ -320,16 +328,16 @@ def _invoice_generate_german(invoice, f):
|
||||
total = Decimal('0.00')
|
||||
for line in invoice.lines.all():
|
||||
tdata.append((
|
||||
line.description,
|
||||
lformat("%.2f", line.tax_rate) + " %",
|
||||
lformat("%.2f", line.net_value) + " " + invoice.event.currency,
|
||||
lformat("%.2f", line.gross_value) + " " + invoice.event.currency,
|
||||
Paragraph(line.description, styles['Normal']),
|
||||
localize(line.tax_rate) + " %",
|
||||
localize(line.net_value) + " " + invoice.event.currency,
|
||||
localize(line.gross_value) + " " + invoice.event.currency,
|
||||
))
|
||||
taxvalue_map[line.tax_rate] += line.tax_value
|
||||
grossvalue_map[line.tax_rate] += line.gross_value
|
||||
total += line.gross_value
|
||||
|
||||
tdata.append([pgettext('invoice', 'Invoice total'), '', '', lformat("%.2f", total) + " " + invoice.event.currency])
|
||||
tdata.append([pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + invoice.event.currency])
|
||||
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=1)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
@@ -361,10 +369,10 @@ def _invoice_generate_german(invoice, f):
|
||||
tax = taxvalue_map[rate]
|
||||
tdata.append((
|
||||
'',
|
||||
lformat("%.2f", rate) + " %",
|
||||
lformat("%.2f", (gross - tax)) + " " + invoice.event.currency,
|
||||
lformat("%.2f", gross) + " " + invoice.event.currency,
|
||||
lformat("%.2f", tax) + " " + invoice.event.currency,
|
||||
localize(rate) + " %",
|
||||
localize((gross - tax)) + " " + invoice.event.currency,
|
||||
localize(gross) + " " + invoice.event.currency,
|
||||
localize(tax) + " " + invoice.event.currency,
|
||||
))
|
||||
|
||||
if len(tdata) > 2:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import bleach
|
||||
import cssutils
|
||||
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.i18n import LazyI18nString, language
|
||||
from pretix.base.models import Event, Order
|
||||
from pretix.base.models import Event, InvoiceAddress, Order
|
||||
from pretix.celery_app import app
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
@@ -30,7 +30,7 @@ class SendMailException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def mail(email: str, subject: str, template: str,
|
||||
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
|
||||
order: Order=None, headers: dict=None):
|
||||
"""
|
||||
@@ -43,7 +43,7 @@ def mail(email: str, subject: str, template: str,
|
||||
|
||||
:param template: The filename of a template to be used. It will be rendered with the locale given in the locale
|
||||
argument and the context given in the next argument. Alternatively, you can pass a LazyI18nString and
|
||||
``context`` will be used as the argument to a Python ``.format()`` call on the template.
|
||||
``context`` will be used as the argument to a Python ``.format_map()`` call on the template.
|
||||
|
||||
:param context: The context for rendering the template (see ``template`` parameter)
|
||||
|
||||
@@ -63,7 +63,20 @@ def mail(email: str, subject: str, template: str,
|
||||
if email == INVALID_ADDRESS:
|
||||
return
|
||||
|
||||
headers = headers or {}
|
||||
|
||||
with language(locale):
|
||||
if isinstance(context, dict) and order:
|
||||
try:
|
||||
context.update({
|
||||
'invoice_name': order.invoice_address.name,
|
||||
'invoice_company': order.invoice_address.company
|
||||
})
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
context.update({
|
||||
'invoice_name': '',
|
||||
'invoice_company': ''
|
||||
})
|
||||
if isinstance(template, LazyI18nString):
|
||||
body = str(template)
|
||||
if context:
|
||||
@@ -92,6 +105,9 @@ def mail(email: str, subject: str, template: str,
|
||||
htmlctx['event'] = event
|
||||
htmlctx['color'] = event.settings.primary_color
|
||||
|
||||
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail:
|
||||
headers['Reply-To'] = event.settings.contact_mail
|
||||
|
||||
prefix = event.settings.get('mail_prefix')
|
||||
if prefix:
|
||||
subject = "[%s] %s" % (prefix, subject)
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import List, Optional
|
||||
|
||||
import pytz
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
from django.dispatch import receiver
|
||||
@@ -63,7 +64,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
|
||||
force: bool=False, send_mail: bool=True, user: User=None) -> Order:
|
||||
force: bool=False, send_mail: bool=True, user: User=None, mail_text='') -> Order:
|
||||
"""
|
||||
Marks an order as paid. This sets the payment provider, info and date and returns
|
||||
the order object.
|
||||
@@ -81,8 +82,13 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
|
||||
:type send_mail: boolean
|
||||
:param user: The user that performed the change
|
||||
:param mail_text: Additional text to be included in the email
|
||||
:type mail_text: str
|
||||
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
|
||||
"""
|
||||
if order.status == Order.STATUS_PAID:
|
||||
return order
|
||||
|
||||
with order.event.lock() as now_dt:
|
||||
can_be_paid = order._can_be_paid()
|
||||
if not force and can_be_paid is not True:
|
||||
@@ -98,14 +104,24 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
order.log_action('pretix.event.order.paid', {
|
||||
'provider': provider,
|
||||
'info': info,
|
||||
'date': date,
|
||||
'date': date or now_dt,
|
||||
'manual': manual,
|
||||
'force': force
|
||||
}, user=user)
|
||||
order_paid.send(order.event, order=order)
|
||||
|
||||
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
|
||||
if not order.invoices.exists():
|
||||
generate_invoice(order)
|
||||
|
||||
if send_mail:
|
||||
with language(order.locale):
|
||||
try:
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
mail(
|
||||
order.email, _('Payment received for your order: %(code)s') % {'code': order.code},
|
||||
order.event.settings.mail_text_order_paid,
|
||||
@@ -115,7 +131,10 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'downloads': order.event.settings.get('ticket_download', as_type=bool)
|
||||
'downloads': order.event.settings.get('ticket_download', as_type=bool),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
'payment_info': mail_text
|
||||
},
|
||||
order.event, locale=order.locale
|
||||
)
|
||||
@@ -171,7 +190,7 @@ def _cancel_order(order, user=None):
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
|
||||
|
||||
return order
|
||||
return order.pk
|
||||
|
||||
|
||||
class OrderError(LazyLocaleException):
|
||||
@@ -270,7 +289,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
raise OrderError(err)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
|
||||
meta_info: dict=None):
|
||||
@@ -299,33 +317,35 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
if last_date < expires:
|
||||
expires = last_date
|
||||
|
||||
order = Order.objects.create(
|
||||
status=Order.STATUS_PENDING,
|
||||
event=event,
|
||||
email=email,
|
||||
datetime=now_dt,
|
||||
expires=expires,
|
||||
locale=locale,
|
||||
total=total,
|
||||
payment_fee=payment_fee,
|
||||
payment_provider=payment_provider.identifier,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
)
|
||||
OrderPosition.transform_cart_positions(positions, order)
|
||||
with transaction.atomic():
|
||||
order = Order.objects.create(
|
||||
status=Order.STATUS_PENDING,
|
||||
event=event,
|
||||
email=email,
|
||||
datetime=now_dt,
|
||||
expires=expires,
|
||||
locale=locale,
|
||||
total=total,
|
||||
payment_fee=payment_fee,
|
||||
payment_provider=payment_provider.identifier,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
)
|
||||
OrderPosition.transform_cart_positions(positions, order)
|
||||
|
||||
if address is not None:
|
||||
try:
|
||||
addr = InvoiceAddress.objects.get(
|
||||
pk=address
|
||||
)
|
||||
if addr.order is not None:
|
||||
addr.pk = None
|
||||
addr.order = order
|
||||
addr.save()
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
if address is not None:
|
||||
try:
|
||||
addr = InvoiceAddress.objects.get(
|
||||
pk=address
|
||||
)
|
||||
if addr.order is not None:
|
||||
addr.pk = None
|
||||
addr.order = order
|
||||
addr.save()
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
order.log_action('pretix.event.order.placed')
|
||||
|
||||
order.log_action('pretix.event.order.placed')
|
||||
order_placed.send(event, order=order)
|
||||
return order
|
||||
|
||||
@@ -363,6 +383,14 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
mailtext = event.settings.mail_text_order_free
|
||||
else:
|
||||
mailtext = event.settings.mail_text_order_placed
|
||||
|
||||
try:
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
|
||||
mail(
|
||||
order.email, _('Your order: %(code)s') % {'code': order.code},
|
||||
mailtext,
|
||||
@@ -375,7 +403,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'paymentinfo': str(pprov.order_pending_mail_render(order))
|
||||
'paymentinfo': str(pprov.order_pending_mail_render(order)),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
},
|
||||
event, locale=order.locale
|
||||
)
|
||||
@@ -404,29 +434,39 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
today = now().replace(hour=0, minute=0, second=0)
|
||||
|
||||
for o in Order.objects.filter(expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING).select_related('event'):
|
||||
settings = eventcache.get(o.event.pk, None)
|
||||
if settings is None:
|
||||
settings = o.event.settings
|
||||
eventcache[o.event.pk] = settings
|
||||
eventsettings = eventcache.get(o.event.pk, None)
|
||||
if eventsettings is None:
|
||||
eventsettings = o.event.settings
|
||||
eventcache[o.event.pk] = eventsettings
|
||||
|
||||
days = settings.get('mail_days_order_expire_warning', as_type=int)
|
||||
days = eventsettings.get('mail_days_order_expire_warning', as_type=int)
|
||||
tz = pytz.timezone(eventsettings.get('timezone', settings.TIME_ZONE))
|
||||
if days and (o.expires - today).days <= days:
|
||||
o.expiry_reminder_sent = True
|
||||
o.save()
|
||||
try:
|
||||
mail(
|
||||
o.email, _('Your order is about to expire: %(code)s') % {'code': o.code},
|
||||
settings.mail_text_order_expire_warning,
|
||||
{
|
||||
'event': o.event.name,
|
||||
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
||||
'order': o.code,
|
||||
'secret': o.secret
|
||||
}),
|
||||
'expire_date': date_format(o.expires, 'SHORT_DATE_FORMAT')
|
||||
},
|
||||
o.event, locale=o.locale
|
||||
)
|
||||
invoice_name = o.invoice_address.name
|
||||
invoice_company = o.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
try:
|
||||
with language(o.locale):
|
||||
mail(
|
||||
o.email, _('Your order is about to expire: %(code)s') % {'code': o.code},
|
||||
eventsettings.mail_text_order_expire_warning,
|
||||
{
|
||||
'event': o.event.name,
|
||||
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
||||
'order': o.code,
|
||||
'secret': o.secret
|
||||
}),
|
||||
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
},
|
||||
o.event, locale=o.locale
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Reminder email could not be sent')
|
||||
else:
|
||||
@@ -557,6 +597,12 @@ class OrderChangeManager:
|
||||
|
||||
def _notify_user(self):
|
||||
with language(self.order.locale):
|
||||
try:
|
||||
invoice_name = self.order.invoice_address.name
|
||||
invoice_company = self.order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
mail(
|
||||
self.order.email, _('Your order has been changed: %(code)s') % {'code': self.order.code},
|
||||
self.order.event.settings.mail_text_order_changed,
|
||||
@@ -566,6 +612,8 @@ class OrderChangeManager:
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
},
|
||||
self.order.event, locale=self.order.locale
|
||||
)
|
||||
|
||||
@@ -5,7 +5,9 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedTicket, Event, Order, OrderPosition
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.services.async import ProfiledTask
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.celery_app import app
|
||||
@@ -37,6 +39,31 @@ def generate(order_position: str, provider: str):
|
||||
ct.file.save(filename, ContentFile(data))
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def generate_order(order: int, provider: str):
|
||||
order = Order.objects.select_related('event').get(id=order)
|
||||
try:
|
||||
ct = CachedCombinedTicket.objects.get(order=order, provider=provider)
|
||||
except CachedCombinedTicket.MultipleObjectsReturned:
|
||||
CachedCombinedTicket.objects.filter(order=order, provider=provider).delete()
|
||||
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension='',
|
||||
type='', file=None)
|
||||
except CachedCombinedTicket.DoesNotExist:
|
||||
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension='',
|
||||
type='', file=None)
|
||||
|
||||
with language(order.locale):
|
||||
responses = register_ticket_outputs.send(order.event)
|
||||
for receiver, response in responses:
|
||||
prov = response(order.event)
|
||||
if prov.identifier == provider:
|
||||
filename, ct.type, data = prov.generate_order(order)
|
||||
path, ext = os.path.splitext(filename)
|
||||
ct.extension = ext
|
||||
ct.save()
|
||||
ct.file.save(filename, ContentFile(data))
|
||||
|
||||
|
||||
class DummyRollbackException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'show_variations_expanded': {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'ticket_download': {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
@@ -231,6 +235,8 @@ Your {event} team"""))
|
||||
|
||||
we successfully received your payment for {event}. Thank you!
|
||||
|
||||
{payment_info}
|
||||
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
@@ -299,6 +305,10 @@ Your {event} team"""))
|
||||
'default': None,
|
||||
'type': File
|
||||
},
|
||||
'invoice_logo_image': {
|
||||
'default': None,
|
||||
'type': File
|
||||
},
|
||||
'frontpage_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString
|
||||
@@ -359,7 +369,7 @@ class SettingsProxy:
|
||||
settings[key] = self.get(key)
|
||||
return settings
|
||||
|
||||
def _unserialize(self, value: str, as_type: type) -> Any:
|
||||
def _unserialize(self, value: str, as_type: type, binary_file=False) -> Any:
|
||||
if as_type is None and value is not None and value.startswith('file://'):
|
||||
as_type = File
|
||||
|
||||
@@ -375,7 +385,7 @@ class SettingsProxy:
|
||||
return value == 'True'
|
||||
elif as_type == File:
|
||||
try:
|
||||
fi = default_storage.open(value[7:], 'r')
|
||||
fi = default_storage.open(value[7:], 'rb' if binary_file else 'r')
|
||||
fi.url = default_storage.url(value[7:])
|
||||
return fi
|
||||
except OSError:
|
||||
@@ -414,7 +424,7 @@ class SettingsProxy:
|
||||
|
||||
raise TypeError('Unable to serialize %s into a setting.' % str(type(value)))
|
||||
|
||||
def get(self, key: str, default=None, as_type: type=None):
|
||||
def get(self, key: str, default=None, as_type: type=None, binary_file=False):
|
||||
"""
|
||||
Get a setting specified by key ``key``. Normally, settings are strings, but
|
||||
if you put non-strings into the settings object, you can request unserialization
|
||||
@@ -440,7 +450,7 @@ class SettingsProxy:
|
||||
if value is None and default is not None:
|
||||
value = default
|
||||
|
||||
return self._unserialize(value, as_type)
|
||||
return self._unserialize(value, as_type, binary_file=binary_file)
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.get(key)
|
||||
|
||||
@@ -94,6 +94,18 @@ subclass of pretix.base.exporter.BaseExporter
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
validate_cart = EventPluginSignal(
|
||||
providing_args=["positions"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out before the user starts checkout. It includes an iterable
|
||||
with the current CartPosition objects.
|
||||
The response of receivers will be ignored, but you can raise a CartError with an
|
||||
appropriate exception message.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_placed = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import os
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from typing import Tuple
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, OrderPosition
|
||||
from pretix.base.models import Event, Order, OrderPosition
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
|
||||
|
||||
@@ -29,7 +32,7 @@ class BaseTicketOutput:
|
||||
"""
|
||||
return self.settings.get('_enabled', as_type=bool)
|
||||
|
||||
def generate(self, order: OrderPosition) -> Tuple[str, str, str]:
|
||||
def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
|
||||
"""
|
||||
This method should generate the download file and return a tuple consisting of a
|
||||
filename, a file type and file content. The extension will be taken from the filename
|
||||
@@ -37,6 +40,29 @@ class BaseTicketOutput:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_order(self, order: Order) -> Tuple[str, str, str]:
|
||||
"""
|
||||
This method is the same as order() but should not generate one file per order position
|
||||
but instead one file for the full order.
|
||||
|
||||
This method is optional to implement. If you don't implement it, the default
|
||||
implementation will offer a zip file of the generate() results for the order positions.
|
||||
|
||||
This method should generate a download file and return a tuple consisting of a
|
||||
filename, a file type and file content. The extension will be taken from the filename
|
||||
which is otherwise ignored.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for pos in order.positions.all():
|
||||
fname, __, content = self.generate(pos)
|
||||
zipf.writestr('{}-{}{}'.format(
|
||||
order.code, pos.positionid, os.path.splitext(fname)[1]
|
||||
), content)
|
||||
|
||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||
return '{}-{}.zip'.format(order.code, self.identifier), 'application/zip', zipf.read()
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import etag
|
||||
from django.views.i18n import (
|
||||
get_javascript_catalog, render_javascript_catalog, to_locale,
|
||||
)
|
||||
from django.views.i18n import get_javascript_catalog, render_javascript_catalog
|
||||
|
||||
# Yes, we want to regenerate this every time the module has been imported to
|
||||
# refresh the cache at least at every code deployment
|
||||
@@ -21,5 +19,5 @@ js_info_dict = {
|
||||
@cache_page(3600, key_prefix='js18n-%s' % import_date)
|
||||
def js_catalog(request, lang):
|
||||
packages = ['pretix']
|
||||
catalog, plural = get_javascript_catalog(to_locale(lang), 'djangojs', packages)
|
||||
catalog, plural = get_javascript_catalog(lang, 'djangojs', packages)
|
||||
return render_javascript_catalog(catalog, plural)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
|
||||
|
||||
from .signals import html_head, nav_event
|
||||
from .signals import html_head, nav_event, nav_topbar
|
||||
from .utils.i18n import get_javascript_format, get_moment_locale
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ def contextprocessor(request):
|
||||
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
|
||||
ctx['nav_event'] = _nav_event
|
||||
|
||||
_nav_topbar = []
|
||||
for receiver, response in nav_topbar.send(request, request=request):
|
||||
_nav_topbar += response
|
||||
ctx['nav_topbar'] = _nav_topbar
|
||||
|
||||
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
|
||||
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
|
||||
ctx['js_locale'] = get_moment_locale()
|
||||
|
||||
@@ -56,7 +56,8 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
'date_from',
|
||||
'date_to',
|
||||
'presale_start',
|
||||
'presale_end'
|
||||
'presale_end',
|
||||
'location',
|
||||
]
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
@@ -107,7 +108,7 @@ class EventWizardCopyForm(forms.Form):
|
||||
),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=_('Do not copy'),
|
||||
required=True
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
@@ -131,6 +132,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
'is_public',
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
'location',
|
||||
]
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
@@ -329,7 +331,8 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
('False', _('No')),
|
||||
('admin', _('Manually in admin panel')),
|
||||
('user', _('Automatically on user request')),
|
||||
('True', _('Automatically for all created orders'))
|
||||
('True', _('Automatically for all created orders')),
|
||||
('paid', _('Automatically on payment')),
|
||||
)
|
||||
)
|
||||
invoice_address_from = forms.CharField(
|
||||
@@ -361,6 +364,12 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
label=_("Invoice language"),
|
||||
choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES,
|
||||
)
|
||||
invoice_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||
)
|
||||
|
||||
|
||||
class MailSettingsForm(SettingsForm):
|
||||
@@ -378,31 +387,32 @@ class MailSettingsForm(SettingsForm):
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}")
|
||||
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}, "
|
||||
"{invoice_name}, {invoice_company}")
|
||||
)
|
||||
mail_text_order_paid = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}")
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}")
|
||||
)
|
||||
mail_text_order_free = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}")
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
|
||||
)
|
||||
mail_text_order_changed = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}")
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
|
||||
)
|
||||
mail_text_resend_link = I18nFormField(
|
||||
label=_("Text (sent by admin)"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}")
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
|
||||
)
|
||||
mail_text_resend_all_links = I18nFormField(
|
||||
label=_("Text (requested by user)"),
|
||||
@@ -421,7 +431,7 @@ class MailSettingsForm(SettingsForm):
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {expire_date}")
|
||||
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}")
|
||||
)
|
||||
smtp_use_custom = forms.BooleanField(
|
||||
label=_("Use custom SMTP server"),
|
||||
@@ -491,6 +501,10 @@ class DisplaySettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=I18nTextarea
|
||||
)
|
||||
show_variations_expanded = forms.BooleanField(
|
||||
label=_("Show variations of a product expanded by default"),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class TicketSettingsForm(SettingsForm):
|
||||
|
||||
@@ -21,10 +21,10 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
if logentry.action_type == 'pretix.event.order.changed.item':
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(event.itemvariations.get(pk=data['old_variation']))
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
|
||||
new_item = str(event.items.get(pk=data['new_item']))
|
||||
if data['new_variation']:
|
||||
new_item += ' - ' + str(event.itemvariations.get(pk=data['new_variation']))
|
||||
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
|
||||
return text + ' ' + _('Position #{posid}: {old_item} ({old_price} {currency}) changed '
|
||||
'to {new_item} ({new_price} {currency}).').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
|
||||
@@ -61,33 +61,53 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
request.user.events_cache = request.user.events.order_by(
|
||||
events = Event.objects.all() if request.user.is_superuser else request.user.events
|
||||
request.user.events_cache = events.order_by(
|
||||
"organizer", "date_from").prefetch_related("organizer")
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
try:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
permitted__id__exact=request.user.id,
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
if request.user.is_superuser:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
else:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
permitted__id__exact=request.user.id,
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
request.organizer = request.event.organizer
|
||||
except IndexError:
|
||||
raise Http404(_("The selected event was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
elif 'organizer' in url.kwargs:
|
||||
try:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
permitted__id__exact=request.user.id,
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
if request.user.is_superuser:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
else:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
permitted__id__exact=request.user.id,
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
except IndexError:
|
||||
raise Http404(_("The selected organizer was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
|
||||
@@ -14,6 +14,8 @@ def event_permission_required(permission):
|
||||
if not request.user.is_authenticated: # NOQA
|
||||
# just a double check, should not ever happen
|
||||
raise PermissionDenied()
|
||||
if request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
try:
|
||||
perm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
@@ -28,7 +30,7 @@ def event_permission_required(permission):
|
||||
allowed = getattr(perm, permission)
|
||||
except AttributeError:
|
||||
pass
|
||||
if allowed:
|
||||
if allowed or request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return wrapper
|
||||
@@ -58,6 +60,8 @@ def organizer_permission_required(permission):
|
||||
if not request.user.is_authenticated: # NOQA
|
||||
# just a double check, should not ever happen
|
||||
raise PermissionDenied()
|
||||
if request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
try:
|
||||
perm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
|
||||
@@ -28,7 +28,7 @@ nav_event = EventPluginSignal(
|
||||
)
|
||||
"""
|
||||
This signal allows you to add additional views to the admin panel
|
||||
navigation. You will get the request as a keyword argument ``return``.
|
||||
navigation. You will get the request as a keyword argument ``request``.
|
||||
Receivers are expected to return a list of dictionaries. The dictionaries
|
||||
should contain at least the keys ``label`` and ``url``. You can also return
|
||||
a fontawesome icon name with the key ``icon``, it will be respected depending
|
||||
@@ -42,6 +42,24 @@ in pretix.
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
nav_topbar = Signal(
|
||||
providing_args=["request"]
|
||||
)
|
||||
"""
|
||||
This signal allows you to add additional views to the top navigation bar.
|
||||
You will get the request as a keyword argument ``return``.
|
||||
Receivers are expected to return a list of dictionaries. The dictionaries
|
||||
should contain at least the keys ``label`` and ``url``. You can also return
|
||||
a fontawesome icon name with the key ``icon``, it will be respected depending
|
||||
on the type of navigation. If set, on desktops only the ``icon`` will be shown.
|
||||
|
||||
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
|
||||
in pretix.
|
||||
|
||||
This is no ``EventPluginSignal``, so you do not get the event in the ``sender`` argument
|
||||
and you may get the signal regardless of whether your plugin is active.
|
||||
"""
|
||||
|
||||
event_dashboard_widgets = EventPluginSignal(
|
||||
providing_args=[]
|
||||
)
|
||||
@@ -104,3 +122,15 @@ quota as argument in the ``quota`` keyword argument.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
organizer_edit_tabs = Signal(
|
||||
providing_args=['organizer', 'request']
|
||||
)
|
||||
"""
|
||||
This signal is sent out to include tabs on the detail page of an organizer. Receivers
|
||||
should return a tuple with the first item being the tab title and the second item
|
||||
being the content as HTML. The receivers get the ``organizer`` and the ``request`` as
|
||||
keyword arguments.
|
||||
|
||||
This is a regular django signal (no pretix event signal).
|
||||
"""
|
||||
|
||||
@@ -74,6 +74,35 @@
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-top-links navbar-right">
|
||||
{% for nav in nav_topbar %}
|
||||
<li {% if nav.children %}class="dropdown"{% endif %}>
|
||||
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
|
||||
{% if nav.children %}class="dropdown-toggle" data-toggle="dropdown"{% endif %}>
|
||||
{% if nav.icon %}
|
||||
<span class="fa fa-{{ nav.icon }}"></span>
|
||||
<span class="visible-xs-inline">{{ nav.label }}</span>
|
||||
{% else %}
|
||||
{{ nav.label }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if nav.children %}
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% for item in nav.children %}
|
||||
<li>
|
||||
<a href="{{ item.url }}"
|
||||
{% if item.active %}class="active"{% endif %}>
|
||||
{% if item.icon %}
|
||||
<span class="fa fa-{{ item.icon }}"></span>
|
||||
{% endif %}
|
||||
{{ item.label|safe }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<li>
|
||||
<a href="{% url 'control:user.settings' %}">
|
||||
<i class="fa fa-user"></i> {{ request.user.get_full_name }}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{% bootstrap_field form.primary_color layout="horizontal" %}
|
||||
{% bootstrap_field form.logo_image layout="horizontal" %}
|
||||
{% bootstrap_field form.frontpage_text layout="horizontal" %}
|
||||
{% bootstrap_field form.show_variations_expanded layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -51,10 +51,14 @@
|
||||
<div class="row dashboard">
|
||||
{% for w in widgets %}
|
||||
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
|
||||
{% if w.url %}
|
||||
{% if w.url %}{# backwards compatibility #}
|
||||
<a href="{{ w.url }}" class="widget">
|
||||
{{ w.content|safe }}
|
||||
</a>
|
||||
{% elif w.link %}
|
||||
<a href="{{ w.link }}" class="widget">
|
||||
{{ w.content|safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="widget">
|
||||
{{ w.content|safe }}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
{% bootstrap_field form.invoice_introductory_text layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_additional_text layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_footer_text layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_logo_image layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue }}</li>
|
||||
<li>{{ issue|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<div class="col-sm-4">
|
||||
{% if plugin.app.compatibility_errors %}
|
||||
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
|
||||
{% elif plugin.restricted and not request.user.is_superuser %}
|
||||
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
|
||||
{% elif plugin.module in plugins_active %}
|
||||
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
|
||||
{% else %}
|
||||
@@ -42,6 +44,11 @@
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
<p>{{ plugin.description }}</p>
|
||||
{% if plugin.restricted and not request.user.is_superuser %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if plugin.app.compatibility_errors %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "This plugin cannot be enabled for the following reasons:" %}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.location layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
{% bootstrap_field form.is_public layout="horizontal" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.location layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -8,23 +8,25 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
<div class="col-xs-12{% if category %} col-lg-10{% endif %}">
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.description layout="horizontal" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Category history" %}
|
||||
</h3>
|
||||
{% if category %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Category history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=category %}
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=category %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -6,15 +6,32 @@
|
||||
<h1>
|
||||
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
|
||||
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
|
||||
class="btn btn-default">
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="{% if request.orgaperm.can_change_permissions %}col-lg-6{% endif %} col-xs-12">
|
||||
<fieldset>
|
||||
<legend>{% trans "Events" %}</legend>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="#tab-events" data-toggle="tab">{% trans "Events" %}</a>
|
||||
</li>
|
||||
{% if request.orgaperm.can_change_permissions %}
|
||||
<li>
|
||||
<a href="#tab-permissions" data-toggle="tab">{% trans "Team" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for title, content in tabs %}
|
||||
<li>
|
||||
<a href="#tab-{{ forloop.counter }}" data-toggle="tab">
|
||||
{{ title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content organizer-tabs">
|
||||
<div class="tab-pane active" id="tab-events">
|
||||
<div class="tab-inner">
|
||||
{% if events|length == 0 %}
|
||||
<p>
|
||||
<em>{% trans "You currently do not have access to any events." %}</em>
|
||||
@@ -31,7 +48,8 @@
|
||||
{% for e in events %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
|
||||
<strong><a
|
||||
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
|
||||
</td>
|
||||
<td>{{ e.get_date_from_display }}</td>
|
||||
</tr>
|
||||
@@ -43,24 +61,25 @@
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new event" %}
|
||||
</a>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.orgaperm.can_change_permissions %}
|
||||
<div class="col-lg-6 col-xs-12">
|
||||
<form action="" method="post" class="form-horizontal form-permissions">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Team" %}</legend>
|
||||
<div class="tab-pane" id="tab-permissions">
|
||||
<div class="tab-inner">
|
||||
<form action="" method="post" class="form-horizontal form-permissions">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can use the following list to control who can create new events in the name of this
|
||||
organizer and who can add more people to this list. This does <strong>not</strong>
|
||||
You can use the following list to control who can create new events in the name of
|
||||
this organizer and who can add more people to this list. This does <strong>not</strong>
|
||||
control who has access to a particular event. You can control the access to an
|
||||
event in the "Permissions" section of the event's settings. A user does not need to be
|
||||
on the list here to get access to an event.
|
||||
event in the "Permissions" section of the event's settings. A user does not need to
|
||||
be on the list here to get access to an event.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Everyone on this list can control the organizer settings on this page." %}
|
||||
</p>
|
||||
|
||||
{% bootstrap_formset_errors formset %}
|
||||
{{ formset.management_form }}
|
||||
@@ -84,7 +103,7 @@
|
||||
{% else %}
|
||||
{{ form.instance.invite_email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.can_create_events }}</td>
|
||||
@@ -98,9 +117,9 @@
|
||||
<td colspan="9">
|
||||
<strong>{% trans "Adding a new user" %}</strong><br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they already have a
|
||||
pretix account, they will immediately be added to the team. Otherwise, they will
|
||||
be sent an email with an invitation.
|
||||
To add a new user, you can enter their email address here. If they
|
||||
already have a pretix account, they will immediately be added to the team.
|
||||
Otherwise, they will be sent an email with an invitation.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
|
||||
@@ -125,9 +144,16 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for title, content in tabs %}
|
||||
<div class="tab-pane" id="tab-{{ forloop.counter }}">
|
||||
<div class="tab-inner">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="form-group">
|
||||
<div class="col-md-7 col-sm-12 col-md-offset-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-xs"
|
||||
<input type="number" class="form-control input-xs"
|
||||
id="voucher-bulk-codes-num"
|
||||
placeholder="{% trans "Number" %}">
|
||||
<div class="input-group-btn">
|
||||
|
||||
@@ -132,6 +132,9 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
|
||||
if key.startswith("plugin:"):
|
||||
module = key.split(":")[1]
|
||||
if value == "enable" and module in plugins_available:
|
||||
if getattr(plugins_available[module], 'restricted', False):
|
||||
if not request.user.is_superuser:
|
||||
continue
|
||||
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
|
||||
data={'plugin': module})
|
||||
if module not in plugins_active:
|
||||
|
||||
@@ -20,11 +20,16 @@ class EventList(ListView):
|
||||
template_name = 'pretixcontrol/events/index.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return Event.objects.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
).select_related("organizer").prefetch_related(
|
||||
"setting_objects", "organizer__setting_objects"
|
||||
)
|
||||
if self.request.user.is_superuser:
|
||||
return Event.objects.all().select_related("organizer").prefetch_related(
|
||||
"setting_objects", "organizer__setting_objects"
|
||||
)
|
||||
else:
|
||||
return Event.objects.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
).select_related("organizer").prefetch_related(
|
||||
"setting_objects", "organizer__setting_objects"
|
||||
)
|
||||
|
||||
|
||||
def condition_copy(wizard):
|
||||
|
||||
@@ -12,8 +12,8 @@ from django.views.generic import DetailView, ListView, TemplateView, View
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, CachedTicket, Invoice, Item, ItemVariation, Order, Quota,
|
||||
generate_position_secret, generate_secret,
|
||||
CachedFile, CachedTicket, Invoice, InvoiceAddress, Item, ItemVariation,
|
||||
Order, Quota, generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.services.export import export
|
||||
from pretix.base.services.invoices import (
|
||||
@@ -323,6 +323,12 @@ class OrderResendLink(OrderView):
|
||||
def post(self, *args, **kwargs):
|
||||
with language(self.order.locale):
|
||||
try:
|
||||
try:
|
||||
invoice_name = self.order.invoice_address.name
|
||||
invoice_company = self.order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
mail(
|
||||
self.order.email, _('Your order: %(code)s') % {'code': self.order.code},
|
||||
self.order.event.settings.mail_text_resend_link,
|
||||
@@ -332,6 +338,8 @@ class OrderResendLink(OrderView):
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
},
|
||||
self.order.event, locale=self.order.locale
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ from pretix.base.models import Organizer, OrganizerPermission, User
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
|
||||
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
||||
from pretix.control.signals import organizer_edit_tabs
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
|
||||
@@ -60,20 +61,38 @@ class OrganizerDetail(OrganizerPermissionRequiredMixin, DetailView):
|
||||
form=OrganizerPermissionForm,
|
||||
can_delete=True, can_order=False, extra=0
|
||||
)
|
||||
return fs(data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix="formset",
|
||||
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer))
|
||||
return fs(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and 'id_formset-TOTAL_FORMS' in self.request.POST
|
||||
else None
|
||||
),
|
||||
prefix="formset",
|
||||
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return OrganizerPermissionCreateForm(data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix="add")
|
||||
return OrganizerPermissionCreateForm(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and 'id_formset-TOTAL_FORMS' in self.request.POST
|
||||
else None
|
||||
),
|
||||
prefix="add"
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['add_form'] = self.add_form
|
||||
ctx['events'] = self.request.organizer.events.all()
|
||||
ctx['tabs'] = []
|
||||
|
||||
for recv, retv in organizer_edit_tabs.send(sender=self.request.organizer, request=self.request,
|
||||
organizer=self.request.organizer):
|
||||
ctx['tabs'].append(retv)
|
||||
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
@@ -100,6 +119,9 @@ class OrganizerDetail(OrganizerPermissionRequiredMixin, DetailView):
|
||||
if not self.request.orgaperm.can_change_permissions:
|
||||
raise PermissionDenied(_("You have no permission to do this."))
|
||||
|
||||
if 'id_formset-TOTAL_FORMS' not in self.request.POST:
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
if self.formset.is_valid() and self.add_form.is_valid():
|
||||
if self.add_form.has_changed():
|
||||
logdata = {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-01-07 12:08+0000\n"
|
||||
"POT-Creation-Date: 2017-02-03 12:16+0000\n"
|
||||
"PO-Revision-Date: 2017-01-01 20:40+0100\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -92,7 +92,7 @@ msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
||||
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:53
|
||||
#: pretix/static/pretixpresale/js/ui/asynctask.js:103
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Wir verarbeiten deine Anfrage …"
|
||||
msgstr "Wir verarbeiten Ihre Anfrage …"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:54
|
||||
#: pretix/static/pretixpresale/js/ui/asynctask.js:104
|
||||
|
||||
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-07 12:08+0000\n"
|
||||
"PO-Revision-Date: 2017-01-01 20:41+0100\n"
|
||||
"POT-Creation-Date: 2017-02-03 12:16+0000\n"
|
||||
"PO-Revision-Date: 2017-01-18 09:42+0100\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
|
||||
@@ -2,36 +2,45 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
|
||||
{% if no_more_payments %}
|
||||
<div class="alert alert-danger">
|
||||
{% blocktrans trimmed with date=request.event.settings.payment_term_last|date:"SHORT_DATE_FORMAT" %}
|
||||
In the payment settings of your event, you set the {{ date }} as the last date of any payments.
|
||||
Therefore, you won't be able to mark any order as paid here.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{% blocktrans trimmed %}
|
||||
This page allows you to upload bank statement files to process incoming payments.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Currently, this feature supports <code>.csv</code> files and files in the MT940 format.
|
||||
{% endblocktrans %}</p>
|
||||
{% if job_running %}
|
||||
<div class="alert alert-info" data-job-waiting data-job-waiting-url="{% url "plugins:banktransfer:import.job" event=request.event.slug organizer=request.event.organizer.slug job=job_running.pk %}?ajax=1">
|
||||
<span class="fa fa-cog fa-spin"></span>
|
||||
{% trans "An import is currently being processed, please try again in a few minutes." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-inline">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file"/>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{% blocktrans trimmed %}
|
||||
This page allows you to upload bank statement files to process incoming payments.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Currently, this feature supports <code>.csv</code> files and files in the MT940 format.
|
||||
{% endblocktrans %}</p>
|
||||
{% if job_running %}
|
||||
<div class="alert alert-info" data-job-waiting data-job-waiting-url="{% url "plugins:banktransfer:import.job" event=request.event.slug organizer=request.event.organizer.slug job=job_running.pk %}?ajax=1">
|
||||
<span class="fa fa-cog fa-spin"></span>
|
||||
{% trans "An import is currently being processed, please try again in a few minutes." %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<button class="btn btn-primary pull-right" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start upload" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-inline">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file"/>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<button class="btn btn-primary pull-right" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start upload" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if transactions_unhandled|length > 0 or request.GET.search %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -2,7 +2,6 @@ import csv
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from locale import format as lformat
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -135,6 +134,8 @@ class ActionView(EventPermissionRequiredMixin, View):
|
||||
})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
from django.utils.formats import localize
|
||||
|
||||
query = request.GET.get('query', '')
|
||||
if len(query) < 2:
|
||||
return JsonResponse({'results': []})
|
||||
@@ -145,7 +146,7 @@ class ActionView(EventPermissionRequiredMixin, View):
|
||||
{
|
||||
'code': o.code,
|
||||
'status': o.get_status_display(),
|
||||
'total': lformat("%.2f", o.total) + ' ' + self.request.event.currency
|
||||
'total': localize(o.total) + ' ' + self.request.event.currency
|
||||
} for o in qs
|
||||
]
|
||||
})
|
||||
@@ -354,4 +355,8 @@ class ImportView(EventPermissionRequiredMixin, ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['job_running'] = self.job_running
|
||||
ctx['no_more_payments'] = False
|
||||
if self.request.event.settings.get('payment_term_last'):
|
||||
if now() > self.request.event.payment_term_last:
|
||||
ctx['no_more_payments'] = True
|
||||
return ctx
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.contrib import messages
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext as __, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Quota, RequiredAction
|
||||
from pretix.base.models import Order, Quota, RequiredAction
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
@@ -160,32 +160,34 @@ class Paypal(BasePaymentProvider):
|
||||
return self._execute_payment(payment, request, order)
|
||||
|
||||
def _execute_payment(self, payment, request, order):
|
||||
payment.replace([
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/transactions/0/item_list",
|
||||
"value": {
|
||||
"items": [
|
||||
{
|
||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
|
||||
"quantity": 1,
|
||||
"price": str(order.total),
|
||||
"currency": order.event.currency
|
||||
}
|
||||
]
|
||||
if payment.state == 'created':
|
||||
payment.replace([
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/transactions/0/item_list",
|
||||
"value": {
|
||||
"items": [
|
||||
{
|
||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
|
||||
"quantity": 1,
|
||||
"price": str(order.total),
|
||||
"currency": order.event.currency
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/transactions/0/description",
|
||||
"value": __('Order {order} for {event}').format(
|
||||
event=request.event.name,
|
||||
order=order.code
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/transactions/0/description",
|
||||
"value": __('Order {order} for {event}').format(
|
||||
event=request.event.name,
|
||||
order=order.code
|
||||
)
|
||||
}
|
||||
])
|
||||
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
||||
])
|
||||
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
||||
|
||||
order.refresh_from_db()
|
||||
if payment.state == 'pending':
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
|
||||
'payment completed.'))
|
||||
@@ -199,6 +201,10 @@ class Paypal(BasePaymentProvider):
|
||||
logger.error('Invalid state: %s' % str(payment))
|
||||
return
|
||||
|
||||
if order.status == Order.STATUS_PAID:
|
||||
logger.warning('PayPal success event even though order is already marked as paid')
|
||||
return
|
||||
|
||||
try:
|
||||
mark_order_paid(order, 'paypal', json.dumps(payment.to_dict()))
|
||||
except Quota.QuotaExceededException as e:
|
||||
|
||||
@@ -71,11 +71,14 @@ def webhook(request, *args, **kwargs):
|
||||
prov.init_api()
|
||||
|
||||
# We do not check the signature, we just use it as a trigger to look the charge up.
|
||||
if event_json['resource_type'] != 'sale':
|
||||
if event_json['resource_type'] not in ('sale', 'refund'):
|
||||
return HttpResponse("Not interested in this resource type", status=200)
|
||||
|
||||
try:
|
||||
sale = paypalrestsdk.Sale.find(event_json['resource']['id'])
|
||||
if event_json['resource_type'] == 'sale':
|
||||
sale = paypalrestsdk.Sale.find(event_json['resource']['id'])
|
||||
else:
|
||||
sale = paypalrestsdk.Sale.find(event_json['resource']['sale_id'])
|
||||
except:
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Sale not found', status=500)
|
||||
|
||||
@@ -20,7 +20,8 @@ class MailForm(forms.Form):
|
||||
self.fields['message'] = I18nFormField(
|
||||
widget=I18nTextarea, required=True,
|
||||
langcodes=event.settings.get('locales'),
|
||||
help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}")
|
||||
help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}, "
|
||||
"{invoice_name}, {invoice_company}")
|
||||
)
|
||||
choices = list(Order.STATUS_CHOICE)
|
||||
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
url(r'^webhook/$', webhook, name='webhook'),{% load i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
{% if order.status == "p" %}
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.ticketoutput import BaseTicketOutput
|
||||
from pretix.control.forms import ExtFileField
|
||||
|
||||
@@ -20,27 +21,11 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
verbose_name = _('PDF output')
|
||||
download_button_text = _('PDF')
|
||||
|
||||
def generate(self, op):
|
||||
def _draw_page(self, p, op, order):
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib import pagesizes, units
|
||||
from reportlab.lib import units
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics import renderPDF
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
|
||||
order = op.order
|
||||
|
||||
pagesize = self.settings.get('pagesize', default='A4')
|
||||
if hasattr(pagesizes, pagesize):
|
||||
pagesize = getattr(pagesizes, pagesize)
|
||||
else:
|
||||
pagesize = pagesizes.A4
|
||||
orientation = self.settings.get('orientation', default='portrait')
|
||||
if hasattr(pagesizes, orientation):
|
||||
pagesize = getattr(pagesizes, orientation)(pagesize)
|
||||
|
||||
buffer = BytesIO()
|
||||
p = canvas.Canvas(buffer, pagesize=pagesize)
|
||||
|
||||
event_s = self.settings.get('event_s', default=22, as_type=float)
|
||||
if event_s:
|
||||
@@ -102,8 +87,41 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
|
||||
p.showPage()
|
||||
|
||||
def generate_order(self, order: Order):
|
||||
buffer = BytesIO()
|
||||
p = self._create_canvas(buffer)
|
||||
for op in order.positions.all():
|
||||
self._draw_page(p, op, order)
|
||||
p.save()
|
||||
outbuffer = self._render_with_background(buffer)
|
||||
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
|
||||
|
||||
def generate(self, op):
|
||||
buffer = BytesIO()
|
||||
p = self._create_canvas(buffer)
|
||||
order = op.order
|
||||
self._draw_page(p, op, order)
|
||||
p.save()
|
||||
outbuffer = self._render_with_background(buffer)
|
||||
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
|
||||
|
||||
def _create_canvas(self, buffer):
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib import pagesizes
|
||||
|
||||
pagesize = self.settings.get('pagesize', default='A4')
|
||||
if hasattr(pagesizes, pagesize):
|
||||
pagesize = getattr(pagesizes, pagesize)
|
||||
else:
|
||||
pagesize = pagesizes.A4
|
||||
orientation = self.settings.get('orientation', default='portrait')
|
||||
if hasattr(pagesizes, orientation):
|
||||
pagesize = getattr(pagesizes, orientation)(pagesize)
|
||||
|
||||
return canvas.Canvas(buffer, pagesize=pagesize)
|
||||
|
||||
def _render_with_background(self, buffer):
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfFileReader(buffer)
|
||||
output = PdfFileWriter()
|
||||
@@ -121,7 +139,7 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
outbuffer = BytesIO()
|
||||
output.write(outbuffer)
|
||||
outbuffer.seek(0)
|
||||
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
|
||||
return outbuffer
|
||||
|
||||
@property
|
||||
def settings_form_fields(self) -> dict:
|
||||
|
||||
@@ -41,7 +41,10 @@ def contextprocessor(request):
|
||||
for receiver, response in html_head.send(request.event, request=request):
|
||||
_html_head.append(response)
|
||||
for receiver, response in footer_link.send(request.event, request=request):
|
||||
_footer.append(response)
|
||||
if isinstance(response, list):
|
||||
_footer += response
|
||||
else:
|
||||
_footer.append(response)
|
||||
|
||||
if request.event.settings.presale_css_file:
|
||||
ctx['css_file'] = default_storage.url(request.event.settings.presale_css_file)
|
||||
|
||||
@@ -124,7 +124,11 @@
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Place binding order" %}
|
||||
{% if cart.total > 0 %}
|
||||
{% trans "Place binding order" %}
|
||||
{% else %}
|
||||
{% trans "Submit registration" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@@ -113,13 +113,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 availability-box">
|
||||
<a href="#" data-toggle="variations" class="js-only">
|
||||
{% trans "Show variants" %}
|
||||
</a>
|
||||
{% if not event.settings.show_variations_expanded %}
|
||||
<a href="#" data-toggle="variations" class="js-only">
|
||||
{% trans "Show variants" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="variations">
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
|
||||
{% for var in item.available_variations %}
|
||||
<div class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
|
||||
@@ -73,6 +73,17 @@
|
||||
You can download your tickets using the buttons below. Please have your ticket ready when entering the event.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% if cart.positions|length > 1 %}
|
||||
<p>
|
||||
{% trans "Download all tickets at once:" %}
|
||||
{% for b in download_buttons %}
|
||||
<a href="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
|
||||
class="btn btn-default btn-sm" data-asyncdownload>
|
||||
<span class="fa fa-download"></span> {{ b.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% elif not download_buttons %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed with date=event.settings.ticket_download_date|date:"SHORT_DATE_FORMAT" %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% block title %}Event list{% endblock %}
|
||||
{% block title %}{% trans "Event list" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% if "old" in request.GET %}
|
||||
<h3>Past events</h3>
|
||||
@@ -9,7 +9,7 @@
|
||||
<small><a href="?">{% trans "Show upcoming" %}</a></small>
|
||||
</p>
|
||||
{% else %}
|
||||
<h3>Upcoming events </h3>
|
||||
<h3>{% trans "Upcoming events" %}</h3>
|
||||
<p>
|
||||
<small><a href="?old=1">{% trans "Show past events" %}</a></small>
|
||||
</p>
|
||||
|
||||
@@ -45,6 +45,9 @@ event_patterns = [
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
|
||||
pretix.presale.views.order.OrderPayChangeMethod.as_view(),
|
||||
name='event.order.pay.change'),
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
|
||||
pretix.presale.views.order.OrderDownload.as_view(),
|
||||
name='event.order.download.combined'),
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<position>[0-9]+)/(?P<output>[^/]+)$',
|
||||
pretix.presale.views.order.OrderDownload.as_view(),
|
||||
name='event.order.download'),
|
||||
|
||||
@@ -67,7 +67,10 @@ def _detect_event(request, require_live=True):
|
||||
url.url_name == 'event.auth'
|
||||
or (
|
||||
request.user.is_authenticated
|
||||
and EventPermission.objects.filter(event=request.event, user=request.user).exists()
|
||||
and (
|
||||
request.user.is_superuser
|
||||
or EventPermission.objects.filter(event=request.event, user=request.user).exists()
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
@@ -42,8 +42,10 @@ class CartActionMixin:
|
||||
amount = int(value)
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
if amount <= 0:
|
||||
if amount < 0:
|
||||
raise CartError(_('Please enter positive numbers only.'))
|
||||
elif amount == 0:
|
||||
return
|
||||
|
||||
price = self.request.POST.get('price_' + "_".join(parts[1:]), "")
|
||||
if key.startswith('item_'):
|
||||
|
||||
@@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import View
|
||||
|
||||
from pretix.base.models import CartPosition
|
||||
from pretix.base.services.cart import CartError
|
||||
from pretix.base.signals import validate_cart
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.checkoutflow import get_checkout_flow
|
||||
|
||||
@@ -12,12 +14,20 @@ from pretix.presale.checkoutflow import get_checkout_flow
|
||||
class CheckoutView(View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
has_cart = CartPosition.objects.filter(
|
||||
cart_id=self.request.session.session_key, event=self.request.event).exists()
|
||||
if not has_cart and "async_id" not in request.GET:
|
||||
cart_pos = CartPosition.objects.filter(
|
||||
cart_id=self.request.session.session_key, event=self.request.event
|
||||
)
|
||||
|
||||
if not cart_pos.exists() and "async_id" not in request.GET:
|
||||
messages.error(request, _("Your cart is empty"))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
|
||||
try:
|
||||
validate_cart.send(sender=self.request.event, positions=cart_pos)
|
||||
except CartError as e:
|
||||
messages.error(request, str(e))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
|
||||
flow = get_checkout_flow(self.request.event)
|
||||
for step in flow:
|
||||
if not step.is_applicable(request):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
@@ -9,12 +11,12 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
|
||||
from pretix.base.models.orders import InvoiceAddress
|
||||
from pretix.base.models.orders import CachedCombinedTicket, InvoiceAddress
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||
)
|
||||
from pretix.base.services.orders import cancel_order
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.services.tickets import generate, generate_order
|
||||
from pretix.base.signals import (
|
||||
register_payment_providers, register_ticket_outputs,
|
||||
)
|
||||
@@ -508,7 +510,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
if not self.output or not self.output.is_enabled:
|
||||
messages.error(request, _('You requested an invalid ticket output type.'))
|
||||
return redirect(self.get_order_url())
|
||||
if not self.order or not self.order_position:
|
||||
if not self.order or ('position' in kwargs and not self.order_position):
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.order.status != Order.STATUS_PAID:
|
||||
messages.error(request, _('Order is not paid.'))
|
||||
@@ -519,6 +521,39 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
messages.error(request, _('Ticket download is not (yet) enabled.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
if 'position' in kwargs:
|
||||
return self._download_position()
|
||||
else:
|
||||
return self._download_order()
|
||||
|
||||
def _download_order(self):
|
||||
try:
|
||||
ct = CachedCombinedTicket.objects.filter(
|
||||
order=self.order, provider=self.output.identifier
|
||||
).last()
|
||||
except CachedCombinedTicket.DoesNotExist:
|
||||
ct = None
|
||||
|
||||
if not ct:
|
||||
ct = CachedCombinedTicket.objects.create(
|
||||
order=self.order, provider=self.output.identifier,
|
||||
extension='', type='', file=None)
|
||||
generate_order.apply_async(args=(self.order.id, self.output.identifier))
|
||||
|
||||
if 'ajax' in self.request.GET:
|
||||
return HttpResponse('1' if ct and ct.file else '0')
|
||||
elif not ct.file:
|
||||
if now() - ct.created > timedelta(minutes=110):
|
||||
generate_order.apply_async(args=(self.order.id, self.output.identifier))
|
||||
return render(self.request, "pretixbase/cachedfiles/pending.html", {})
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
|
||||
def _download_position(self):
|
||||
try:
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier
|
||||
@@ -532,10 +567,12 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
extension='', type='', file=None)
|
||||
generate.apply_async(args=(self.order_position.id, self.output.identifier))
|
||||
|
||||
if 'ajax' in request.GET:
|
||||
if 'ajax' in self.request.GET:
|
||||
return HttpResponse('1' if ct and ct.file else '0')
|
||||
elif not ct.file:
|
||||
return render(request, "pretixbase/cachedfiles/pending.html", {})
|
||||
if now() - ct.created > timedelta(minutes=110):
|
||||
generate.apply_async(args=(self.order_position.id, self.output.identifier))
|
||||
return render(self.request, "pretixbase/cachedfiles/pending.html", {})
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
|
||||
@@ -10,7 +10,7 @@ class OrganizerIndex(OrganizerViewMixin, ListView):
|
||||
model = Event
|
||||
context_object_name = 'events'
|
||||
template_name = 'pretixpresale/organizers/index.html'
|
||||
paginate_by = 1
|
||||
paginate_by = 30
|
||||
|
||||
def get_queryset(self):
|
||||
query = Q(is_public=True)
|
||||
|
||||
@@ -84,7 +84,15 @@ $(function () {
|
||||
|
||||
$('.collapsible').collapse();
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
var url = document.location.toString();
|
||||
if (url.match('#')) {
|
||||
$('.nav-tabs a[href="#' + url.split('#')[1] + '"]').tab('show');
|
||||
}
|
||||
$('a[data-toggle="tab"]').on('click', function (e) {
|
||||
window.location.hash = e.target.hash;
|
||||
});
|
||||
|
||||
// Question editor
|
||||
if ($("#answer-options").length) {
|
||||
@@ -96,12 +104,21 @@ $(function () {
|
||||
|
||||
// Vouchers
|
||||
$("#voucher-bulk-codes-generate").click(function () {
|
||||
var url = $(this).attr("data-rng-url"),
|
||||
num = $("#voucher-bulk-codes-num").val();
|
||||
$("#id_codes").html("Generating...");
|
||||
$.getJSON(url + '?num=' + num, function (data) {
|
||||
$("#id_codes").val(data.codes.join("\n"));
|
||||
});
|
||||
var num = $("#voucher-bulk-codes-num").val();
|
||||
if (num != "") {
|
||||
$(".form-group:has(#voucher-bulk-codes-num)").addClass("has-error");
|
||||
var url = $(this).attr("data-rng-url");
|
||||
$("#id_codes").html("Generating...");
|
||||
$.getJSON(url + '?num=' + num, function (data) {
|
||||
$("#id_codes").val(data.codes.join("\n"));
|
||||
});
|
||||
} else {
|
||||
$(".form-group:has(#voucher-bulk-codes-num)").addClass("has-error");
|
||||
$("#voucher-bulk-codes-num").focus();
|
||||
setTimeout(function() {
|
||||
$(".form-group:has(#voucher-bulk-codes-num)").removeClass("has-error");
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
|
||||
|
||||
@@ -91,6 +91,9 @@ h1 .btn-sm {
|
||||
}
|
||||
}
|
||||
|
||||
.organizer-tabs .tab-inner {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.helper-display-inline {
|
||||
display: inline !important;
|
||||
|
||||
@@ -20,7 +20,7 @@ $(function () {
|
||||
$($(this).attr("data-target")).collapse('show');
|
||||
});
|
||||
$(".js-only").removeClass("js-only");
|
||||
$(".variations").hide();
|
||||
$(".variations-collapsed").hide();
|
||||
$("a[data-toggle=variations]").click(function (e) {
|
||||
$(this).parent().parent().parent().find(".variations").slideToggle();
|
||||
e.preventDefault();
|
||||
|
||||
@@ -15,7 +15,7 @@ python-u2flib-server==4.*
|
||||
django-formtools==1.0
|
||||
celery==4.0.2
|
||||
kombu==4.0.2
|
||||
django-statici18n==1.2.*
|
||||
django-statici18n==1.3.*
|
||||
inlinestyler==0.2.*
|
||||
BeautifulSoup4
|
||||
html5lib<0.99999999,>=0.999 # version requirement by bleach
|
||||
|
||||
@@ -56,7 +56,7 @@ setup(
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Framework :: Django :: 1.8'
|
||||
'Framework :: Django :: 1.10'
|
||||
],
|
||||
|
||||
keywords='tickets web shop ecommerce',
|
||||
@@ -77,7 +77,7 @@ setup(
|
||||
'django-formtools==1.0',
|
||||
'celery==4.0.2',
|
||||
'kombu==4.0.2',
|
||||
'django-statici18n==1.2.*',
|
||||
'django-statici18n==1.3.*',
|
||||
'inlinestyler==0.2.*',
|
||||
'BeautifulSoup4',
|
||||
'html5lib<0.99999999,>=0.999',
|
||||
|
||||
@@ -223,7 +223,7 @@ def test_webhook_mark_paid(env, client, monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_webhook_partial_refund(env, client, monkeypatch):
|
||||
def test_webhook_refund1(env, client, monkeypatch):
|
||||
charge = get_test_charge(env[1])
|
||||
charge['state'] = 'refunded'
|
||||
|
||||
@@ -232,6 +232,60 @@ def test_webhook_partial_refund(env, client, monkeypatch):
|
||||
|
||||
client.post('/dummy/dummy/paypal/webhook/', json.dumps(
|
||||
{
|
||||
# Sample obtained in a sandbox webhook
|
||||
"id": "WH-9K829080KA1622327-31011919VC6498738",
|
||||
"create_time": "2017-01-15T20:15:36Z",
|
||||
"resource_type": "refund",
|
||||
"event_type": "PAYMENT.SALE.REFUNDED",
|
||||
"summary": "A EUR 255.41 EUR sale payment was refunded",
|
||||
"resource": {
|
||||
"amount": {
|
||||
"total": "255.41",
|
||||
"currency": "EUR"
|
||||
},
|
||||
"id": "75S46770PP192124D",
|
||||
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||
"update_time": "2017-01-15T20:15:06Z",
|
||||
"create_time": "2017-01-15T20:14:29Z",
|
||||
"state": "completed",
|
||||
"links": [],
|
||||
"refund_to_payer": {
|
||||
"value": "255.41",
|
||||
"currency": "EUR"
|
||||
},
|
||||
"invoice_number": "",
|
||||
"refund_reason_code": "REFUND",
|
||||
"sale_id": "9T0916710M1105906"
|
||||
},
|
||||
"links": [],
|
||||
"event_version": "1.0"
|
||||
}
|
||||
), content_type='application_json')
|
||||
|
||||
order = env[1]
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_PAID
|
||||
|
||||
ra = RequiredAction.objects.get(action_type="pretix.plugins.paypal.refund")
|
||||
client.login(username='dummy@dummy.dummy', password='dummy')
|
||||
client.post('/control/event/dummy/dummy/paypal/refund/{}/'.format(ra.pk))
|
||||
|
||||
order = env[1]
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_REFUNDED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_webhook_refund2(env, client, monkeypatch):
|
||||
charge = get_test_charge(env[1])
|
||||
charge['state'] = 'refunded'
|
||||
|
||||
monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
|
||||
monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
|
||||
|
||||
client.post('/dummy/dummy/paypal/webhook/', json.dumps(
|
||||
{
|
||||
# Sample obtained in the webhook simulator
|
||||
"id": "WH-2N242548W9943490U-1JU23391CS4765624",
|
||||
"create_time": "2014-10-31T15:42:24Z",
|
||||
"resource_type": "sale",
|
||||
|
||||
Reference in New Issue
Block a user