Compare commits

...

81 Commits

Author SHA1 Message Date
Raphael Michel
581f3c3d58 Release 1.8.1 2017-11-25 19:07:42 +01:00
Raphael Michel
6600c430ab [SECURITY] Respect session timeout in API 2017-11-25 19:06:46 +01:00
Raphael Michel
807eb2ea7f [SECURITY] Fix handling of session timeouts 2017-11-25 19:06:03 +01:00
Raphael Michel
7dea6fc1b7 Bump version number 2017-10-07 20:38:12 +02:00
Raphael Michel
bd306e9400 Best-effort backwards compatibility of isolated cart IDs 2017-10-07 20:37:12 +02:00
Raphael Michel
3e686211e1 Update translations 2017-10-07 18:42:02 +02:00
Raphael Michel
6d1b4b0a39 Re-order travis matrix for better productivity 2017-10-07 18:16:36 +02:00
Sanket Dasgupta
58938fc07c Fix #531 -- Make placeholders replace in subject (#594)
Placeholders in subject were not being replaced because there was
no `.format()` called on the subject.

This commit creates a context dict that is used for both the body
and the subject. It is then replaced using `.format_map()`

Fixes https://github.com/pretix/pretix/issues/531
2017-10-07 18:16:13 +02:00
Raphael Michel
96dd4e02f3 Add tests for style generation and propagated settings 2017-10-07 18:13:06 +02:00
Raphael Michel
411c537438 UI for settings propagation 2017-10-07 18:13:06 +02:00
Raphael Michel
bbd112280a Propagate setting and add organizer display settings page 2017-10-07 18:13:06 +02:00
Marvin Sipp
28d074366e added organizer color field 2017-10-07 18:13:06 +02:00
Haroon Sheikh
11d76656de Fix #538 -- Remove pyvenv from docs (#633) 2017-10-07 16:50:14 +02:00
Raphael Michel
1c96bc31d5 Re-calculate quotas for all events with recent logs 2017-10-06 11:43:08 +02:00
Raphael Michel
0030064f55 Form UX: Better label in sendmail form 2017-10-06 11:23:21 +02:00
Raphael Michel
4726f5c136 Fix i18n for confirm_text 2017-10-06 11:14:42 +02:00
Raphael Michel
c7fafedc51 Checkout UX: Pre-select payment provider if there is only one 2017-10-06 11:08:00 +02:00
Raphael Michel
3eeb70ae36 Form UX: Add more helpful placeholders 2017-10-06 11:05:24 +02:00
Raphael Michel
29b1a3dca3 Do not send navigation singals for authentication pages 2017-10-06 10:35:24 +02:00
Raphael Michel
caf844b5fb Fix wrong signal name in documentation 2017-10-05 11:55:09 +02:00
Raphael Michel
6b7bdf8c4f Item creation UX: Clearer placeholders, defaults 2017-10-05 10:47:46 +02:00
Raphael Michel
aad433a3bc Welcome wizard UX: Use primary color for button 2017-10-05 10:32:14 +02:00
Raphael Michel
3f1bb56826 Event creation UX: Show clearer that the event is now created 2017-10-05 10:31:25 +02:00
Raphael Michel
b2b3add616 Form UX: Display units for more number inputs 2017-10-05 10:21:00 +02:00
Raphael Michel
2d484d4a8e Event creation UX: Label changes 2017-10-05 10:20:00 +02:00
Raphael Michel
2f252f19c9 Form UX: Use splitted date/time widgets 2017-10-05 10:17:17 +02:00
Raphael Michel
a27f372785 Event creation UX: Pre-choose organizer if there is only one 2017-10-05 08:01:22 +02:00
Raphael Michel
f074e642ec Display quotas in event list 2017-10-04 11:25:51 +02:00
Raphael Michel
217ed905d4 Contract columns in event list table 2017-10-04 10:12:46 +02:00
Raphael Michel
b920efc955 Add database cache for quotas 2017-10-04 09:45:37 +02:00
Raphael Michel
330fadbea9 Fix wrong execution order 2017-10-04 09:43:14 +02:00
Raphael Michel
50c595e3d6 Fix migration error (unique app configuragion keys) 2017-10-02 17:40:31 +02:00
Raphael Michel
26f258c6cf Isolate cart sessions 2017-10-02 17:00:35 +02:00
Raphael Michel
f15a72e59d Fix mail_text_download_reminder email preview 2017-10-02 15:44:32 +02:00
Raphael Michel
8accaae6b1 New signal: allow_ticket_download 2017-10-02 15:07:23 +02:00
Raphael Michel
d4259501af Remove legacy ordering code 2017-10-02 14:59:01 +02:00
Jakob Schnell
fd5d5ae98e Fix #628 -- Sorting of filtered order list (#631)
* fix sorting of filtered order list

fixes #628

* implement comments on pr
2017-10-02 14:55:02 +02:00
Raphael Michel
457901ff82 Fix flake8 error 2017-10-01 17:43:51 +02:00
Raphael Michel
e201be1c65 Clarify payment fee / shipping fee relation 2017-09-29 17:08:04 +02:00
Raphael Michel
acde14372d PDF editor: Change default text 2017-09-29 17:01:13 +02:00
Raphael Michel
79988a2325 New signal order_fee_type_name 2017-09-29 16:54:27 +02:00
Raphael Michel
784f6e703c CSP: Exclude PDF editor (just doesn't work in FF) 2017-09-28 18:44:12 +02:00
Raphael Michel
29b157f287 CSP: Add reporting endpoint 2017-09-28 18:43:45 +02:00
Raphael Michel
c030bd35ca Make PDF ticket cover more extensible 2017-09-27 18:32:50 +02:00
Raphael Michel
06fe076ce2 Add request argument to pretix.control.signals.order_info 2017-09-27 18:19:47 +02:00
Raphael Michel
ae6cba067c Fix issue created in 1f889be0 2017-09-27 14:40:15 +02:00
Raphael Michel
72ae19a95d Update translation 2017-09-27 13:24:03 +02:00
Raphael Michel
1f889be07a Refactor and add signal layout_text_variables 2017-09-27 13:15:18 +02:00
Raphael Michel
39061b659a PDF Editor: More extensible implementation 2017-09-26 13:05:51 +02:00
Raphael Michel
d38f29ac7c Add signal pretix.control.signals.order_info 2017-09-26 11:47:46 +02:00
Raphael Michel
1a8e67f4de Allow clicking on typeahead results 2017-09-25 22:03:25 +02:00
Raphael Michel
8265c302ad Fix missing required=False 2017-09-25 13:33:41 +02:00
Raphael Michel
110d7c6acf Allow to enter a custom text that needs to be confirmed during checkout 2017-09-25 12:48:31 +02:00
Tobias Kunze
244b767f8f Allow markdown rendering in transaction comments. (#621)
This commit allows transaction comments to display newlines and URLs in
a useful way, helping when additional data (such as a reference to a
ticket system or a longer discussion) is required.
This PR also prevents pretix from having to bring its own chat system ;)
2017-09-25 12:25:32 +03:00
Raphael Michel
f40950efc9 Adjust to newer sentry version 2017-09-25 10:46:47 +02:00
Raphael Michel
0e0534c273 Fix incorrect timezones on event dashboard 2017-09-25 10:25:22 +02:00
Raphael Michel
9b3ea3656f PDF Output: Prevent subsequent exception on permission errors 2017-09-25 10:22:09 +02:00
Raphael Michel
62b2a367ff PDF Output: Fix AttributeError with undefined used meta data 2017-09-25 10:20:46 +02:00
Raphael Michel
ab9dd32902 Add font-src to default CSP header 2017-09-25 10:19:36 +02:00
Raphael Michel
43fc498297 Prevent some pages from search indexing 2017-09-25 10:04:37 +02:00
Raphael Michel
ef3eee7873 ContactForm: Prevent TypeError during validation 2017-09-25 09:38:35 +02:00
Raphael Michel
9f0deea9dd Rich text: Do not rewrite mailto: URLs 2017-09-25 09:37:17 +02:00
Abhiraj Hinge
e3798600ed Fixed typo in Concepts.rst (#624) 2017-09-14 16:16:56 +03:00
Raphael Michel
00834cd5e0 Fix test_checkoutflow 2017-09-13 18:29:08 +02:00
Raphael Michel
ed35c4f74e Add new signal logentry_object_link 2017-09-13 17:36:13 +02:00
Raphael Michel
9cd3e2d494 Require payment even if total consists only of fees 2017-09-13 16:42:00 +02:00
Raphael Michel
3345f48986 nav_event_settings should be an EventPluginSignal 2017-09-13 16:21:14 +02:00
Raphael Michel
b611d63975 ModelRelativeDateTimeField: Deal with None values 2017-09-13 16:20:54 +02:00
Raphael Michel
fb3866aa1a Fix TypError in PDF preview 2017-09-13 14:59:19 +02:00
Raphael Michel
a9f131b645 Make PDF download more prominent 2017-09-12 19:06:02 +02:00
Raphael Michel
e5728662c5 Allow to extend expired order even if waiting list entries exist 2017-09-12 18:50:13 +02:00
Raphael Michel
94a97fb0fd Fix broken toggling script 2017-09-09 11:09:03 +02:00
Raphael Michel
b5bea6fe7a Do not disable core modules' URLs 2017-09-08 17:50:50 +02:00
Raphael Michel
fb9d677d76 CSP: Allow blob: URLs for images in PDFs 2017-09-07 23:29:21 +02:00
Raphael Michel
7c4fc7bd0d New signals: fee_calculation_for_cart, order_fee_calculation 2017-09-07 18:59:21 +02:00
Raphael Michel
de992cecf3 New signal checkout_confirm_page_content 2017-09-07 18:15:36 +02:00
Raphael Michel
cd94549606 Fix export of answered files with binary content 2017-09-07 12:38:39 +02:00
Raphael Michel
214a6eb5ce Database field for RelativeDateTime 2017-09-06 11:25:12 +02:00
Raphael Michel
db5f0aa02d Fix #156 -- Plug-in settings navigation hook 2017-09-06 09:31:33 +02:00
Raphael Michel
ba48ab3659 Re-do squashed migration 2017-09-05 15:34:40 +02:00
Raphael Michel
d1538e07d3 Bump version 2017-09-05 12:47:10 +02:00
132 changed files with 5268 additions and 3092 deletions

View File

@@ -12,29 +12,29 @@ services:
- postgresql
matrix:
include:
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=style
- python: 3.6
env: JOB=plugins
- python: 3.6
env: JOB=tests-cov
- python: 3.6
env: JOB=style
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=plugins
addons:
postgresql: "9.4"

View File

@@ -60,7 +60,85 @@ your views::
def admin_view(request, organizer, event):
...
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``.
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``. In case of
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
from django.core.urlresolvers import resolve, reverse
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.control.signals import nav_event
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
def navbar_info(sender, request, **kwargs):
url = resolve(request.path_info)
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
return []
return [{
'label': _('My plugin view'),
'icon': 'heart',
'url': reverse('plugins:myplugin:index', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}),
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'review',
}]
Event settings view
-------------------
A special case of a control panel view is a view hooked into the event settings page. For this case, there is a
special navigation signal::
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
def navbar_settings(sender, request, **kwargs):
url = resolve(request.path_info)
return [{
'label': _('My settings'),
'url': reverse('plugins:myplugin:settings', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}),
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'settings',
}]
Also, your view should inherit from ``EventSettingsViewMixin`` and your template from ``pretixcontrol/event/settings_base.html``
for good integration. If you just want to display a form, you could do it like the following::
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event
permission = 'can_change_settings'
form_class = MySettingsForm
template_name = 'my_plugin/settings.html'
def get_success_url(self, **kwargs):
return reverse('plugins:myplugin:settings', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
With this template::
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %} {% trans "Friends Tickets Settings" %} {% endblock %}
{% block inside %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<fieldset>
<legend>{% trans "Friends Tickets Settings" %}</legend>
{% bootstrap_form form layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
Frontend views
--------------

View File

@@ -19,13 +19,13 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, order_paid, order_placed
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, question_form_fields, checkout_confirm_messages
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
.. automodule:: pretix.presale.signals
@@ -47,11 +47,11 @@ Backend
-------
.. automodule:: pretix.control.signals
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info
.. automodule:: pretix.base.signals
:members: logentry_display, requiredaction_display
:members: logentry_display, logentry_object_link, requiredaction_display
Vouchers
""""""""
@@ -64,3 +64,9 @@ Dashboards
.. automodule:: pretix.control.signals
:members: event_dashboard_widgets, user_dashboard_widgets
Ticket designs
""""""""""""""
.. automodule:: pretix.plugins.ticketoutputpdf.signals
:members: layout_text_variables

View File

@@ -114,6 +114,19 @@ method to make your receivers available::
def ready(self):
from . import signals # NOQA
You can optionally specify code that is executed when your plugin is activated for an event
in the ``installed`` method::
class PaypalApp(AppConfig):
def installed(self, event):
pass # Your code here
Note that ``installed`` will *not* be called if the plugin in indirectly activated for an event
because the event is created with settings copied from another event.
Views
-----

View File

@@ -59,7 +59,7 @@ If an item is assigned to multiple quotas, it can only be bought if *all of them
If multiple items are assigned to the same quota, the quota will be counted as sold out as soon as the
*sum* of the two items exceeds the quota limit.
The availability of a quota is currently calculated by substracting the following numbers from the quota
The availability of a quota is currently calculated by subtracting the following numbers from the quota
limit:
* The number of orders placed for an item that are either already paid or within their granted payment period

View File

@@ -20,7 +20,6 @@ Your should install the following on your system:
* Python 3.4 or newer
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
* ``pyvenv`` for Python 3 (Debian package: ``python3-venv``)
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
* ``libffi`` (Debian package: ``libffi-dev``)
* ``libssl`` (Debian package: ``libssl-dev``)
@@ -37,7 +36,7 @@ Please execute ``python -V`` or ``python3 -V`` to make sure you have Python 3.4
execute ``pip3 -V`` to check. Then use Python's internal tools to create a virtual
environment and activate it for your current session::
pyvenv env
python3 -m venv env
source env/bin/activate
You should now see a ``(env)`` prepended to your shell prompt. You have to do this

View File

@@ -1 +1 @@
__version__ = "1.7.0"
__version__ = "1.8.1"

View File

@@ -1,3 +1,7 @@
import time
from django.conf import settings
from django.contrib.auth import logout
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.base.models import Event
@@ -13,6 +17,18 @@ class EventPermission(BasePermission):
return True
return False
if request.user.is_authenticated:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return False
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return False
request.session['pretix_auth_last_used'] = int(time.time())
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:

View File

@@ -11,7 +11,7 @@ class PretixBaseConfig(AppConfig):
from . import payment # NOQA
from . import exporters # NOQA
from . import invoice # NOQA
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check # NOQA
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas # NOQA
try:
from .celery_app import app as celery_app # NOQA

View File

@@ -41,7 +41,7 @@ class AnswerFilesExporter(BaseExporter):
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
if i.file:
i.file.open('r')
i.file.open('rb')
fname = '{}-{}-{}-q{}-{}'.format(
self.event.slug.upper(),
i.orderposition.order.code,

View File

@@ -186,11 +186,13 @@ class SecurityMiddleware(MiddlewareMixin):
'style-src': ["{static}", "{media}", "'nonce-{nonce}'"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
'font-src': ["{static}"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"],
'report-uri': ["/csp_report/"],
}
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
@@ -218,7 +220,14 @@ class SecurityMiddleware(MiddlewareMixin):
domain = '%s:%d' % (domain, siteurlsplit.port)
dynamicdomain += " " + domain
if request.path not in self.CSP_EXEMPT:
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain, nonce=request.csp_nonce)
for k, v in h.items():
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain,
nonce=request.csp_nonce).split(' ')
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp

File diff suppressed because one or more lines are too long

View File

@@ -1,484 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-05 10:20
from __future__ import unicode_literals
from decimal import Decimal
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
import django_countries.fields
import i18nfield.fields
from django.core.cache import cache
from django.db import migrations, models
from i18nfield.strings import LazyI18nString
import pretix.base.models.base
import pretix.base.models.vouchers
def tax_rate_converter(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
Item = app.get_model('pretixbase', 'Item')
TaxRule = app.get_model('pretixbase', 'TaxRule')
Order = app.get_model('pretixbase', 'Order')
OrderPosition = app.get_model('pretixbase', 'OrderPosition')
InvoiceLine = app.get_model('pretixbase', 'InvoiceLine')
n = LazyI18nString({
'en': 'VAT',
'de': 'MwSt.',
'de-informal': 'MwSt.'
})
for i in Item.objects.select_related('event').exclude(tax_rate=0):
try:
i.tax_rule = i.event.tax_rules.get(rate=i.tax_rate)
except TaxRule.DoesNotExist:
tr = i.event.tax_rules.create(rate=i.tax_rate, name=n)
i.tax_rule = tr
i.save()
for o in Order.objects.select_related('event').exclude(payment_fee_tax_rate=0):
try:
o.payment_fee_tax_rule = o.event.tax_rules.get(rate=o.payment_fee_tax_rate)
except TaxRule.DoesNotExist:
tr = o.event.tax_rules.create(rate=o.payment_fee_tax_rate, name=n)
o.tax_rule = tr
o.save()
for op in OrderPosition.objects.select_related('order', 'order__event').exclude(tax_rate=0):
try:
op.tax_rule = op.order.event.tax_rules.get(rate=op.tax_rate)
except TaxRule.DoesNotExist:
tr = op.order.event.tax_rules.create(rate=op.tax_rate, name=n)
op.tax_rule = tr
op.save()
for il in InvoiceLine.objects.select_related('invoice', 'invoice__event').exclude(tax_rate=0):
try:
il.tax_name = il.invoice.event.tax_rules.get(rate=op.tax_rate).name
except TaxRule.DoesNotExist:
tr = il.invoice.event.tax_rules.create(rate=op.tax_rate, name=n)
il.tax_name = tr.name
il.save()
for setting in EventSettingsStore.objects.filter(key='tax_rate_default'):
try:
tr = setting.object.tax_rules.get(rate=setting.value)
except TaxRule.DoesNotExist:
tr = setting.object.tax_rules.create(rate=setting.value, name=n)
setting.value = tr.pk
setting.save()
cache.delete('hierarkey_{}_{}'.format('event', setting.object.pk))
def fee_converter(app, schema_editor):
OrderFee = app.get_model('pretixbase', 'OrderFee')
Order = app.get_model('pretixbase', 'Order')
of = []
for o in Order.objects.exclude(payment_fee=Decimal('0.00')).iterator():
of.append(OrderFee(
order=o,
value=o.payment_fee,
fee_type='payment',
tax_rate=o.payment_fee_tax_rate,
tax_rule=o.payment_fee_tax_rule,
tax_value=o.payment_fee_tax_value,
internal_type=o.payment_provider
))
if len(of) > 900:
OrderFee.objects.bulk_create(of)
of = []
OrderFee.objects.bulk_create(of)
def assign_positions(app, schema_editor):
Invoice = app.get_model('pretixbase', 'Invoice')
for i in Invoice.objects.iterator():
for j, l in enumerate(i.lines.all()):
l.position = j
l.save()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0071_auto_20170729_1616'), ('pretixbase', '0072_order_download_reminder_sent'),
('pretixbase', '0073_auto_20170716_1333'), ('pretixbase', '0074_auto_20170825_1258'),
('pretixbase', '0075_auto_20170828_0901'), ('pretixbase', '0076_orderfee'),
('pretixbase', '0077_auto_20170829_1126')]
dependencies = [
('pretixbase', '0070_auto_20170719_0910'),
]
operations = [
migrations.AddField(
model_name='question',
name='help_text',
field=i18nfield.fields.I18nTextField(blank=True,
help_text='If the question needs to be explained or clarified, '
'do it here!',
null=True, verbose_name='Help text'),
),
migrations.AlterField(
model_name='invoiceaddress',
name='vat_id',
field=models.CharField(blank=True, help_text='Only for business customers within the EU.', max_length=255,
verbose_name='VAT ID'),
),
migrations.AddField(
model_name='order',
name='download_reminder_sent',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='TaxRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', i18nfield.fields.I18nCharField(help_text='Should be short, e.g. "VAT"', max_length=190,
verbose_name='Name')),
('rate', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax rate')),
('price_includes_tax', models.BooleanField(default=True,
verbose_name='The configured product prices includes the '
'tax amount')),
('eu_reverse_charge', models.BooleanField(default=False,
help_text='Not recommended. Most events will NOT be '
'qualified for reverse charge since the place of '
'taxation is the location of the event. This '
'option only enables reverse charge for business '
'customers who entered a valid EU VAT ID. Only '
'enable this option after consulting a tax '
'counsel. No warranty given for correct tax '
'calculation.',
verbose_name='Use EU reverse charge taxation')),
('home_country', models.CharField(blank=True,
choices=[('AT', 'Austria'), ('BE', 'Belgium'), ('BG', 'Bulgaria'),
('HR', 'Croatia'), ('CY', 'Cyprus'),
('CZ', 'Czech Republic'), ('DK', 'Denmark'),
('EE', 'Estonia'), ('FI', 'Finland'), ('FR', 'France'),
('DE', 'Germany'), ('GR', 'Greece'), ('HU', 'Hungary'),
('IE', 'Ireland'), ('IT', 'Italy'), ('LV', 'Latvia'),
('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MT', 'Malta'),
('NL', 'Netherlands'), ('PL', 'Poland'), ('PT', 'Portugal'),
('RO', 'Romania'), ('SK', 'Slovakia'), ('SI', 'Slovenia'),
('ES', 'Spain'), ('SE', 'Sweden'), ('UJ', 'United Kingdom')],
help_text='Your country. Only relevant for EU reverse charge.',
max_length=2, verbose_name='Merchant country')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tax_rules',
to='pretixbase.Event')),
],
),
migrations.AddField(
model_name='item',
name='tax_rule',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule', verbose_name='Sales tax'),
),
migrations.AddField(
model_name='order',
name='payment_fee_tax_rule',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule'),
),
migrations.AddField(
model_name='orderposition',
name='tax_rule',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule'),
),
migrations.RunPython(
code=tax_rate_converter,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='item',
name='tax_rate',
),
migrations.AddField(
model_name='invoiceaddress',
name='vat_id_validated',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='invoiceaddress',
name='vat_id',
field=models.CharField(blank=True, help_text='Only for business customers within the EU.', max_length=255,
verbose_name='VAT ID'),
),
migrations.AlterField(
model_name='taxrule',
name='home_country',
field=django_countries.fields.CountryField(blank=True,
help_text='Your country of residence. This is the country the '
'EU reverse charge rule will not apply in, '
'if configured above.',
max_length=2, verbose_name='Merchant country'),
),
migrations.AddField(
model_name='cartposition',
name='includes_tax',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='invoiceline',
name='tax_name',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AlterField(
model_name='taxrule',
name='eu_reverse_charge',
field=models.BooleanField(default=False,
help_text='Not recommended. Most events will NOT be qualified for reverse '
'charge since the place of taxation is the location of the event. '
'This option disables charging VAT for all customers outside the EU '
'and for business customers in different EU countries that do not '
'customers who entered a valid EU VAT ID. Only enable this option '
'after consulting a tax counsel. No warranty given for correct tax '
'calculation. USE AT YOUR OWN RISK.',
verbose_name='Use EU reverse charge taxation rules'),
),
migrations.AddField(
model_name='invoice',
name='foreign_currency_display',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name='invoice',
name='foreign_currency_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
),
migrations.AddField(
model_name='invoice',
name='foreign_currency_rate_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='item',
name='checkin_attention',
field=models.BooleanField(default=False,
help_text='If you set this, the check-in app will show a visible warning that '
'this ticket requires special attention. You can use this for example '
'for student tickets to indicate to the person at check-in that the '
'student ID card still needs to be checked.',
verbose_name='Requires special attention'),
),
migrations.AlterField(
model_name='event',
name='currency',
field=models.CharField(choices=[('AED', 'AED - UAE Dirham'), ('AFN', 'AFN - Afghani'), ('ALL', 'ALL - Lek'),
('AMD', 'AMD - Armenian Dram'),
('ANG', 'ANG - Netherlands Antillean Guilder'), ('AOA', 'AOA - Kwanza'),
('ARS', 'ARS - Argentine Peso'), ('AUD', 'AUD - Australian Dollar'),
('AWG', 'AWG - Aruban Florin'), ('AZN', 'AZN - Azerbaijanian Manat'),
('BAM', 'BAM - Convertible Mark'), ('BBD', 'BBD - Barbados Dollar'),
('BDT', 'BDT - Taka'), ('BGN', 'BGN - Bulgarian Lev'),
('BHD', 'BHD - Bahraini Dinar'), ('BIF', 'BIF - Burundi Franc'),
('BMD', 'BMD - Bermudian Dollar'), ('BND', 'BND - Brunei Dollar'),
('BOB', 'BOB - Boliviano'), ('BRL', 'BRL - Brazilian Real'),
('BSD', 'BSD - Bahamian Dollar'), ('BTN', 'BTN - Ngultrum'),
('BWP', 'BWP - Pula'), ('BYN', 'BYN - Belarusian Ruble'),
('BZD', 'BZD - Belize Dollar'), ('CAD', 'CAD - Canadian Dollar'),
('CDF', 'CDF - Congolese Franc'), ('CHF', 'CHF - Swiss Franc'),
('CLP', 'CLP - Chilean Peso'), ('CNY', 'CNY - Yuan Renminbi'),
('COP', 'COP - Colombian Peso'), ('CRC', 'CRC - Costa Rican Colon'),
('CUC', 'CUC - Peso Convertible'), ('CUP', 'CUP - Cuban Peso'),
('CVE', 'CVE - Cabo Verde Escudo'), ('CZK', 'CZK - Czech Koruna'),
('DJF', 'DJF - Djibouti Franc'), ('DKK', 'DKK - Danish Krone'),
('DOP', 'DOP - Dominican Peso'), ('DZD', 'DZD - Algerian Dinar'),
('EGP', 'EGP - Egyptian Pound'), ('ERN', 'ERN - Nakfa'),
('ETB', 'ETB - Ethiopian Birr'), ('EUR', 'EUR - Euro'),
('FJD', 'FJD - Fiji Dollar'), ('FKP', 'FKP - Falkland Islands Pound'),
('GBP', 'GBP - Pound Sterling'), ('GEL', 'GEL - Lari'),
('GHS', 'GHS - Ghana Cedi'), ('GIP', 'GIP - Gibraltar Pound'),
('GMD', 'GMD - Dalasi'), ('GNF', 'GNF - Guinea Franc'),
('GTQ', 'GTQ - Quetzal'), ('GYD', 'GYD - Guyana Dollar'),
('HKD', 'HKD - Hong Kong Dollar'), ('HNL', 'HNL - Lempira'),
('HRK', 'HRK - Kuna'), ('HTG', 'HTG - Gourde'), ('HUF', 'HUF - Forint'),
('IDR', 'IDR - Rupiah'), ('ILS', 'ILS - New Israeli Sheqel'),
('INR', 'INR - Indian Rupee'), ('IQD', 'IQD - Iraqi Dinar'),
('IRR', 'IRR - Iranian Rial'), ('ISK', 'ISK - Iceland Krona'),
('JMD', 'JMD - Jamaican Dollar'), ('JOD', 'JOD - Jordanian Dinar'),
('JPY', 'JPY - Yen'), ('KES', 'KES - Kenyan Shilling'),
('KGS', 'KGS - Som'), ('KHR', 'KHR - Riel'), ('KMF', 'KMF - Comoro Franc'),
('KPW', 'KPW - North Korean Won'), ('KRW', 'KRW - Won'),
('KWD', 'KWD - Kuwaiti Dinar'), ('KYD', 'KYD - Cayman Islands Dollar'),
('KZT', 'KZT - Tenge'), ('LAK', 'LAK - Kip'),
('LBP', 'LBP - Lebanese Pound'), ('LKR', 'LKR - Sri Lanka Rupee'),
('LRD', 'LRD - Liberian Dollar'), ('LSL', 'LSL - Loti'),
('LYD', 'LYD - Libyan Dinar'), ('MAD', 'MAD - Moroccan Dirham'),
('MDL', 'MDL - Moldovan Leu'), ('MGA', 'MGA - Malagasy Ariary'),
('MKD', 'MKD - Denar'), ('MMK', 'MMK - Kyat'), ('MNT', 'MNT - Tugrik'),
('MOP', 'MOP - Pataca'), ('MRO', 'MRO - Ouguiya'),
('MUR', 'MUR - Mauritius Rupee'), ('MVR', 'MVR - Rufiyaa'),
('MWK', 'MWK - Malawi Kwacha'), ('MXN', 'MXN - Mexican Peso'),
('MYR', 'MYR - Malaysian Ringgit'), ('MZN', 'MZN - Mozambique Metical'),
('NAD', 'NAD - Namibia Dollar'), ('NGN', 'NGN - Naira'),
('NIO', 'NIO - Cordoba Oro'), ('NOK', 'NOK - Norwegian Krone'),
('NPR', 'NPR - Nepalese Rupee'), ('NZD', 'NZD - New Zealand Dollar'),
('OMR', 'OMR - Rial Omani'), ('PAB', 'PAB - Balboa'), ('PEN', 'PEN - Sol'),
('PGK', 'PGK - Kina'), ('PHP', 'PHP - Philippine Peso'),
('PKR', 'PKR - Pakistan Rupee'), ('PLN', 'PLN - Zloty'),
('PYG', 'PYG - Guarani'), ('QAR', 'QAR - Qatari Rial'),
('RON', 'RON - Romanian Leu'), ('RSD', 'RSD - Serbian Dinar'),
('RUB', 'RUB - Russian Ruble'), ('RWF', 'RWF - Rwanda Franc'),
('SAR', 'SAR - Saudi Riyal'), ('SBD', 'SBD - Solomon Islands Dollar'),
('SCR', 'SCR - Seychelles Rupee'), ('SDG', 'SDG - Sudanese Pound'),
('SEK', 'SEK - Swedish Krona'), ('SGD', 'SGD - Singapore Dollar'),
('SHP', 'SHP - Saint Helena Pound'), ('SLL', 'SLL - Leone'),
('SOS', 'SOS - Somali Shilling'), ('SRD', 'SRD - Surinam Dollar'),
('SSP', 'SSP - South Sudanese Pound'), ('STD', 'STD - Dobra'),
('SVC', 'SVC - El Salvador Colon'), ('SYP', 'SYP - Syrian Pound'),
('SZL', 'SZL - Lilangeni'), ('THB', 'THB - Baht'), ('TJS', 'TJS - Somoni'),
('TMT', 'TMT - Turkmenistan New Manat'), ('TND', 'TND - Tunisian Dinar'),
('TOP', 'TOP - Paanga'), ('TRY', 'TRY - Turkish Lira'),
('TTD', 'TTD - Trinidad and Tobago Dollar'),
('TWD', 'TWD - New Taiwan Dollar'), ('TZS', 'TZS - Tanzanian Shilling'),
('UAH', 'UAH - Hryvnia'), ('UGX', 'UGX - Uganda Shilling'),
('USD', 'USD - US Dollar'), ('UYU', 'UYU - Peso Uruguayo'),
('UZS', 'UZS - Uzbekistan Sum'), ('VEF', 'VEF - Bolívar'),
('VND', 'VND - Dong'), ('VUV', 'VUV - Vatu'), ('WST', 'WST - Tala'),
('XAF', 'XAF - CFA Franc BEAC'), ('XAG', 'XAG - Silver'),
('XAU', 'XAU - Gold'),
('XBA', 'XBA - Bond Markets Unit European Composite Unit (EURCO)'),
('XBB', 'XBB - Bond Markets Unit European Monetary Unit (E.M.U.-6)'),
('XBC', 'XBC - Bond Markets Unit European Unit of Account 9 (E.U.A.-9)'),
('XBD', 'XBD - Bond Markets Unit European Unit of Account 17 (E.U.A.-17)'),
('XCD', 'XCD - East Caribbean Dollar'),
('XDR', 'XDR - SDR (Special Drawing Right)'),
('XOF', 'XOF - CFA Franc BCEAO'), ('XPD', 'XPD - Palladium'),
('XPF', 'XPF - CFP Franc'), ('XPT', 'XPT - Platinum'),
('XSU', 'XSU - Sucre'),
('XTS', 'XTS - Codes specifically reserved for testing purposes'),
('XUA', 'XUA - ADB Unit of Account'), ('XXX',
'XXX - The codes assigned for '
'transactions where no currency is '
'involved'),
('YER', 'YER - Yemeni Rial'), ('ZAR', 'ZAR - Rand'),
('ZMW', 'ZMW - Zambian Kwacha'), ('ZWL', 'ZWL - Zimbabwe Dollar')],
default='EUR', max_length=10, verbose_name='Event currency'),
),
migrations.AlterField(
model_name='taxrule',
name='price_includes_tax',
field=models.BooleanField(default=True,
verbose_name='The configured product prices include the tax amount'),
),
migrations.AlterField(
model_name='voucher',
name='code',
field=models.CharField(db_index=True, default=pretix.base.models.vouchers.generate_code, max_length=255,
validators=[django.core.validators.MinLengthValidator(5)],
verbose_name='Voucher code'),
),
migrations.CreateModel(
name='EventMetaProperty',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True,
help_text='Can not contain spaces or special characters execpt underscores',
max_length=50, validators=[django.core.validators.RegexValidator(
message='The property name may only contain letters, numbers and underscores.',
regex='^[a-zA-Z0-9_]+$')], verbose_name='Name')),
('default', models.TextField()),
('organizer',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_properties',
to='pretixbase.Organizer')),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='EventMetaValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values',
to='pretixbase.Event')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_values',
to='pretixbase.EventMetaProperty')),
],
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='SubEventMetaValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField()),
('property',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subevent_values',
to='pretixbase.EventMetaProperty')),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values',
to='pretixbase.SubEvent')),
],
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AlterUniqueTogether(
name='subeventmetavalue',
unique_together=set([('subevent', 'property')]),
),
migrations.AlterUniqueTogether(
name='eventmetavalue',
unique_together=set([('event', 'property')]),
),
migrations.CreateModel(
name='OrderFee',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Value')),
('description', models.CharField(blank=True, max_length=190)),
('internal_type', models.CharField(blank=True, max_length=255)),
('fee_type', models.CharField(choices=[('payment', 'Payment method fee'), ('shipping', 'Shipping fee')],
max_length=100)),
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Tax rate')),
('tax_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax value')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='fees',
to='pretixbase.Order', verbose_name='Order')),
('tax_rule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule')),
],
),
migrations.RunPython(
code=fee_converter,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='order',
name='payment_fee',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_rate',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_rule',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_value',
),
migrations.AddField(
model_name='invoiceline',
name='position',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(
choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('other', 'Other fees')],
max_length=100),
),
migrations.RunPython(
code=assign_positions,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterModelOptions(
name='invoiceline',
options={'ordering': ('position', 'pk')},
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-10-03 16:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0077_auto_20170829_1126'),
]
operations = [
migrations.AddField(
model_name='quota',
name='cached_availability_number',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='quota',
name='cached_availability_state',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='quota',
name='cached_availability_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='eventmetaproperty',
name='default',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='taxrule',
name='eu_reverse_charge',
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
),
]

View File

@@ -38,6 +38,31 @@ class EventMixin:
raise ValidationError({'date_to': _('The end of the event has to be later than its start.')})
super().clean()
def get_short_date_from_display(self, tz=None, show_times=True) -> str:
"""
Returns a shorter formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
"""
tz = tz or pytz.timezone(self.settings.timezone)
return _date(
self.date_from.astimezone(tz),
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
)
def get_short_date_to_display(self, tz=None) -> str:
"""
Returns a shorter formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return ""
return _date(
self.date_to.astimezone(tz),
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_from_display(self, tz=None, show_times=True) -> str:
"""
Returns a formatted string containing the start date of the event with respect
@@ -169,7 +194,7 @@ class Event(EventMixin, LoggedModel):
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),
verbose_name=_("Event name"),
)
slug = models.SlugField(
max_length=50, db_index=True,

View File

@@ -175,6 +175,7 @@ class Item(LoggedModel):
related_name="items",
blank=True, null=True,
verbose_name=_("Category"),
help_text=_("If you have many products, you can optionally sort them into categories to keep things organized.")
)
name = I18nCharField(
max_length=255,
@@ -704,6 +705,9 @@ class Quota(LoggedModel):
blank=True,
verbose_name=_("Variations")
)
cached_availability_state = models.PositiveIntegerField(null=True, blank=True)
cached_availability_number = models.PositiveIntegerField(null=True, blank=True)
cached_availability_time = models.DateTimeField(null=True, blank=True)
class Meta:
verbose_name = _("Quota")
@@ -718,11 +722,24 @@ class Quota(LoggedModel):
self.event.get_cache().clear()
def save(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', True)
super().save(*args, **kwargs)
if self.event:
if self.event and clear_cache:
self.event.get_cache().clear()
def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
def rebuild_cache(self, now_dt=None):
self.cached_availability_time = None
self.cached_availability_number = None
self.cached_availability_state = None
self.availability(now_dt=now_dt)
def cache_is_hot(self, now_dt=None):
now_dt = now_dt or now()
return self.cached_availability_time and (now_dt - self.cached_availability_time).total_seconds() < 120
def availability(
self, now_dt: datetime=None, count_waitinglist=True, _cache=None, allow_cache=False
) -> Tuple[int, int]:
"""
This method is used to determine whether Items or ItemVariations belonging
to this quota should currently be available for sale.
@@ -730,12 +747,26 @@ class Quota(LoggedModel):
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
and the second is the number of available tickets.
"""
if allow_cache and self.cache_is_hot() and count_waitinglist:
return self.cached_availability_state, self.cached_availability_number
if _cache and count_waitinglist is not _cache.get('_count_waitinglist', True):
_cache.clear()
if _cache is not None and self.pk in _cache:
return _cache[self.pk]
now_dt = now_dt or now()
res = self._availability(now_dt, count_waitinglist)
if count_waitinglist and not self.cache_is_hot(now_dt):
self.cached_availability_state = res[0]
self.cached_availability_number = res[1]
self.cached_availability_time = now_dt
self.save(
update_fields=['cached_availability_state', 'cached_availability_number', 'cached_availability_time'],
clear_cache=False
)
if _cache is not None:
_cache[self.pk] = res
_cache['_count_waitinglist'] = count_waitinglist

View File

@@ -8,6 +8,8 @@ from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.signals import logentry_object_link
class LogEntry(models.Model):
"""
@@ -146,6 +148,9 @@ class LogEntry(models.Model):
elif a_text:
return a_text
else:
for receiver, response in logentry_object_link.send(self.event, logentry=self):
if response:
return response
return ''
@cached_property

View File

@@ -333,7 +333,7 @@ class Order(LoggedModel):
return self._is_still_available()
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True) -> Union[bool, str]:
error_messages = {
'unavailable': _('The ordered product "{item}" is no longer available.'),
}
@@ -351,7 +351,7 @@ class Order(LoggedModel):
for quota in quotas:
if quota.id not in quota_cache:
quota_cache[quota.id] = quota
quota.cached_availability = quota.availability(now_dt)[1]
quota.cached_availability = quota.availability(now_dt, count_waitinglist=count_waitinglist)[1]
else:
# Use cached version
quota = quota_cache[quota.id]

View File

@@ -21,6 +21,7 @@ from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import get_or_create_cart_id
logger = logging.getLogger(__name__)
@@ -149,7 +150,9 @@ class BasePaymentProvider:
('_fee_percent',
forms.DecimalField(
label=_('Additional fee'),
help_text=_('Percentage'),
help_text=_('Percentage of the order total. Note that this percentage will currently only '
'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
'fees, if there are any.'),
required=False
)),
('_availability_date',
@@ -173,6 +176,7 @@ class BasePaymentProvider:
help_text=_('Will be printed just below the payment figures and above the closing text on invoices.'),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
])
@@ -273,7 +277,7 @@ class BasePaymentProvider:
The default implementation checks for the _availability_date setting to be either unset or in the future.
"""
return self._is_still_available(cart_id=request.session.session_key)
return self._is_still_available(cart_id=get_or_create_cart_id(request))
def payment_form_render(self, request: HttpRequest) -> str:
"""
@@ -591,7 +595,11 @@ class FreeOrderProvider(BasePaymentProvider):
messages.success(request, _('The order has been marked as refunded.'))
def is_allowed(self, request: HttpRequest) -> bool:
return get_cart_total(request) == 0
from .services.cart import get_fees
total = get_cart_total(request)
total += sum([f.value for f in get_fees(self.event, request, total, None, None)])
return total == 0
def order_change_allowed(self, order: Order) -> bool:
return False

View File

@@ -6,6 +6,7 @@ import pytz
from dateutil import parser
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
BASE_CHOICES = (
@@ -107,6 +108,9 @@ class RelativeDateWrapper:
data = parser.parse(input)
return RelativeDateWrapper(data)
def __len__(self):
return len(self.to_string())
class RelativeDateTimeWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/reldatetime.html'
@@ -168,6 +172,8 @@ class RelativeDateTimeField(forms.MultiValueField):
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
@@ -277,3 +283,34 @@ class RelativeDateField(RelativeDateTimeField):
raise ValidationError(self.error_messages['incomplete'])
return super().clean(value)
class ModelRelativeDateTimeField(models.CharField):
form_class = RelativeDateTimeField
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event', None)
kwargs.setdefault('max_length', 255)
super().__init__(*args, **kwargs)
def to_python(self, value):
if isinstance(value, RelativeDateWrapper):
return value
if value is None:
return None
return RelativeDateWrapper.from_string(value)
def get_prep_value(self, value):
if isinstance(value, RelativeDateWrapper):
return value.to_string()
return value
def from_db_value(self, value, expression, connection, context):
if value is None:
return None
return RelativeDateWrapper.from_string(value)
def formfield(self, **kwargs):
defaults = {'form_class': self.form_class}
defaults.update(kwargs)
return super().formfield(**defaults)

View File

@@ -6,6 +6,7 @@ from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django.db import transaction
from django.db.models import Q
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext as _
@@ -19,7 +20,11 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.services.async import ProfiledTask
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.pricing import get_price
from pretix.base.templatetags.rich_text import rich_text
from pretix.celery_app import app
from pretix.presale.signals import (
checkout_confirm_messages, fee_calculation_for_cart,
)
class CartError(LazyLocaleException):
@@ -627,13 +632,10 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
return totaldiff
def get_fees(event, total, invoice_address, provider):
def get_fees(event, request, total, invoice_address, provider):
fees = []
if total == 0:
return fees
if provider:
if provider and total != 0:
provider = event.get_payment_providers().get(provider)
if provider:
payment_fee = provider.calculate_fee(total)
@@ -643,7 +645,7 @@ def get_fees(event, total, invoice_address, provider):
if payment_fee_tax_rule.tax_applicable(invoice_address):
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
fees.append(OrderFee(
fee_type="PAYMENT",
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
@@ -651,13 +653,16 @@ def get_fees(event, total, invoice_address, provider):
))
else:
fees.append(OrderFee(
fee_type="PAYMENT",
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'),
tax_rule=payment_fee_tax_rule
))
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address):
fees += resp
return fees
@@ -760,3 +765,13 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
@receiver(checkout_confirm_messages, dispatch_uid="cart_confirm_messages")
def confirm_messages(sender, *args, **kwargs):
if not sender.settings.confirm_text:
return {}
return {
'confirm_text': rich_text(str(sender.settings.confirm_text))
}

View File

@@ -82,6 +82,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
'invoice_company': ''
})
body, body_md = render_mail(template, context)
subject = str(subject).format_map(context)
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
subject = str(subject)

View File

@@ -34,7 +34,10 @@ from pretix.base.services.invoices import (
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.signals import order_paid, order_placed, periodic_task
from pretix.base.signals import (
allow_ticket_download, order_fee_calculation, order_paid, order_placed,
periodic_task,
)
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -342,7 +345,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
raise OrderError(err, errargs)
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider):
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider, address: InvoiceAddress,
meta_info: dict, event: Event):
fees = []
total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total)
@@ -350,15 +354,18 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
fees.append(OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
internal_type=payment_provider.identifier))
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address,
meta_info=meta_info, posiitons=positions):
fees += resp
return fees
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None):
from datetime import time
fees = _get_fees(positions, payment_provider)
fees = _get_fees(positions, payment_provider, address, meta_info, event)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
tz = pytz.timezone(event.settings.timezone)
@@ -561,6 +568,9 @@ def send_download_reminders(sender, **kwargs):
if now() < reminder_date:
continue
for o in e.orders.filter(status=Order.STATUS_PAID, download_reminder_sent=False):
if not all([r for rr, r in allow_ticket_download.send(e, order=o)]):
continue
o.download_reminder_sent = True
o.save()
email_template = e.settings.mail_text_download_reminder

View File

@@ -0,0 +1,32 @@
from django.db import models
from django.db.models import F, Max, OuterRef, Q, Subquery
from django.dispatch import receiver
from pretix.base.models import LogEntry, Quota
from pretix.celery_app import app
from ..signals import periodic_task
@receiver(signal=periodic_task)
def build_all_quota_caches(sender, **kwargs):
refresh_quota_cashes.apply_async()
@app.task
def refresh_quota_cashes():
last_activity = LogEntry.objects.filter(
event=OuterRef('event_id'),
).order_by().values('event').annotate(
m=Max('datetime')
).values(
'm'
)
quotas = Quota.objects.annotate(
last_activity=Subquery(last_activity, output_field=models.DateTimeField())
).filter(
Q(cached_availability_time__isnull=True) |
Q(cached_availability_time__lt=F('last_activity'))
)
for q in quotas:
q.availability()

View File

@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.signals import order_fee_type_name
class DummyObject:
@@ -199,9 +200,15 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
for pprov, total in sorted(num_total.items(), key=lambda i: i[0]):
ppobj = DummyObject()
if pprov[0] == OrderFee.FEE_TYPE_PAYMENT:
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], provider_names.get(pprov[1], pprov[1]))
ppobj.name = '{} - {}'.format(names[pprov[0]], provider_names.get(pprov[1], pprov[1]))
else:
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], pprov[1])
name = pprov[1]
for r, resp in order_fee_type_name.send(sender=event, fee_type=pprov[0], internal_type=pprov[1]):
if resp:
name = resp
break
ppobj.name = '{} - {}'.format(names[pprov[0]], name)
ppobj.provider = pprov[1]
ppobj.has_variations = False
ppobj.num_total = total

View File

@@ -209,6 +209,10 @@ DEFAULTS = {
'default': None,
'type': str
},
'confirm_text': {
'default': None,
'type': LazyI18nString
},
'mail_prefix': {
'default': None,
'type': str

View File

@@ -166,6 +166,34 @@ to the user. The receivers are expected to return plain text.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
logentry_object_link = EventPluginSignal(
providing_args=["logentry"]
)
"""
To display the relationship of an instance of the ``LogEntry`` model to another model
to a human user, ``pretix.base.signals.logentry_object_link`` will be sent out with a
``logentry`` argument.
The first received response that is not ``None`` will be used to display the related object
to the user. The receivers are expected to return a HTML link. The internal implementation
builds the links like this::
a_text = _('Tax rule {val}')
a_map = {
'href': reverse('control:event.settings.tax.edit', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
'rule': logentry.content_object.id
}),
'val': escape(logentry.content_object.name),
}
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
Make sure that any user content in the HTML code you return is properly escaped!
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
requiredaction_display = EventPluginSignal(
providing_args=["action", "request"]
)
@@ -209,3 +237,37 @@ register_global_settings = django.dispatch.Signal()
All plugins that are installed may send fields for the global settings form, as
an OrderedDict of (setting name, form field).
"""
order_fee_calculation = EventPluginSignal(
providing_args=['request']
)
"""
This signals allows you to add fees to an order while it is being created. You are expected to
return a list of ``OrderFee`` objects that are not yet saved to the database
(because there is no order yet).
As with all plugin signals, the ``sender`` keyword argument will contain the event. A ``positions``
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
tax calculation). The argument ``meta_info`` contains the order's meta dictionary.
"""
order_fee_type_name = EventPluginSignal(
providing_args=['request', 'fee']
)
"""
This signals allows you to return a human-readable description for a fee type based on the ``fee_type``
and ``internal_type`` attributes of the ``OrderFee`` model that you get as keyword arguments. You are
expected to return a string or None, if you don't know about this fee.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
allow_ticket_download = EventPluginSignal(
providing_args=['order']
)
"""
This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false,
a download will not be offered.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""

View File

@@ -56,7 +56,7 @@ ALLOWED_ATTRIBUTES = {
def safelink_callback(attrs, new=False):
url = attrs.get((None, 'href'), '/')
if not is_safe_url(url):
if not is_safe_url(url) and not url.startswith('mailto:'):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'

View File

@@ -32,6 +32,7 @@ class EventSlugBlacklistValidator(BlacklistValidator):
'__debug__',
'api',
'events',
'csp_report',
]
@@ -51,4 +52,5 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
'__debug__',
'about',
'api',
'csp_report',
]

View File

@@ -0,0 +1,24 @@
import json
import logging
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
logger = logging.getLogger('pretix.security.csp')
@csrf_exempt
def csp_report(request):
try:
body = json.loads(request.body.decode())
logger.warning(
'CSP violation at {r[document-uri]}\n'
'Referer: {r[referrer]}\n'
'Blocked: {r[blocked-uri]}\n'
'Violated: {r[violated-directive]}\n'
'Original polity: {r[original-policy]}'.format(r=body['csp-report'])
)
except (ValueError, KeyError) as e:
logger.exception('CSP report failed ' + str(e))
return HttpResponseBadRequest()
return HttpResponse()

View File

@@ -29,14 +29,14 @@ def contextprocessor(request):
'DEBUG': settings.DEBUG,
}
_html_head = []
if hasattr(request, 'event'):
if hasattr(request, 'event') and request.user.is_authenticated:
for receiver, response in html_head.send(request.event, request=request):
_html_head.append(response)
ctx['html_head'] = "".join(_html_head)
_js_payment_weekdays_disabled = '[]'
_nav_event = []
if getattr(request, 'event', None) and hasattr(request, 'organizer'):
if getattr(request, 'event', None) and hasattr(request, 'organizer') and request.user.is_authenticated:
for receiver, response in nav_event.send(request.event, request=request):
_nav_event += response
if request.event.settings.get('payment_term_weekdays'):
@@ -61,15 +61,16 @@ def contextprocessor(request):
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
_nav_global = []
if not hasattr(request, 'event'):
if not hasattr(request, 'event') and request.user.is_authenticated:
for receiver, response in nav_global.send(request, request=request):
_nav_global += response
ctx['nav_global'] = sorted(_nav_global, key=lambda n: n['label'])
_nav_topbar = []
for receiver, response in nav_topbar.send(request, request=request):
_nav_topbar += response
if request.user.is_authenticated:
for receiver, response in nav_topbar.send(request, request=request):
_nav_topbar += response
ctx['nav_topbar'] = sorted(_nav_topbar, key=lambda n: n['label'])
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')

View File

@@ -1,7 +1,9 @@
import os
from django import forms
from django.utils.formats import get_format
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from ...base.forms import I18nModelForm
@@ -98,3 +100,34 @@ class SlugWidget(forms.TextInput):
ctx = super().get_context(name, value, attrs)
ctx['pre'] = self.prefix
return ctx
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
def __init__(self, attrs=None, date_format=None, time_format=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
date_attrs = dict(attrs)
time_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control splitdatetimepart')
time_attrs.setdefault('class', 'form-control splitdatetimepart')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
time_attrs['class'] += ' timepickerfield'
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(df)
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
widgets = (
forms.DateInput(attrs=date_attrs, format=date_format),
forms.TimeInput(attrs=time_attrs, format=time_format),
)
# Skip one hierarchy level
forms.MultiWidget.__init__(self, widgets, attrs)

View File

@@ -12,7 +12,9 @@ from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import ExtFileField, SlugWidget
from pretix.control.forms import (
ExtFileField, SlugWidget, SplitDateTimePickerWidget,
)
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.style import get_fonts
@@ -46,6 +48,8 @@ class EventWizardFoundationForm(forms.Form):
empty_label=None,
required=True
)
if len(self.fields['organizer'].choices) == 1:
self.fields['organizer'].initial = self.fields['organizer'].queryset.first()
class EventWizardBasicsForm(I18nModelForm):
@@ -54,7 +58,7 @@ class EventWizardBasicsForm(I18nModelForm):
}
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
label=_("Event timezone"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
@@ -80,14 +84,18 @@ class EventWizardBasicsForm(I18nModelForm):
'presale_end',
'location',
]
field_classes = {
'date_from': forms.SplitDateTimeField,
'date_to': forms.SplitDateTimeField,
'presale_start': forms.SplitDateTimeField,
'presale_end': forms.SplitDateTimeField,
}
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_basics-date_from'}),
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_basics-presale_start'}),
'slug': SlugWidget
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}),
'slug': SlugWidget,
}
def __init__(self, *args, **kwargs):
@@ -99,6 +107,9 @@ class EventWizardBasicsForm(I18nModelForm):
self.initial['timezone'] = get_current_timezone_name()
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
)
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
if self.has_subevents:
del self.fields['presale_start']
@@ -188,6 +199,9 @@ class EventUpdateForm(I18nModelForm):
super().__init__(*args, **kwargs)
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
)
class Meta:
model = Event
@@ -204,14 +218,19 @@ class EventUpdateForm(I18nModelForm):
'presale_end',
'location',
]
field_classes = {
'date_from': forms.SplitDateTimeField,
'date_to': forms.SplitDateTimeField,
'date_admission': forms.SplitDateTimeField,
'presale_start': forms.SplitDateTimeField,
'presale_end': forms.SplitDateTimeField,
}
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-default': '#id_date_from'}),
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_presale_start'}),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
}
@@ -339,6 +358,14 @@ class EventSettingsForm(SettingsForm):
label=_("Imprint URL"),
required=False,
)
confirm_text = I18nFormField(
label=_('Confirmation text'),
help_text=_('This text needs to be confirmed by the user before a purchase is possible. You could for example '
'link your terms of service here. If you use the Pages feature to publish your terms of service, '
'you don\'t need this setting since you can configure it there.'),
required=False,
widget=I18nTextarea
)
contact_mail = forms.EmailField(
label=_("Contact address"),
required=False,
@@ -366,6 +393,14 @@ class EventSettingsForm(SettingsForm):
})
return data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['confirm_text'].widget.attrs['rows'] = '3'
self.fields['confirm_text'].widget.attrs['placeholder'] = _(
'e.g. I hereby confirm that I have read and agree with the event organizer\'s terms of service '
'and agree with them.'
)
class PaymentSettingsForm(SettingsForm):
payment_term_days = forms.IntegerField(
@@ -517,25 +552,51 @@ class InvoiceSettingsForm(SettingsForm):
choices=[]
)
invoice_address_from = forms.CharField(
widget=forms.Textarea(attrs={'rows': 5}), required=False,
widget=forms.Textarea(attrs={
'rows': 5,
'placeholder': _(
'Sample Event Company\n'
'Albert Einstein Road 52\n'
'12345 Samplecity'
)
}),
required=False,
label=_("Your address"),
help_text=_("Will be printed as the sender on invoices. Be sure to include relevant details required in "
"your jurisdiction (e.g. your VAT ID).")
"your jurisdiction.")
)
invoice_introductory_text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 3,
'placeholder': _(
'e.g. With this document, we sent you the invoice for your ticket order.'
)
}},
required=False,
label=_("Introductory text"),
help_text=_("Will be printed on every invoice above the invoice rows.")
)
invoice_additional_text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 3,
'placeholder': _(
'e.g. Thank you for your purchase! You can find more information on the event at ...'
)
}},
required=False,
label=_("Additional text"),
help_text=_("Will be printed on every invoice below the invoice total.")
)
invoice_footer_text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 5,
'placeholder': _(
'e.g. your bank details, legal details like your VAT ID, registration numbers, etc.'
)
}},
required=False,
label=_("Footer"),
help_text=_("Will be printed centered and in a smaller font at the end of every invoice page.")
@@ -578,7 +639,13 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nTextarea,
help_text=_("This will be attached to every email. Available placeholders: {event}"),
validators=[PlaceholderValidator(['{event}'])]
validators=[PlaceholderValidator(['{event}'])],
widget_kwargs={'attrs': {
'rows': '4',
'placeholder': _(
'e.g. your contact details'
)
}}
)
mail_text_order_placed = I18nFormField(
@@ -683,14 +750,17 @@ class MailSettingsForm(SettingsForm):
)
smtp_host = forms.CharField(
label=_("Hostname"),
required=False
required=False,
widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'})
)
smtp_port = forms.IntegerField(
label=_("Port"),
required=False
required=False,
widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'})
)
smtp_username = forms.CharField(
label=_("Username"),
widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}),
required=False
)
smtp_password = forms.CharField(

View File

@@ -93,10 +93,16 @@ class OrderFilterForm(FilterForm):
else:
qs = qs.filter(status=s)
if fdata.get('ordering'):
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
return qs
class EventOrderFilterForm(OrderFilterForm):
orders = {'code': 'code', 'email': 'email', 'total': 'total',
'datetime': 'datetime', 'status': 'status', 'pcnt': 'pcnt'}
item = forms.ModelChoiceField(
label=_('Products'),
queryset=Item.objects.none(),
@@ -157,6 +163,10 @@ class EventOrderFilterForm(OrderFilterForm):
class OrderSearchFilterForm(OrderFilterForm):
orders = {'code': 'code', 'email': 'email', 'total': 'total',
'datetime': 'datetime', 'status': 'status', 'pcnt': 'pcnt',
'event': 'event'}
organizer = forms.ModelChoiceField(
label=_('Organizer'),
queryset=Organizer.objects.none(),
@@ -187,7 +197,8 @@ class OrderSearchFilterForm(OrderFilterForm):
class SubEventFilterForm(FilterForm):
orders = {
'date_from': 'date_from',
'active': 'active'
'active': 'active',
'sum_quota_available': 'sum_quota_available'
}
status = forms.ChoiceField(
label=_('Status'),
@@ -248,7 +259,8 @@ class EventFilterForm(FilterForm):
'organizer': 'organizer__name',
'date_from': 'order_from',
'date_to': 'order_to',
'live': 'live'
'live': 'live',
'sum_quota_available': 'sum_quota_available'
}
status = forms.ChoiceField(
label=_('Status'),

View File

@@ -12,6 +12,7 @@ from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
)
from pretix.base.models.items import ItemAddOn
from pretix.control.forms import SplitDateTimePickerWidget
class CategoryForm(I18nModelForm):
@@ -149,14 +150,18 @@ class ItemCreateForm(I18nModelForm):
)
if not self.event.has_subevents:
choices = [
(self.NONE, _("Do not add to a quota now")),
(self.EXISTING, _("Add product to an existing quota")),
(self.NEW, _("Create a new quota for this product"))
]
if not self.event.quotas.exists():
choices.remove(choices[1])
self.fields['quota_option'] = forms.ChoiceField(
label=_("Quota options"),
widget=forms.RadioSelect,
choices=(
(self.NONE, _("Do not add to a quota now")),
(self.EXISTING, _("Add product to an existing quota")),
(self.NEW, _("Create a new quota for this product"))
),
choices=choices,
initial=self.NONE,
required=False
)
@@ -178,7 +183,7 @@ class ItemCreateForm(I18nModelForm):
self.fields['quota_add_new_size'] = forms.IntegerField(
min_value=0,
label=_("Size"),
widget=forms.TextInput(attrs={'placeholder': _("New quota size")}),
widget=forms.TextInput(attrs={'placeholder': _("Number of tickets")}),
help_text=_("Leave empty for an unlimited number of tickets."),
required=False
)
@@ -263,6 +268,11 @@ class ItemUpdateForm(I18nModelForm):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
self.fields['description'].widget.attrs['placeholder'] = _(
'e.g. This reduced price is available for full-time students, jobless and people '
'over 65. This ticket includes access to all parts of the event, except the VIP '
'area.'
)
class Meta:
model = Item
@@ -286,9 +296,13 @@ class ItemUpdateForm(I18nModelForm):
'min_per_order',
'checkin_attention'
]
field_classes = {
'available_from': forms.SplitDateTimeField,
'available_until': forms.SplitDateTimeField,
}
widgets = {
'available_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'available_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'available_from': SplitDateTimePickerWidget(),
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
}

View File

@@ -1,6 +1,7 @@
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
@@ -8,6 +9,7 @@ from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Organizer, Team
from pretix.control.forms import ExtFileField
from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
class OrganizerForm(I18nModelForm):
@@ -113,20 +115,6 @@ class TeamForm(forms.ModelForm):
class OrganizerSettingsForm(SettingsForm):
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=forms.CheckboxSelectMultiple,
help_text=_('Choose all languages that your organizer homepage should be available in.')
)
organizer_homepage_text = I18nFormField(
label=_('Homepage text'),
required=False,
widget=I18nTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
organizer_info_text = I18nFormField(
label=_('Info text'),
required=False,
@@ -134,6 +122,23 @@ class OrganizerSettingsForm(SettingsForm):
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
class OrganizerDisplaySettingsForm(SettingsForm):
primary_color = forms.CharField(
label=_("Primary color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
organizer_homepage_text = I18nFormField(
label=_('Homepage text'),
required=False,
widget=I18nTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
organizer_logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
@@ -141,7 +146,6 @@ class OrganizerSettingsForm(SettingsForm):
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. We will show your logo with a maximal height of 120 pixels.')
)
event_list_type = forms.ChoiceField(
label=_('Default overview style'),
choices=(
@@ -149,3 +153,22 @@ class OrganizerSettingsForm(SettingsForm):
('calendar', _('Calendar'))
)
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=forms.CheckboxSelectMultiple,
help_text=_('Choose all languages that your organizer homepage should be available in.')
)
primary_font = forms.ChoiceField(
label=_('Font'),
choices=[
('Open Sans', 'Open Sans')
],
help_text=_('Only respected by modern browsers.')
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['primary_font'].choices += [
(a, a) for a in get_fonts()
]

View File

@@ -5,6 +5,7 @@ from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.control.forms import SplitDateTimePickerWidget
class SubEventForm(I18nModelForm):
@@ -27,13 +28,19 @@ class SubEventForm(I18nModelForm):
'location',
'frontpage_text'
]
field_classes = {
'date_from': forms.SplitDateTimeField,
'date_to': forms.SplitDateTimeField,
'date_admission': forms.SplitDateTimeField,
'presale_start': forms.SplitDateTimeField,
'presale_end': forms.SplitDateTimeField,
}
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_presale_start'}),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
}

View File

@@ -8,6 +8,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, ItemVariation, Quota, Voucher
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.control.signals import voucher_form_validation
@@ -27,8 +28,11 @@ class VoucherForm(I18nModelForm):
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
'comment', 'max_usages', 'price_mode', 'subevent'
]
field_classes = {
'valid_until': forms.SplitDateTimeField,
}
widgets = {
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'valid_until': SplitDateTimePickerWidget(),
}
def __init__(self, *args, **kwargs):
@@ -218,8 +222,11 @@ class VoucherBulkForm(VoucherForm):
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
'max_usages', 'price_mode', 'subevent'
]
field_classes = {
'valid_until': forms.SplitDateTimeField,
}
widgets = {
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'valid_until': SplitDateTimePickerWidget(),
}
labels = {
'max_usages': _('Maximum usages per voucher')

View File

@@ -64,15 +64,17 @@ class PermissionMiddleware(MiddlewareMixin):
return self._login_redirect(request)
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
# If this logic is updated, make sure to also update the logic in pretix/api/auth/permission.py
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return self._login_redirect(request)
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE and url_name != 'user.reauth':
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
if url_name != 'user.reauth':
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
request.session['pretix_auth_last_used'] = int(time.time())
request.session['pretix_auth_last_used'] = int(time.time())
if 'event' in url.kwargs and 'organizer' in url.kwargs:
request.event = Event.objects.filter(

View File

@@ -181,3 +181,32 @@ and your tempalte inherits from ``pretixcontrol/organizers/base.html``.
This is a regular django signal (no pretix event signal). Receivers will be passed
the keyword arguments ``organizer`` and ``request``.
"""
order_info = EventPluginSignal(
providing_args=["order", "request"]
)
"""
This signal is sent out to display additional information on the order detail page
As with all plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
"""
nav_event_settings = EventPluginSignal(
providing_args=['request']
)
"""
This signal is sent out to include tab links on the settings page of an event.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You should also return
an ``active`` key with a boolean set to ``True``, when this item should be marked
as active.
If your linked view should stay in the tab-like context of this page, we recommend
that you use ``pretix.control.views.event.EventSettingsViewMixin`` for your view
and your tempalte inherits from ``pretixcontrol/event/settings_base.html``.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
A second keyword argument ``request`` will contain the request object.
"""

View File

@@ -8,13 +8,13 @@
<a href="{% url 'control:event.index' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.index" %}class="active"{% endif %}>
<i class="fa fa-dashboard fa-fw"></i>
{% trans "Dashboard" %}
{% trans "Event dashboard" %}
</a>
</li>
{% if 'can_change_event_settings' in request.eventpermset %}
<li>
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
{% if is_event_settings or "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
<i class="fa fa-wrench fa-fw"></i>
{% trans "Settings" %}
</a>

View File

@@ -7,6 +7,6 @@
<p>{{ text }}</p>
{% endif %}
{% if button_text %}
<p><a href="{{ button_url }}" class="btn btn-default btn-lg">{{ button_text }}</a></p>
<p><a href="{{ button_url }}" class="btn btn-primary btn-lg">{{ button_text }}</a></p>
{% endif %}
</div>

View File

@@ -1,18 +1,25 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% block inside %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Display settings" %}</legend>
{% bootstrap_field form.primary_color layout="horizontal" %}
{% bootstrap_field form.primary_font layout="horizontal" %}
<legend>{% trans "Event page" %}</legend>
{% bootstrap_field form.logo_image layout="horizontal" %}
{% bootstrap_field form.frontpage_text layout="horizontal" %}
{% bootstrap_field form.show_variations_expanded layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Shop design" %}</legend>
{% url "control:organizer.display" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "primary_color" "primary_font" %}
{% bootstrap_field form.primary_color layout="horizontal" %}
{% bootstrap_field form.primary_font layout="horizontal" %}
{% endpropagated %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -9,10 +9,10 @@
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.date_to layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.date_admission layout="horizontal" %}
{% bootstrap_field form.date_admission layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.is_public layout="horizontal" %}
@@ -45,14 +45,15 @@
{% bootstrap_field sform.show_times layout="horizontal" %}
{% bootstrap_field sform.contact_mail layout="horizontal" %}
{% bootstrap_field sform.imprint_url layout="horizontal" %}
{% bootstrap_field sform.confirm_text layout="horizontal" %}
{% bootstrap_field sform.show_quota_left layout="horizontal" %}
{% bootstrap_field sform.display_net_prices layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_start layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field sform.presale_start_show_date layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field sform.show_items_outside_presale_period layout="horizontal" %}
{% bootstrap_field sform.last_order_modification_date layout="horizontal" %}
</fieldset>

View File

@@ -3,6 +3,30 @@
{% load bootstrap3 %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
{% if "congratulations" in request.GET %}
<div class="thank-you">
<span class="fa fa-check-circle"></span>
<h2>{% trans "Congratulations!" %}</h2>
<p>
<strong>{% trans "You just created an event!" %}</strong>
</p>
<p>
{% blocktrans trimmed %}
You can now scroll down and modify the settings in more detail, if you want, or you can create your
first product to start selling tickets right away!
{% endblocktrans %}
</p>
<p>
<a href="{% url "control:event.items.add" organizer=request.organizer.slug event=request.event.slug %}"
class="btn btn-default">
{% trans "Create a first product" %}
</a>
</p>
<div class="clearfix"></div>
</div>
{% endif %}
<h1>{% trans "Settings" %}</h1>
<ul class="nav nav-pills">
{% if 'can_change_event_settings' in request.eventpermset %}
@@ -52,6 +76,13 @@
</a>
</li>
{% endif %}
{% for nav in nav_event_settings %}
<li {% if nav.active %}class="active"{% endif %}>
<a href="{{ nav.url }}">
{{ nav.label }}
</a>
</li>
{% endfor %}
</ul>
{% block inside %}
{% endblock %}

View File

@@ -18,7 +18,7 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.rate layout="horizontal" %}
{% bootstrap_field form.rate addon_after="%" layout="horizontal" %}
<legend>{% trans "Advanced settings" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-w fa-legal fa-4x pull-left"></span>

View File

@@ -14,15 +14,15 @@
{% block form %}
{% endblock %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save pull-right">
{% trans "Continue" %}
</button>
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
class="btn btn-default btn-lg pull-left">
{% trans "Back" %}
</button>
{% endif %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
</div>
</form>
{% else %}

View File

@@ -29,11 +29,11 @@
</div>
</div>
</div>
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.date_to layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
{% bootstrap_field form.tax_rate addon_after="%" layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>
@@ -43,8 +43,8 @@
{% if form.presale_start %}
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
{% bootstrap_field form.presale_start layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.presale_end layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
</fieldset>
{% endif %}
{% endblock %}

View File

@@ -51,11 +51,6 @@
<th>
{% trans "Event name" %}
</th>
<th>
{% trans "Short form" %}
<a href="?{% url_replace request 'ordering' '-slug' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'slug' %}"><i class="fa fa-caret-up"></i></a>
</th>
{% if not hide_orga %}
<th>
{% trans "Organizer" %}
@@ -67,12 +62,16 @@
{% trans "Start date" %}
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
/
{% trans "End date" %}
<a href="?{% url_replace request 'ordering' '-date_to' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date_to' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Quota available" %}
<a href="?{% url_replace request 'ordering' '-sum_quota_available' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'sum_quota_available' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th class="text-right">
{% trans "Status" %}
<a href="?{% url_replace request 'ordering' '-live' %}"><i class="fa fa-caret-down"></i></a>
@@ -83,26 +82,39 @@
<tbody>
{% for e in events %}
<tr>
<td>
<td class="event-name-col">
<strong><a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
{% if e.has_subevents %}
<span class="label label-default">{% trans "Series" %}</span>
{% endif %}
<br><small>{{ e.slug }}</small>
</td>
<td>{{ e.slug }}</td>
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
<td>
<td class="event-date-col">
{% if e.has_subevents %}
{{ e.min_from|default_if_none:"" }}
{{ e.min_from|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{{ e.get_date_from_display }}
{{ e.get_short_date_from_display }}
{% endif %}
{% if e.has_subevents %}
<span class="label label-default">{% trans "Series" %}</span>
{% endif %}
{% if e.settings.show_date_to and e.date_to %}
<br>
{% if e.has_subevents %}
{{ e.max_fromto|default_if_none:e.max_from|default_if_none:e.max_to|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{{ e.get_short_date_to_display }}
{% endif %}
{% endif %}
</td>
<td>
{% if e.has_subevents %}
{{ e.max_fromto|default_if_none:e.max_from|default_if_none:e.max_to|default_if_none:"" }}
{% else %}
{{ e.get_date_from_display }}
{% for q in e.first_quotas|slice:":3" %}
{% include "pretixcontrol/fragment_quota_box.html" with quota=q %}
{% endfor %}
{% if e.first_quotas|length > 3 %}
<a href="{% url "control:event.items.quotas" organizer=e.organizer.slug event=e.slug %}"
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
data-placement="top">
&middot;&middot;&middot;
</a>
{% endif %}
</td>
<td class="text-right">

View File

@@ -0,0 +1,18 @@
{% load i18n %}
<div class="quotabox" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-100">
</div>
</div>
{% else %}
<div class="progress">
<div class="progress-bar progress-bar-{% if q.cached_avail.0 <= 10 or q.cached_avail.0 >= 100 %}danger{% else %}warning{% endif %} progress-bar-{{ q.inv_percent }}">
</div>
</div>
{% endif %}
<div class="numbers">
{{ q.cached_avail.1|default_if_none:"∞" }} / {{ q.size|default_if_none:"∞" }}
</div>
</div>

View File

@@ -17,14 +17,14 @@
</fieldset>
<fieldset>
<legend>{% trans "Price settings" %}</legend>
{% bootstrap_field form.default_price layout="horizontal" %}
{% bootstrap_field form.default_price addon_after=request.event.currency layout="horizontal" %}
{% bootstrap_field form.tax_rule layout="horizontal" %}
{% bootstrap_field form.free_price layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.available_from layout="horizontal" %}
{% bootstrap_field form.available_until layout="horizontal" %}
{% bootstrap_field form.available_from layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.available_until layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.max_per_order layout="horizontal" %}
{% bootstrap_field form.min_per_order layout="horizontal" %}
{% bootstrap_field form.require_voucher layout="horizontal" %}

View File

@@ -36,7 +36,7 @@
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.active layout='horizontal' %}
{% bootstrap_field form.default_price layout='horizontal' %}
{% bootstrap_field form.default_price addon_after=request.event.currency layout='horizontal' %}
{% bootstrap_field form.description layout='horizontal' %}
</div>
</div>
@@ -69,7 +69,7 @@
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout='horizontal' %}
{% bootstrap_field formset.empty_form.description layout='horizontal' %}
</div>
</div>

View File

@@ -102,7 +102,7 @@
<input name="{{ position.form.prefix }}-operation" type="radio" value="price"
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}>
{% trans "Change price to" %}
{% bootstrap_field position.form.price layout='inline' %}
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
{% if position.apply_tax %}
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %}
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
@@ -150,7 +150,7 @@
{% endif %}
{% bootstrap_field add_form.do layout='horizontal' %}
{% bootstrap_field add_form.itemvar layout='horizontal' %}
{% bootstrap_field add_form.price layout='horizontal' %}
{% bootstrap_field add_form.price addon_after=request.event.currency layout='horizontal' %}
{% if add_form.addon_to %}
{% bootstrap_field add_form.addon_to layout='horizontal' %}
{% endif %}

View File

@@ -3,6 +3,7 @@
{% load bootstrap3 %}
{% load eventurl %}
{% load safelink %}
{% load eventsignal %}
{% block title %}
{% blocktrans trimmed with code=order.code %}
Order details: {{ code }}
@@ -331,6 +332,7 @@
</div>
</div>
</div>
{% eventsignal event "pretix.control.signals.order_info" order=order request=request %}
<div class="row">
<div class="{% if request.event.settings.invoice_address_asked %}col-md-6{% else %}col-md-12{% endif %}">
<div class="panel panel-default items">

View File

@@ -26,6 +26,13 @@
</a>
</li>
{% endif %}
{% if 'can_change_organizer_settings' in request.orgapermset %}
<li {% if "organizer.display" in url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer.display" organizer=organizer.slug %}">
{% trans "Display" %}
</a>
</li>
{% endif %}
{% for nav in nav_organizer %}
<li {% if nav.active %}class="active"{% endif %}>
<a href="{{ nav.url }}">

View File

@@ -0,0 +1,32 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
<legend>{% trans "Organizer page" %}</legend>
{% bootstrap_form_errors form %}
{% bootstrap_field form.locales layout="horizontal" %}
{% bootstrap_field form.organizer_logo_image layout="horizontal" %}
{% bootstrap_field form.organizer_homepage_text layout="horizontal" %}
{% bootstrap_field form.event_list_type layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Shop design" %}</legend>
<p class="help-block">
{% blocktrans trimmed %}
These settings will be used for the organizer page as well as for the default settings
for all events in this account that do not have their own design settings.
{% endblocktrans %}
</p>
{% bootstrap_field form.primary_color layout="horizontal" %}
{% bootstrap_field form.primary_font layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -16,14 +16,6 @@
{% bootstrap_field form.domain layout="horizontal" %}
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>
{% bootstrap_form_errors sform %}
{% bootstrap_field sform.locales layout="horizontal" %}
{% bootstrap_field sform.organizer_logo_image layout="horizontal" %}
{% bootstrap_field sform.organizer_homepage_text layout="horizontal" %}
{% bootstrap_field sform.event_list_type layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Other" %}</legend>
{% bootstrap_form_errors sform %}

View File

@@ -22,10 +22,10 @@
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.active layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.date_to layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.date_admission layout="horizontal" %}
{% bootstrap_field form.date_admission layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.frontpage_text layout="horizontal" %}
{% if meta_forms %}
<div class="form-group metadata-group">
@@ -49,8 +49,8 @@
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
{% bootstrap_field form.presale_start layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.presale_end layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
</fieldset>
<fieldset>
<legend>{% trans "Quotas" %}</legend>

View File

@@ -51,6 +51,11 @@
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Quota available" %}
<a href="?{% url_replace request 'ordering' '-sum_quota_available' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'sum_quota_available' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Status" %}
<a href="?{% url_replace request 'ordering' '-active' %}"><i class="fa fa-caret-down"></i></a>
@@ -67,6 +72,18 @@
{{ s.name }}</a></strong>
</td>
<td>{{ s.get_date_from_display }}</td>
<td>
{% for q in s.first_quotas|slice:":3" %}
{% include "pretixcontrol/fragment_quota_box.html" with quota=q %}
{% endfor %}
{% if s.first_quotas|length > 3 %}
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}"
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
data-placement="top">
&middot;&middot;&middot;
</a>
{% endif %}
</td>
<td>
{% if not s.active %}
<span class="label label-danger">{% trans "Disabled" %}</span>

View File

@@ -38,7 +38,7 @@
</fieldset>
<fieldset>
<legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.valid_until layout="horizontal" %}
{% bootstrap_field form.valid_until layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.block_quota layout="horizontal" %}
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
<div class="form-group">

View File

@@ -26,7 +26,7 @@
<legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.code layout="horizontal" %}
{% bootstrap_field form.max_usages layout="horizontal" %}
{% bootstrap_field form.valid_until layout="horizontal" %}
{% bootstrap_field form.valid_until layout="horizontal" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.block_quota layout="horizontal" %}
{% bootstrap_field form.allow_ignore_quota layout="horizontal" %}
<div class="form-group">

View File

@@ -0,0 +1,68 @@
from django import template
from django.template import Node
from django.utils.translation import ugettext as _
register = template.Library()
class PropagatedNode(Node):
def __init__(self, nodelist, event, field_names, url):
self.nodelist = nodelist
self.event = template.Variable(event)
self.field_names = field_names
self.url = template.Variable(url)
def render(self, context):
event = self.event.resolve(context)
url = self.url.resolve(context)
body = self.nodelist.render(context)
if all([fn not in event.settings._cache() for fn in self.field_names]):
body = """
<div class="propagated-settings-box">
<input type="hidden" name="_settings_ignore" value="{fnames}">
<div class="propagated-settings-form blurred">
{body}
</div>
<div class="propagated-settings-overlay">
<h4><span class="fa fa-link"></span> {text_inh}</h4>
<p>
{text_expl}
</p>
<button class="btn btn-default" name="decouple" value="{fnames}" data-action="unlink">
<span class="fa fa-unlink"></span> {text_unlink}
</button>
<a class="btn btn-default" href="{url}" target="_blank">
<span class="fa fa-group"></span> {text_orga}
</a>
</div>
</div>
""".format(
body=body,
text_inh=_("Organizer-level settings"),
fnames=','.join(self.field_names),
text_expl=_(
'These settings are currently set on organizer level. This way, you can easily change them for '
'all of your events at the same time. You can either go to the organizer settings to change them '
'or decouple them from the organizer account to change them for this event individually.'
),
text_unlink=_('Change only for this event'),
text_orga=_('Change for all events'),
url=url
)
return body
@register.tag
def propagated(parser, token):
try:
tag, event, url, *args = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires at least three arguments" % token.contents.split()[0]
)
nodelist = parser.parse(('endpropagated',))
parser.delete_first_token()
return PropagatedNode(nodelist, event, [f[1:-1] for f in args], url)

View File

@@ -35,6 +35,8 @@ urlpatterns = [
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
name='organizer.display'),
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
url(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),

View File

@@ -152,7 +152,7 @@ def quota_widgets(sender, subevent=None, **kwargs):
widgets = []
for q in sender.quotas.filter(subevent=subevent):
status, left = q.availability()
status, left = q.availability(allow_cache=True)
widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
text=_('{quota} left').format(quota=escape(q.name))),
@@ -333,8 +333,8 @@ def user_event_widgets(**kwargs):
).select_related('organizer')[:100]
for event in events:
dr = event.get_date_range_display()
tz = pytz.timezone(event.settings.timezone)
if event.has_subevents:
tz = pytz.timezone(event.settings.timezone)
dr = daterange(
(event.min_from).astimezone(tz),
(event.max_fromto or event.max_to or event.max_from).astimezone(tz)
@@ -355,9 +355,9 @@ def user_event_widgets(**kwargs):
'content': tpl.format(
event=escape(event.name),
times=_('Event series') if event.has_subevents else (
((date_format(event.date_admission, 'TIME_FORMAT') + ' / ')
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
if event.date_admission and event.date_admission != event.date_from else '')
+ (date_format(event.date_from, 'TIME_FORMAT') if event.date_from else '')
+ (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
),
url=reverse('control:event.index', kwargs={
'event': event.slug,

View File

@@ -39,6 +39,7 @@ from pretix.control.forms.event import (
PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import nav_event_settings
from pretix.helpers.urls import build_absolute_uri
from pretix.presale.style import regenerate_css
@@ -46,6 +47,18 @@ from . import CreateView, UpdateView
from ..logdisplay import OVERVIEW_BLACKLIST
class EventSettingsViewMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['nav_event_settings'] = []
ctx['is_event_settings'] = True
for recv, retv in nav_event_settings.send(sender=self.request.event, request=self.request):
ctx['nav_event_settings'] += retv
ctx['nav_event_settings'].sort(key=lambda n: n['label'])
return ctx
class MetaDataEditorMixin:
meta_form = EventMetaValueForm
meta_model = EventMetaValue
@@ -81,7 +94,7 @@ class MetaDataEditorMixin:
f.delete()
class EventUpdate(EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
class EventUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
model = Event
form_class = EventUpdateForm
template_name = 'pretixcontrol/event/settings.html'
@@ -150,7 +163,7 @@ class EventUpdate(EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView)
return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_event_settings'
@@ -192,6 +205,10 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
if getattr(plugins_available[module], 'restricted', False):
if not request.user.is_superuser:
continue
if hasattr(plugins_available[module].app, 'installed'):
getattr(plugins_available[module].app, 'installed')(self.request.event)
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': module})
if module not in plugins_active:
@@ -213,7 +230,7 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
})
class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
class PaymentSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_event_settings'
@@ -311,11 +328,23 @@ class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
kwargs['obj'] = self.request.event
return kwargs
def _save_decoupled(self, form):
# Save fields that are currently only set via the organizer but should be decoupled
fields = set()
for f in self.request.POST.getlist("decouple"):
fields |= set(f.split(","))
for f in fields:
if f not in form.fields:
continue
if f not in self.request.event.settings._cache():
self.request.event.settings.set(f, self.request.event.settings.get(f))
@transaction.atomic
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
form.save()
self._save_decoupled(form)
if form.has_changed():
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, data={
@@ -332,7 +361,7 @@ class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
return self.get(request)
class InvoiceSettings(EventSettingsFormView):
class InvoiceSettings(EventSettingsViewMixin, EventSettingsFormView):
model = Event
form_class = InvoiceSettingsForm
template_name = 'pretixcontrol/event/invoicing.html'
@@ -360,7 +389,7 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
return resp
class DisplaySettings(EventSettingsFormView):
class DisplaySettings(EventSettingsViewMixin, EventSettingsFormView):
model = Event
form_class = DisplaySettingsForm
template_name = 'pretixcontrol/event/display.html'
@@ -377,6 +406,7 @@ class DisplaySettings(EventSettingsFormView):
form = self.get_form()
if form.is_valid():
form.save()
self._save_decoupled(form)
if form.has_changed():
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, data={
@@ -396,7 +426,7 @@ class DisplaySettings(EventSettingsFormView):
return self.get(request)
class MailSettings(EventSettingsFormView):
class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
model = Event
form_class = MailSettingsForm
template_name = 'pretixcontrol/event/mail.html'
@@ -486,7 +516,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
'mail_text_waiting_list': ['event', 'url', 'product', 'hours', 'code'],
'mail_text_order_canceled': ['code', 'event', 'url'],
'mail_text_order_custom_mail': ['expire_date', 'event', 'code', 'date', 'url',
'invoice_name', 'invoice_company']
'invoice_name', 'invoice_company'],
'mail_text_download_reminder': ['event', 'url']
}
@cached_property
@@ -580,7 +611,7 @@ class TicketSettingsPreview(EventPermissionRequiredMixin, View):
})
class TicketSettings(EventPermissionRequiredMixin, FormView):
class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormView):
model = Event
form_class = TicketSettingsForm
template_name = 'pretixcontrol/event/tickets.html'
@@ -689,7 +720,7 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
return providers
class EventPermissions(EventPermissionRequiredMixin, TemplateView):
class EventPermissions(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/event/permissions.html'
@@ -844,7 +875,7 @@ class EventComment(EventPermissionRequiredMixin, View):
})
class TaxList(EventPermissionRequiredMixin, ListView):
class TaxList(EventSettingsViewMixin, EventPermissionRequiredMixin, ListView):
model = TaxRule
context_object_name = 'taxrules'
paginate_by = 30
@@ -855,7 +886,7 @@ class TaxList(EventPermissionRequiredMixin, ListView):
return self.request.event.tax_rules.all()
class TaxCreate(EventPermissionRequiredMixin, CreateView):
class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView):
model = TaxRule
form_class = TaxRuleForm
template_name = 'pretixcontrol/event/tax_edit.html'
@@ -886,7 +917,7 @@ class TaxCreate(EventPermissionRequiredMixin, CreateView):
return super().form_invalid(form)
class TaxUpdate(EventPermissionRequiredMixin, UpdateView):
class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView):
model = TaxRule
form_class = TaxRuleForm
template_name = 'pretixcontrol/event/tax_edit.html'
@@ -923,7 +954,7 @@ class TaxUpdate(EventPermissionRequiredMixin, UpdateView):
return super().form_invalid(form)
class TaxDelete(EventPermissionRequiredMixin, DeleteView):
class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, DeleteView):
model = TaxRule
template_name = 'pretixcontrol/event/tax_delete.html'
permission = 'can_change_event_settings'

View File

@@ -698,6 +698,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
'id': form.instance.pk
}
)
form.instance.rebuild_cache()
return super().form_valid(form)
def get_success_url(self) -> str:

View File

@@ -1,8 +1,9 @@
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Max, Min
from django.db.models import (
F, IntegerField, Max, Min, OuterRef, Prefetch, Subquery, Sum,
)
from django.db.models.functions import Coalesce, Greatest
from django.http import JsonResponse
from django.shortcuts import redirect
@@ -14,7 +15,7 @@ from django.views.generic import ListView
from formtools.wizard.views import SessionWizardView
from i18nfield.strings import LazyI18nString
from pretix.base.models import Event, Organizer, Team
from pretix.base.models import Event, Organizer, Quota, Team
from pretix.control.forms.event import (
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
)
@@ -43,6 +44,22 @@ class EventList(ListView):
order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to'),
)
sum_quota_available = Quota.objects.filter(
event=OuterRef('pk'), subevent__isnull=True
).order_by().values('event').annotate(
s=Sum('cached_availability_number')
).values(
's'
)
qs = qs.annotate(
sum_quota_available=Subquery(sum_quota_available, output_field=IntegerField())
).prefetch_related(
Prefetch('quotas',
queryset=Quota.objects.filter(subevent__isnull=True).annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
to_attr='first_quotas')
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs
@@ -54,6 +71,18 @@ class EventList(ListView):
pk__in=self.request.user.teams.values_list('organizer', flat=True)
).count()
ctx['hide_orga'] = orga_c <= 1
for s in ctx['events']:
s.first_quotas = s.first_quotas[:4]
for q in s.first_quotas:
q.cached_avail = (
(q.cached_availability_state, q.cached_availability_number)
if q.cached_availability_time is not None
else q.availability(allow_cache=True)
)
if q.cached_avail[1] is not None:
q.percent = round(q.cached_avail[1] / q.size * 100) if q.size > 0 else 0
q.inv_percent = 100 - q.percent
return ctx
@cached_property
@@ -157,12 +186,10 @@ class EventWizard(SessionWizardView):
event.settings.set('locale', basics_data['locale'])
event.settings.set('locales', foundation_data['locales'])
messages.success(self.request, _('The new event has been created. You can now adjust the event settings in '
'detail.'))
return redirect(reverse('control:event.settings', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}))
}) + '?congratulations=1')
class SlugRNG(OrganizerPermissionRequiredMixin, View):

View File

@@ -70,13 +70,6 @@ class OrderList(EventPermissionRequiredMixin, ListView):
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.request.GET.get("ordering", "") != "":
p = self.request.GET.get("ordering", "")
p_admissable = ('-code', 'code', '-email', 'email', '-total', 'total', '-datetime',
'datetime', '-status', 'status', 'pcnt', '-pcnt')
if p in p_admissable:
qs = qs.order_by(p)
return qs.distinct()
def get_context_data(self, **kwargs):
@@ -469,7 +462,7 @@ class OrderExtend(OrderView):
else:
try:
with self.order.event.lock() as now_dt:
is_available = self.order._is_still_available(now_dt)
is_available = self.order._is_still_available(now_dt, count_waitinglist=False)
if is_available is True:
self.form.save()
self.order.status = Order.STATUS_PENDING

View File

@@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import (
CreateView, DeleteView, DetailView, ListView, UpdateView,
CreateView, DeleteView, DetailView, FormView, ListView, UpdateView,
)
from pretix.base.models import Organizer, Team, TeamInvite, User
@@ -18,12 +18,13 @@ from pretix.base.models.event import EventMetaProperty
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import (
EventMetaPropertyForm, OrganizerForm, OrganizerSettingsForm,
OrganizerUpdateForm, TeamForm,
EventMetaPropertyForm, OrganizerDisplaySettingsForm, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
)
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import nav_organizer
from pretix.helpers.urls import build_absolute_uri
from pretix.presale.style import regenerate_organizer_css
class OrganizerList(ListView):
@@ -85,6 +86,71 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
context_object_name = 'organizer'
class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = Organizer
permission = 'can_change_organizer_settings'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['obj'] = self.request.organizer
return kwargs
@transaction.atomic
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
form.save()
if form.has_changed():
self.request.organizer.log_action(
'pretix.organizer.settings', user=self.request.user, data={
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
else form.cleaned_data.get(k))
for k in form.changed_data
}
)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.get(request)
class OrganizerDisplaySettings(OrganizerSettingsFormView):
model = Organizer
form_class = OrganizerDisplaySettingsForm
template_name = 'pretixcontrol/organizers/display.html'
permission = 'can_change_organizer_settings'
def get_success_url(self) -> str:
return reverse('control:organizer.display', kwargs={
'organizer': self.request.organizer.slug,
})
@transaction.atomic
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
form.save()
if form.has_changed():
self.request.organizer.log_action(
'pretix.organizer.settings', user=self.request.user, data={
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
else form.cleaned_data.get(k))
for k in form.changed_data
}
)
regenerate_organizer_css.apply_async(args=(self.request.organizer.pk,))
messages.success(self.request, _('Your changes have been saved. Please note that it can '
'take a short period of time until your changes become '
'active.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.get(request)
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm

View File

@@ -34,11 +34,4 @@ class OrderSearch(ListView):
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.request.GET.get("ordering", "") != "":
p = self.request.GET.get("ordering", "")
p_admissable = ('event', '-event', '-code', 'code', '-email', 'email', '-total', 'total', '-datetime',
'datetime', '-status', 'status', 'pcnt', '-pcnt')
if p in p_admissable:
qs = qs.order_by(p)
return qs.distinct().prefetch_related('event', 'event__organizer')

View File

@@ -3,6 +3,8 @@ import copy
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
from django.db.models.functions import Coalesce
from django.forms import inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.utils.functional import cached_property
@@ -29,7 +31,21 @@ class SubEventList(EventPermissionRequiredMixin, ListView):
permission = 'can_change_settings'
def get_queryset(self):
qs = self.request.event.subevents.all()
sum_quota_available = Quota.objects.filter(
subevent=OuterRef('pk')
).order_by().values('subevent').annotate(
s=Sum('cached_availability_number')
).values(
's'
)
qs = self.request.event.subevents.annotate(
sum_quota_available=Subquery(sum_quota_available, output_field=IntegerField())
).prefetch_related(
Prefetch('quotas',
queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
to_attr='first_quotas')
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs
@@ -37,6 +53,17 @@ class SubEventList(EventPermissionRequiredMixin, ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
for s in ctx['subevents']:
s.first_quotas = s.first_quotas[:4]
for q in s.first_quotas:
q.cached_avail = (
(q.cached_availability_state, q.cached_availability_number)
if q.cached_availability_time is not None
else q.availability(allow_cache=True)
)
if q.cached_avail[1] is not None:
q.percent = round(q.cached_avail[1] / q.size * 100) if q.size > 0 else 0
q.inv_percent = 100 - q.percent
return ctx
@cached_property

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-05 09:55+0000\n"
"POT-Creation-Date: 2017-10-07 16:25+0000\n"
"PO-Revision-Date: 2017-08-27 09:35+0200\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
@@ -18,13 +18,13 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:65
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr "Als bezahlt markiert"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:72
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr "Kommentar:"
@@ -50,117 +50,40 @@ msgstr "Kontaktiere Stripe …"
msgid "QR Code"
msgstr "QR-Code"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:164
msgid "Sample product"
msgstr "Beispielprodukt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:165
msgid "Sample variation"
msgstr "Beispielvariante"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:166
msgid "Sample product sample variation"
msgstr "Beispielprodukt Beispielvariante"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:167
msgid "Sample product description"
msgstr "Beispielproduktbeschreibung"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:168
msgid "123.45 EUR"
msgstr "123,45 EUR"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:169
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:170
msgid "John Doe"
msgstr "Max Mustermann"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:171
msgid "Sample company"
msgstr "Musterfirma GmbH"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:172
msgid "Sample event name"
msgstr "Beispielevent"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:173
msgid "May 31st, 2017"
msgstr "31. Mai 2017"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:174
msgid "May 31st June 4th, 2017"
msgstr "31. Mai 4. Juni 2017"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:175
msgid "20:00"
msgstr "20:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:176
msgid "19:00"
msgstr "19:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:177
msgid "2017-05-31 20:00"
msgstr "31.05.2016 20:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:178
msgid "2017-05-31 19:00"
msgstr "31.05.2016 19:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:179
msgid "Random City"
msgstr "Musterstadt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:180
msgid "Event organizer company"
msgstr "Ausrichtende Firma"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:181
msgid "Event organizer info text"
msgstr "Information zum Veranstalter"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:182
msgid ""
"Addon 1\n"
"Addon 2"
msgstr ""
"Workshop 1\n"
"Workshop 2"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:234
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:210
msgid "The PDF background file could not be loaded for the following reason:"
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:383
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:359
msgid "Group of objects"
msgstr "Gruppe von Objekten"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:389
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:365
msgid "Text object"
msgstr "Text-Objekt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:391
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:367
msgid "Barcode area"
msgstr "QR-Code-Bereich"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:393
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:369
msgid "Object"
msgstr "Objekt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:397
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:373
msgid "Ticket design"
msgstr "Ticket-Design"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:633
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:609
msgid "Saving failed."
msgstr "Speichern fehlgeschlagen."
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:679
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:655
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:693
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:669
msgid "Error while uploading your PDF file, please try again."
msgstr ""
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
@@ -284,6 +207,64 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in Ihrem Warenkorb sind noch {num} Minuten für Sie reserviert."
#~ msgid "Sample product"
#~ msgstr "Beispielprodukt"
#~ msgid "Sample variation"
#~ msgstr "Beispielvariante"
#~ msgid "Sample product sample variation"
#~ msgstr "Beispielprodukt Beispielvariante"
#~ msgid "Sample product description"
#~ msgstr "Beispielproduktbeschreibung"
#~ msgid "123.45 EUR"
#~ msgstr "123,45 EUR"
#~ msgid "John Doe"
#~ msgstr "Max Mustermann"
#~ msgid "Sample company"
#~ msgstr "Musterfirma GmbH"
#~ msgid "Sample event name"
#~ msgstr "Beispielevent"
#~ msgid "May 31st, 2017"
#~ msgstr "31. Mai 2017"
#~ msgid "May 31st June 4th, 2017"
#~ msgstr "31. Mai 4. Juni 2017"
#~ msgid "20:00"
#~ msgstr "20:00"
#~ msgid "19:00"
#~ msgstr "19:00"
#~ msgid "2017-05-31 20:00"
#~ msgstr "31.05.2016 20:00"
#~ msgid "2017-05-31 19:00"
#~ msgstr "31.05.2016 19:00"
#~ msgid "Random City"
#~ msgstr "Musterstadt"
#~ msgid "Event organizer company"
#~ msgstr "Ausrichtende Firma"
#~ msgid "Event organizer info text"
#~ msgstr "Information zum Veranstalter"
#~ msgid ""
#~ "Addon 1\n"
#~ "Addon 2"
#~ msgstr ""
#~ "Workshop 1\n"
#~ "Workshop 2"
#~ msgid ""
#~ "Your request has been queued on the server and will now be processed."
#~ msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-05 09:55+0000\n"
"POT-Creation-Date: 2017-10-07 16:25+0000\n"
"PO-Revision-Date: 2017-08-27 09:35+0200\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
@@ -18,13 +18,13 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:65
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr "Als bezahlt markiert"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:72
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr "Kommentar:"
@@ -50,117 +50,40 @@ msgstr "Kontaktiere Stripe …"
msgid "QR Code"
msgstr "QR-Code"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:164
msgid "Sample product"
msgstr "Beispielprodukt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:165
msgid "Sample variation"
msgstr "Beispielvariante"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:166
msgid "Sample product sample variation"
msgstr "Beispielprodukt Beispielvariante"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:167
msgid "Sample product description"
msgstr "Beispielproduktbeschreibung"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:168
msgid "123.45 EUR"
msgstr "Beispielproduktbeschreibung"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:169
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:170
msgid "John Doe"
msgstr "Max Mustermann"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:171
msgid "Sample company"
msgstr "Musterfirma GmbH"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:172
msgid "Sample event name"
msgstr "Beispielevent"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:173
msgid "May 31st, 2017"
msgstr "31. Mai 2017"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:174
msgid "May 31st June 4th, 2017"
msgstr "31. Mai 4. Juni 2017"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:175
msgid "20:00"
msgstr "20:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:176
msgid "19:00"
msgstr "19:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:177
msgid "2017-05-31 20:00"
msgstr "31.05.2016 20:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:178
msgid "2017-05-31 19:00"
msgstr "31.05.2016 19:00"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:179
msgid "Random City"
msgstr "Musterstadt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:180
msgid "Event organizer company"
msgstr "Ausrichtende Firma"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:181
msgid "Event organizer info text"
msgstr "Information zum Veranstalter"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:182
msgid ""
"Addon 1\n"
"Addon 2"
msgstr ""
"Workshop 1\n"
"Workshop 2"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:234
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:210
msgid "The PDF background file could not be loaded for the following reason:"
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:383
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:359
msgid "Group of objects"
msgstr "Gruppe von Objekten"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:389
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:365
msgid "Text object"
msgstr "Text-Objekt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:391
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:367
msgid "Barcode area"
msgstr "QR-Code-Bereich"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:393
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:369
msgid "Object"
msgstr "Objekt"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:397
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:373
msgid "Ticket design"
msgstr "Ticket-Design"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:633
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:609
msgid "Saving failed."
msgstr "Speichern fehlgeschlagen."
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:679
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:655
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:693
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:669
msgid "Error while uploading your PDF file, please try again."
msgstr ""
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
@@ -284,6 +207,64 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in deinem Warenkorb sind noch {num} Minuten für dich reserviert."
#~ msgid "Sample product"
#~ msgstr "Beispielprodukt"
#~ msgid "Sample variation"
#~ msgstr "Beispielvariante"
#~ msgid "Sample product sample variation"
#~ msgstr "Beispielprodukt Beispielvariante"
#~ msgid "Sample product description"
#~ msgstr "Beispielproduktbeschreibung"
#~ msgid "123.45 EUR"
#~ msgstr "Beispielproduktbeschreibung"
#~ msgid "John Doe"
#~ msgstr "Max Mustermann"
#~ msgid "Sample company"
#~ msgstr "Musterfirma GmbH"
#~ msgid "Sample event name"
#~ msgstr "Beispielevent"
#~ msgid "May 31st, 2017"
#~ msgstr "31. Mai 2017"
#~ msgid "May 31st June 4th, 2017"
#~ msgstr "31. Mai 4. Juni 2017"
#~ msgid "20:00"
#~ msgstr "20:00"
#~ msgid "19:00"
#~ msgstr "19:00"
#~ msgid "2017-05-31 20:00"
#~ msgstr "31.05.2016 20:00"
#~ msgid "2017-05-31 19:00"
#~ msgstr "31.05.2016 19:00"
#~ msgid "Random City"
#~ msgstr "Musterstadt"
#~ msgid "Event organizer company"
#~ msgstr "Ausrichtende Firma"
#~ msgid "Event organizer info text"
#~ msgstr "Information zum Veranstalter"
#~ msgid ""
#~ "Addon 1\n"
#~ "Addon 2"
#~ msgstr ""
#~ "Workshop 1\n"
#~ "Workshop 2"
#~ msgid ""
#~ "Your request has been queued on the server and will now be processed."
#~ msgstr ""

View File

@@ -19,6 +19,18 @@ class BankTransfer(BasePaymentProvider):
form_field = I18nFormField(
label=_('Bank account details'),
widget=I18nTextarea,
help_text=_('Include everything that your customers need to send you a bank transfer payment. Within SEPA '
'countries, IBAN, BIC and account owner should suffice. If you have lots of international '
'customers, they might also need your full address and your bank\'s full address.'),
widget_kwargs={'attrs': {
'rows': '4',
'placeholder': _(
'e.g. IBAN: DE12 1234 5678 8765 4321\n'
'BIC: GENEXAMPLE1\n'
'Account owner: John Doe\n'
'Name of Bank: Professional Banking Institute Ltd., London'
)
}}
)
return OrderedDict(
list(super().settings_form_fields.items()) + [('bank_details', form_field)]

View File

@@ -31,6 +31,9 @@ var bankimport_transactionlist = {
"success": function (data) {
if (data.status == "ok") {
$("tr[data-id=" + id + "]").removeClass("has-error");
if (data.comment) {
bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain);
}
success();
} else {
$("tr[data-id=" + id + "] button").prop("disabled", false);
@@ -66,12 +69,13 @@ var bankimport_transactionlist = {
});
},
comment_reset_to_text: function (id, text) {
comment_reset_to_text: function (id, text, plain) {
var $box = $("tr[data-id=" + id + "] .comment-box");
$box[0].dataset["plain"] = plain;
$box.html("")
.append($("<strong>").text(gettext("Comment:")))
.append(" ")
.append($("<span>").addClass("comment").text(text))
.append($("<span>").addClass("comment").append(" ").append(text))
.append(" ")
.append($("<a>").addClass("comment-modify btn btn-default btn-xs")
.append("<span class='fa fa-edit'></span>"));
@@ -81,7 +85,8 @@ var bankimport_transactionlist = {
var $box = $(e.target).closest("div");
var id = $box.closest("tr").attr("data-id");
var $inp = $("<textarea>").addClass("form-control");
var orig_text = $box.find(".comment").text();
var orig_rendered = $box.find(".comment");
var orig_text = $box[0].dataset.plain;
$inp.val(orig_text);
var $btngrp = $("<div>");
@@ -99,11 +104,10 @@ var bankimport_transactionlist = {
var text = $box.find("textarea").val();
$box.find("input, textarea, button").prop("disabled", true);
bankimport_transactionlist._action(id, "comment:" + text, function () {
bankimport_transactionlist.comment_reset_to_text(id, text);
});
});
$btn2.click(function () {
bankimport_transactionlist.comment_reset_to_text(id, orig_text);
bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text);
});
e.preventDefault();

View File

@@ -1,4 +1,5 @@
{% load i18n %}
{% load rich_text %}
{% load staticfiles %}
<div class="table-responsive">
{% csrf_token %}
@@ -57,9 +58,9 @@
<td>
{{ trans.payer }}<br/>
{{ trans.reference }}
<div class="comment-box">
<div class="comment-box" data-plain="{{ trans.comment }}">
<strong>{% trans "Comment:" %}</strong>
<span class="comment">{{ trans.comment }}</span>
<span class="comment">{{ trans.comment|rich_text }}</span>
<a href="#" class="comment-modify btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>

View File

@@ -106,10 +106,13 @@ class ActionView(View):
return self._retry(trans)
def _comment(self, trans, comment):
from pretix.base.templatetags.rich_text import rich_text
trans.comment = comment
trans.save()
return JsonResponse({
'status': 'ok'
'status': 'ok',
'comment': rich_text(comment),
'plain': comment,
})
def post(self, request, *args, **kwargs):

View File

@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
name='AppConfiguration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, max_length=190, unique=True)),
('key', models.CharField(db_index=True, max_length=190)),
('all_items', models.BooleanField(default=True)),
('allow_search', models.BooleanField(default=True)),
('show_info', models.BooleanField(default=True)),

View File

@@ -7,7 +7,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
class AppConfiguration(models.Model):
event = models.ForeignKey('pretixbase.Event')
key = models.CharField(max_length=190, unique=True, db_index=True)
key = models.CharField(max_length=190, db_index=True)
all_items = models.BooleanField(default=True, verbose_name=_('Can scan all products'))
items = models.ManyToManyField('pretixbase.Item', blank=True, verbose_name=_('Can scan these products'))
subevent = models.ForeignKey('pretixbase.SubEvent', null=True, blank=True,

View File

@@ -23,7 +23,11 @@ class MailForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['subject'] = I18nFormField(
widget=I18nTextInput, required=True,
locales=event.settings.get('locales')
locales=event.settings.get('locales'),
help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}',
'{invoice_name}', '{invoice_company}'])]
)
self.fields['message'] = I18nFormField(
widget=I18nTextarea, required=True,
@@ -39,7 +43,8 @@ class MailForm(forms.Form):
('overdue', _('pending with payment overdue'))
)
self.fields['sendto'] = forms.MultipleChoiceField(
label=_("Send to"), widget=forms.CheckboxSelectMultiple,
label=_("Send to customers with order status"),
widget=forms.CheckboxSelectMultiple,
choices=choices
)
if event.has_subevents:

View File

@@ -78,24 +78,33 @@ class SenderView(EventPermissionRequiredMixin, FormView):
if self.request.POST.get("action") == "preview":
for l in self.request.event.settings.locales:
with language(l):
self.output[l] = []
self.output[l].append(
_('Subject: {subject}').format(subject=form.cleaned_data['subject'].localize(l)))
message = form.cleaned_data['message'].localize(l)
preview_text = message.format(
code='ORDER1234',
event=self.request.event.name,
date=date_format(now(), 'SHORT_DATE_FORMAT'),
expire_date=date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'),
url=build_absolute_uri(self.request.event, 'presale:event.order', kwargs={
context_dict = {
'code': 'ORDER1234',
'event': self.request.event.name,
'date': date_format(now(), 'SHORT_DATE_FORMAT'),
'expire_date': date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'),
'url': build_absolute_uri(self.request.event, 'presale:event.order', kwargs={
'order': 'ORDER1234',
'secret': 'longrandomsecretabcdef123456'
}),
invoice_name=_('John Doe'),
invoice_company=_('Sample Company LLC'),
)
'invoice_name': _('John Doe'),
'invoice_company': _('Sample Company LLC')
}
self.output[l] = []
subject = form.cleaned_data['subject'].localize(l)
preview_subject = subject.format_map(context_dict)
self.output[l].append(
_('Subject: {subject}').format(subject=preview_subject))
message = form.cleaned_data['message'].localize(l)
preview_text = message.format_map(context_dict)
self.output[l].append(preview_text)
return self.get(self.request, *self.args, **self.kwargs)
for o in orders:

View File

@@ -3,7 +3,7 @@ from django.template.loader import get_template
from django.urls import resolve
from pretix.base.signals import (
register_data_exporters, register_ticket_outputs,
EventPluginSignal, register_data_exporters, register_ticket_outputs,
)
from pretix.control.signals import html_head
from pretix.presale.style import ( # NOQA: legacy import
@@ -26,10 +26,29 @@ def register_data(sender, **kwargs):
@receiver(html_head, dispatch_uid="ticketoutputpdf_html_head")
def html_head_presale(sender, request=None, **kwargs):
url = resolve(request.path_info)
if url.namespace == 'plugins:ticketoutputpdf':
if url.namespace == 'plugins:ticketoutputpdf' and getattr(request, 'organizer', None):
template = get_template('pretixplugins/ticketoutputpdf/control_head.html')
return template.render({
'request': request
})
else:
return ""
layout_text_variables = EventPluginSignal()
"""
This signal is sent out to collect variables that can be used to display text in PDF ticket layouts.
Receivers are expected to return a dictionary with globally unique identifiers as keys and more
dictionaries as values that contain keys like in the following example::
return {
"product": {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}
}
The evaluate member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
"""

View File

@@ -158,35 +158,11 @@ var editor = {
editor._update_toolbox_values();
},
text_samples: {
"secret": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
"order": "A1B2C",
"item": gettext("Sample product"),
"variation": gettext("Sample variation"),
"itemvar": gettext("Sample product sample variation"),
"item_description": gettext("Sample product description"),
"price": gettext("123.45 EUR"),
"attendee_name": gettext("John Doe"),
"invoice_name": gettext("John Doe"),
"invoice_company": gettext("Sample company"),
"event_name": gettext("Sample event name"),
"event_date": gettext("May 31st, 2017"),
"event_date_range": gettext("May 31st June 4th, 2017"),
"event_begin_time": gettext("20:00"),
"event_admission_time": gettext("19:00"),
"event_begin": gettext("2017-05-31 20:00"),
"event_admission": gettext("2017-05-31 19:00"),
"event_location": gettext("Random City"),
"organizer": gettext("Event organizer company"),
"organizer_info_text": gettext("Event organizer info text"),
"addons": gettext("Addon 1\nAddon 2"),
},
_get_text_sample: function (key) {
if (key.startsWith('meta:')) {
return key.substr(5);
}
return editor.text_samples[key];
return $('#toolbox-content option[value='+key+']').attr('data-sample') || '';
},
_load_pdf: function (dump) {
@@ -406,7 +382,7 @@ var editor = {
},
_add_text: function () {
var text = new fabric.Textarea(editor.text_samples['item'], {
var text = new fabric.Textarea(editor._get_text_sample('event_name'), {
left: 100,
top: 100,
width: editor._mm2px(50),

View File

@@ -37,11 +37,7 @@
<div class="panel-body">
<div id="editor-canvas-area">
<canvas id="pdf-canvas"
{% if request.event.settings.ticketoutput_pdf_background %}
data-pdf-url="{{ request.event.settings.ticketoutput_pdf_background.url }}"
{% else %}
data-pdf-url="{% static "pretixpresale/pdf/ticket_default_a4.pdf" %}"
{% endif %}
data-pdf-url="{{ pdf }}"
data-worker-url="{% static "pretixplugins/ticketoutputpdf/pdf.worker.js" %}">
</canvas>
@@ -283,27 +279,9 @@
<div class="col-sm-12">
<label>{% trans "Text content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-content">
<option value="secret">{% trans "Ticket code (barcode content)" %}</option>
<option value="order">{% trans "Order code" %}</option>
<option value="item">{% trans "Product name" %}</option>
<option value="variation">{% trans "Variation name" %}</option>
<option value="item_description">{% trans "Product description" %}</option>
<option value="itemvar">{% trans "Product name and variation" %}</option>
<option value="price">{% trans "Price" %}</option>
<option value="attendee_name">{% trans "Attendee name" %}</option>
<option value="event_name">{% trans "Event name" %}</option>
<option value="event_date">{% trans "Event date" %}</option>
<option value="event_date_range">{% trans "Event date range" %}</option>
<option value="event_begin">{% trans "Event begin date and time" %}</option>
<option value="event_begin_time">{% trans "Event begin time" %}</option>
<option value="event_admission">{% trans "Event admission date and time" %}</option>
<option value="event_admission_time">{% trans "Event admission time" %}</option>
<option value="event_location">{% trans "Event location" %}</option>
<option value="invoice_name">{% trans "Invoice address: name" %}</option>
<option value="invoice_company">{% trans "Invoice address: company" %}</option>
<option value="addons">{% trans "List of Add-Ons" %}</option>
<option value="organizer">{% trans "Organizer name" %}</option>
<option value="organizer_info_text">{% trans "Organizer info text" %}</option>
{% for varname, var in variables.items %}
<option data-sample="{{ var.editor_sample }}" value="{{ varname }}">{{ var.label }}</option>
{% endfor %}
{% for p in request.organizer.meta_properties.all %}
<option value="meta:{{ p.name }}">
{% trans "Event attribute:" %} {{ p.name }}

View File

@@ -1,6 +1,7 @@
import copy
import logging
import uuid
from collections import OrderedDict
from io import BytesIO
from django.contrib.staticfiles import finders
@@ -26,11 +27,141 @@ from reportlab.platypus import Paragraph
from pretix.base.models import Order, OrderPosition
from pretix.base.ticketoutput import BaseTicketOutput
from pretix.plugins.ticketoutputpdf.signals import get_fonts
from pretix.plugins.ticketoutputpdf.signals import (
get_fonts, layout_text_variables,
)
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
DEFAULT_VARIABLES = OrderedDict((
("secret", {
"label": _("Ticket code (barcode content)"),
"editor_sample": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
"evaluate": lambda orderposition, order, event: orderposition.secret
}),
("order", {
"label": _("Order code"),
"editor_sample": "A1B2C",
"evaluate": lambda orderposition, order, event: orderposition.order.code
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}),
("variation", {
"label": _("Variation name"),
"editor_sample": _("Sample variation"),
"evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
}),
("item_description", {
"label": _("Product description"),
"editor_sample": _("Sample product sample variation"),
"evaluate": lambda orderposition, order, event: (
'{} - {}'.format(orderposition.item, orderposition.variation)
if orderposition.variation else str(orderposition.item)
)
}),
("itemvar", {
"label": _("Product name and variation"),
"editor_sample": _("Sample product description"),
"evaluate": lambda orderposition, order, event: str(orderposition.item.description)
}),
("price", {
"label": _("Price"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: '{} {}'.format(event.currency, localize(op.price))
}),
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: str(ev.name)
}),
("event_date", {
"label": _("Event date"),
"editor_sample": _("May 31st, 2017"),
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=False)
}),
("event_date_range", {
"label": _("Event date range"),
"editor_sample": _("May 31st June 4th, 2017"),
"evaluate": lambda op, order, ev: ev.get_date_range_display()
}),
("event_begin", {
"label": _("Event begin date and time"),
"editor_sample": _("2017-05-31 20:00"),
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=True)
}),
("event_begin_time", {
"label": _("Event begin time"),
"editor_sample": _("20:00"),
"evaluate": lambda op, order, ev: ev.get_time_from_display()
}),
("event_admission", {
"label": _("Event admission date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT"
) if ev.date_admission else ""
}),
("event_admission_time", {
"label": _("Event admission time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT"
) if ev.date_admission else ""
}),
("event_location", {
"label": _("Event location"),
"editor_sample": _("Random City"),
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n")
}),
("invoice_name", {
"label": _("Invoice address: name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address') else ''
}),
("invoice_company", {
"label": _("Invocie address: company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address') else ''
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Addon 1\nAddon 2"),
"evaluate": lambda op, order, ev: "<br/>".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
for p in op.addons.select_related('item', 'variation')
])
}),
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
"evaluate": lambda op, order, ev: str(order.event.organizer.name)
}),
("organizer_info_text", {
"label": _("Organizer info text"),
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
))
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
for recv, res in layout_text_variables.send(sender=event):
v.update(res)
return v
class PdfTicketOutput(BaseTicketOutput):
identifier = 'pdf'
verbose_name = _('PDF output')
@@ -39,6 +170,7 @@ class PdfTicketOutput(BaseTicketOutput):
def __init__(self, event, override_layout=None, override_background=None):
self.override_layout = override_layout
self.override_background = override_background
self.variables = get_variables(event)
super().__init__(event)
def _register_fonts(self):
@@ -70,62 +202,13 @@ class PdfTicketOutput(BaseTicketOutput):
if o['content'] == 'other':
return o['text'].replace("\n", "<br/>\n")
elif o['content'].startswith('meta:'):
return ev.meta_data.get(o['content'][5:])
elif o['content'] == 'order':
return order.code
elif o['content'] == 'item':
return str(op.item)
elif o['content'] == 'item_description':
return str(op.item.description)
elif o['content'] == 'organizer':
return str(order.event.organizer.name)
elif o['content'] == 'organizer_info_text':
return str(order.event.settings.organizer_info_text)
elif o['content'] == 'secret':
return op.secret
elif o['content'] == 'variation':
return str(op.variation) if op.variation else ''
elif o['content'] == 'itemvar':
return '{} - {}'.format(op.item, op.variation) if op.variation else str(op.item)
elif o['content'] == 'price':
return '{} {}'.format(order.event.currency, localize(op.price))
elif o['content'] == 'attendee_name':
return op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
elif o['content'] == 'event_name':
return str(ev.name)
elif o['content'] == 'event_location':
return str(ev.location).replace("\n", "<br/>\n")
elif o['content'] == 'event_date':
return ev.get_date_from_display(show_times=False)
elif o['content'] == 'event_date_range':
return ev.get_date_range_display()
elif o['content'] == 'event_begin':
return ev.get_date_from_display(show_times=True)
elif o['content'] == 'event_begin_time':
return ev.get_time_from_display()
elif o['content'] == 'event_admission':
if ev.date_admission:
tz = timezone(order.event.settings.timezone)
return date_format(ev.date_admission.astimezone(tz), "SHORT_DATETIME_FORMAT")
elif o['content'] == 'event_admission_time':
if ev.date_admission:
tz = timezone(order.event.settings.timezone)
return date_format(ev.date_admission.astimezone(tz), "TIME_FORMAT")
elif o['content'] == 'invoice_name':
return ev.meta_data.get(o['content'][5:]) or ''
elif o['content'] in self.variables:
try:
return order.invoice_address.name
return self.variables[o['content']]['evaluate'](op, order, ev)
except:
return ""
elif o['content'] == 'invoice_company':
try:
return order.invoice_address.company
except:
return ""
elif o['content'] == 'addons':
return "<br/>".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
for p in op.addons.select_related('item', 'variation')
])
logger.exception('Failed to process variable.')
return '(error)'
return ''
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
@@ -150,7 +233,7 @@ class PdfTicketOutput(BaseTicketOutput):
alignment=align_map[o['align']]
)
p = Paragraph(self._get_text_content(op, order, o), style=style)
p = Paragraph(self._get_text_content(op, order, o) or "", style=style)
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
ad = getAscentDescent(font, float(o['fontsize']))
@@ -198,6 +281,9 @@ class PdfTicketOutput(BaseTicketOutput):
self._register_fonts()
return canvas.Canvas(buffer, pagesize=pagesize)
def _get_default_background(self):
return open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
def _render_with_background(self, buffer, title=_('Ticket')):
from PyPDF2 import PdfFileWriter, PdfFileReader
buffer.seek(0)
@@ -209,7 +295,7 @@ class PdfTicketOutput(BaseTicketOutput):
elif isinstance(bg_file, File):
bgf = default_storage.open(bg_file.name, "rb")
else:
bgf = open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
bgf = self._get_default_background()
bg_pdf = PdfFileReader(bgf)
for page in new_pdf.pages:

View File

@@ -3,6 +3,7 @@ import logging
import mimetypes
from datetime import timedelta
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.core.files import File
from django.core.files.storage import default_storage
from django.http import (
@@ -20,16 +21,15 @@ from pretix.base.models import (
CachedCombinedTicket, CachedFile, CachedTicket, InvoiceAddress,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import ChartContainingView
from pretix.helpers.database import rolledback_transaction
from pretix.plugins.ticketoutputpdf.signals import get_fonts
from .ticketoutput import PdfTicketOutput
from .ticketoutput import PdfTicketOutput, get_variables
logger = logging.getLogger(__name__)
class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
class EditorView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixplugins/ticketoutputpdf/index.html'
permission = 'can_change_settings'
accepted_formats = (
@@ -37,6 +37,15 @@ class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView
)
maxfilesize = 1024 * 1024 * 10
minfilesize = 10
identifier = 'pdf'
def get_output(self, *args, **kwargs):
return PdfTicketOutput(self.request.event, *args, **kwargs)
def get(self, request, *args, **kwargs):
resp = super().get(request, *args, **kwargs)
resp._csp_ignore = True
return resp
def process_upload(self):
f = self.request.FILES.get('background')
@@ -52,6 +61,23 @@ class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView
return error, None
return None, f
def _get_preview_position(self):
item = self.request.event.items.create(name=_("Sample product"), default_price=42.23,
description=_("Sample product description"))
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=23.40)
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
return p
def post(self, request, *args, **kwargs):
if "background" in request.FILES:
error, fileobj = self.process_upload()
@@ -87,25 +113,13 @@ class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView
if "preview" in request.POST:
with rolledback_transaction(), language(request.event.settings.locale):
item = request.event.items.create(name=_("Sample product"), default_price=42.23,
description=_("Sample product description"))
item2 = request.event.items.create(name=_("Sample workshop"), default_price=23.40)
p = self._get_preview_position()
from pretix.base.models import Order
order = request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
prov = PdfTicketOutput(request.event,
override_layout=(json.loads(request.POST.get("data"))
if request.POST.get("data") else None),
override_background=cf.file if cf else None)
prov = self.get_output(
override_layout=(json.loads(request.POST.get("data"))
if request.POST.get("data") else None),
override_background=cf.file if cf else None
)
fname, mimet, data = prov.generate(p)
resp = HttpResponse(data, content_type=mimet)
@@ -114,7 +128,7 @@ class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView
return resp
elif "data" in request.POST:
if cf:
fexisting = request.event.settings.get('ticketoutput_pdf_layout', as_type=File)
fexisting = request.event.settings.get('ticketoutput_{}_layout'.format(self.identifier), as_type=File)
if fexisting:
try:
default_storage.delete(fexisting.name)
@@ -124,18 +138,18 @@ class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView
# Create new file
nonce = get_random_string(length=8)
fname = '%s-%s/%s/%s.%s.%s' % (
'event', 'settings', self.request.event.pk, 'ticketoutput_pdf_layout', nonce, 'pdf'
'event', 'settings', self.request.event.pk, 'ticketoutput_{}_layout'.format(self.identifier), nonce, 'pdf'
)
newname = default_storage.save(fname, cf.file)
request.event.settings.set('ticketoutput_pdf_background', 'file://' + newname)
request.event.settings.set('ticketoutput_{}_background'.format(self.identifier), 'file://' + newname)
request.event.settings.set('ticketoutput_pdf_layout', request.POST.get("data"))
request.event.settings.set('ticketoutput_{}_layout'.format(self.identifier), request.POST.get("data"))
CachedTicket.objects.filter(
order_position__order__event=self.request.event, provider='pdf'
order_position__order__event=self.request.event, provider=self.identifier
).delete()
CachedCombinedTicket.objects.filter(
order__event=self.request.event, provider='pdf'
order__event=self.request.event, provider=self.identifier
).delete()
return JsonResponse({'status': 'ok'})
@@ -143,10 +157,16 @@ class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
prov = PdfTicketOutput(self.request.event)
prov = self.get_output()
ctx['fonts'] = get_fonts()
ctx['pdf'] = (
self.request.event.settings.get('ticketoutput_{}_background'.format(self.identifier)).url
if self.request.event.settings.get('ticketoutput_{}_background'.format(self.identifier))
else static('pretixpresale/pdf/ticket_default_a4.pdf')
)
ctx['variables'] = get_variables(self.request.event)
ctx['layout'] = json.dumps(
self.request.event.settings.get('ticketoutput_pdf_layout', as_type=list)
self.request.event.settings.get('ticketoutput_{}_layout'.format(self.identifier), as_type=list)
or prov._default_layout()
)
return ctx

View File

@@ -11,7 +11,9 @@ from django.views.generic.base import TemplateResponseMixin
from pretix.base.models import Order
from pretix.base.models.orders import InvoiceAddress
from pretix.base.services.cart import set_cart_addons, update_tax_rates
from pretix.base.services.cart import (
get_fees, set_cart_addons, update_tax_rates,
)
from pretix.base.services.orders import perform_order
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import (
@@ -23,6 +25,9 @@ from pretix.presale.signals import (
)
from pretix.presale.views import CartMixin, get_cart, get_cart_total
from pretix.presale.views.async import AsyncAction
from pretix.presale.views.cart import (
cart_session, create_empty_cart_id, get_or_create_cart_id,
)
from pretix.presale.views.questions import QuestionsViewMixin
@@ -80,9 +85,13 @@ class BaseCheckoutFlowStep:
if n:
return n.get_step_url()
@cached_property
def cart_session(self):
return cart_session(self.request)
@cached_property
def invoice_address(self):
iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk))
iapk = self.cart_session.get('invoice_address')
if not iapk:
return InvoiceAddress()
@@ -255,7 +264,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if not is_valid:
return self.get(request, *args, **kwargs)
return self.do(self.request.event.id, data, self.request.session.session_key,
return self.do(self.request.event.id, data, get_or_create_cart_id(self.request),
invoice_address=self.invoice_address.pk)
@@ -270,9 +279,9 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@cached_property
def contact_form(self):
initial = {
'email': self.request.session.get('email', '')
'email': self.cart_session.get('email', '')
}
initial.update(self.request.session.get('contact_form_data', {}))
initial.update(self.cart_session.get('contact_form_data', {}))
return ContactForm(data=self.request.POST if self.request.method == "POST" else None,
event=self.request.event,
initial=initial)
@@ -299,15 +308,15 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
messages.error(request,
_("We had difficulties processing your input. Please review the errors below."))
return self.render()
request.session['email'] = self.contact_form.cleaned_data['email']
self.cart_session['email'] = self.contact_form.cleaned_data['email']
if request.event.settings.invoice_address_asked:
addr = self.invoice_form.save()
request.session['invoice_address_{}'.format(request.event.pk)] = addr.pk
request.session['contact_form_data'] = self.contact_form.cleaned_data
self.cart_session['invoice_address'] = addr.pk
self.cart_session['contact_form_data'] = self.contact_form.cleaned_data
update_tax_rates(
event=request.event,
cart_id=request.session.session_key,
cart_id=get_or_create_cart_id(request),
invoice_address=self.invoice_form.instance
)
@@ -317,11 +326,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
self.request = request
try:
emailval = EmailValidator()
if 'email' not in request.session:
if 'email' not in self.cart_session:
if warn:
messages.warning(request, _('Please enter a valid email address.'))
return False
emailval(request.session.get('email'))
emailval(self.cart_session.get('email'))
except ValidationError:
if warn:
messages.warning(request, _('Please enter a valid email address.'))
@@ -379,7 +388,9 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@cached_property
def _total_order_value(self):
return get_cart_total(self.request)
total = get_cart_total(self.request)
total += sum([f.value for f in get_fees(self.request.event, self.request, total, self.invoice_address, None)])
return total
@cached_property
def provider_forms(self):
@@ -400,7 +411,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
self.request = request
for p in self.provider_forms:
if p['provider'].identifier == request.POST.get('payment', ''):
request.session['payment'] = p['provider'].identifier
self.cart_session['payment'] = p['provider'].identifier
resp = p['provider'].checkout_prepare(
request,
self.get_cart()
@@ -418,16 +429,18 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx = super().get_context_data(**kwargs)
ctx['providers'] = self.provider_forms
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
ctx['selected'] = self.request.POST.get('payment', self.request.session.get('payment', ''))
ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', ''))
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
return ctx
@cached_property
def payment_provider(self):
return self.request.event.get_payment_providers().get(self.request.session['payment'])
return self.request.event.get_payment_providers().get(self.cart_session['payment'])
def is_completed(self, request, warn=False):
self.request = request
if 'payment' not in request.session or not self.payment_provider:
if 'payment' not in self.cart_session or not self.payment_provider:
if warn:
messages.error(request, _('The payment information you entered was incomplete.'))
return False
@@ -442,7 +455,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
def is_applicable(self, request):
self.request = request
if self._total_order_value == 0:
request.session['payment'] = 'free'
self.cart_session['payment'] = 'free'
return False
return True
@@ -472,7 +485,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
responses = contact_form_fields.send(self.event)
for r, response in sorted(responses, key=lambda r: str(r[0])):
for key, value in response.items():
v = self.request.session.get('contact_form_data', {}).get(key)
v = self.cart_session.get('contact_form_data', {}).get(key)
if v is True:
v = _('Yes')
elif v is False:
@@ -491,7 +504,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
@cached_property
def payment_provider(self):
return self.request.event.get_payment_providers().get(self.request.session['payment'])
return self.request.event.get_payment_providers().get(self.cart_session['payment'])
def get(self, request):
self.request = request
@@ -516,16 +529,17 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
return redirect(self.get_error_url())
meta_info = {
'contact_form_data': self.request.session.get('contact_form_data', {})
'contact_form_data': self.cart_session.get('contact_form_data', {})
}
for receiver, response in order_meta_from_request.send(sender=request.event, request=request):
meta_info.update(response)
return self.do(self.request.event.id, self.payment_provider.identifier,
[p.id for p in self.positions], request.session.get('email'),
[p.id for p in self.positions], self.cart_session.get('email'),
translation.get_language(), self.invoice_address.pk, meta_info)
def get_success_message(self, value):
create_empty_cart_id(self.request)
return None
def get_success_url(self, value):

View File

@@ -57,6 +57,8 @@ def contextprocessor(request):
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales]
if hasattr(request, 'organizer'):
if request.organizer.settings.presale_css_file and not hasattr(request, 'event'):
ctx['css_file'] = default_storage.url(request.organizer.settings.presale_css_file)
ctx['organizer_logo'] = request.organizer.settings.get('organizer_logo_image', as_type=str, default='')[7:]
ctx['organizer_homepage_text'] = request.organizer.settings.get('organizer_homepage_text', as_type=LazyI18nString)
ctx['organizer'] = request.organizer

View File

@@ -49,7 +49,7 @@ class ContactForm(forms.Form):
self.fields[key] = value
def clean(self):
if self.event.settings.order_email_asked_twice:
if self.event.settings.order_email_asked_twice and self.cleaned_data.get('email') and self.cleaned_data.get('email_repeat'):
if self.cleaned_data.get('email').lower() != self.cleaned_data.get('email_repeat').lower():
raise ValidationError(_('Please enter the same email address twice.'))

View File

@@ -68,6 +68,28 @@ You will recieve the request triggering the order creation as the ``request`` ke
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
checkout_confirm_page_content = EventPluginSignal(
providing_args=['request']
)
"""
This signals allows you to add HTML content to the confirmation page that is presented at the
end of the checkout process, just before the order is being created.
As with all plugin signals, the ``sender`` keyword argument will contain the event. A ``request``
argument will contain the request object.
"""
fee_calculation_for_cart = EventPluginSignal(
providing_args=['request']
)
"""
This signals allows you to add fees to a cart. You are expected to return a list of ``OrderFee``
objects that are not yet saved to the database (because there is no order yet).
As with all plugin signals, the ``sender`` keyword argument will contain the event. A ``request``
argument will contain the request object and ``invoice_address`` the invoice address (useful for
tax calculation).
"""
contact_form_fields = EventPluginSignal(
providing_args=[]

View File

@@ -11,23 +11,22 @@ from django.core.files.storage import default_storage
from django.dispatch import Signal
from django.templatetags.static import static as _static
from pretix.base.models import Event
from pretix.base.models import Event, Event_SettingsStore, Organizer
from pretix.base.services.async import ProfiledTask
from pretix.celery_app import app
from pretix.multidomain.urlreverse import get_domain
logger = logging.getLogger('pretix.presale.style')
affected_keys = ['primary_font', 'primary_color']
@app.task(base=ProfiledTask)
def regenerate_css(event_id: int):
event = Event.objects.select_related('organizer').get(pk=event_id)
def compile_scss(object):
sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss')
def static(path):
sp = _static(path)
if not settings.MEDIA_URL.startswith("/") and sp.startswith("/"):
domain = get_domain(event.organizer)
domain = get_domain(object.organizer if isinstance(object, Event) else object)
if domain:
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
@@ -38,15 +37,16 @@ def regenerate_css(event_id: int):
return '"{}"'.format(sp)
sassrules = []
if event.settings.get('primary_color'):
sassrules.append('$brand-primary: {};'.format(event.settings.get('primary_color')))
if object.settings.get('primary_color'):
sassrules.append('$brand-primary: {};'.format(object.settings.get('primary_color')))
font = event.settings.get('primary_font')
font = object.settings.get('primary_font')
if font != 'Open Sans':
sassrules.append(get_font_stylesheet(font))
sassrules.append('$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default'.format(
font
))
sassrules.append(
'$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default'.format(
font
))
sassrules.append('@import "main.scss";')
@@ -58,6 +58,14 @@ def regenerate_css(event_id: int):
custom_functions=cf
)
checksum = hashlib.sha1(css.encode('utf-8')).hexdigest()
return css, checksum
@app.task(base=ProfiledTask)
def regenerate_css(event_id: int):
event = Event.objects.select_related('organizer').get(pk=event_id)
css, checksum = compile_scss(event)
fname = '{}/{}/presale.{}.css'.format(
event.organizer.slug, event.slug, checksum[:16]
)
@@ -68,6 +76,28 @@ def regenerate_css(event_id: int):
event.settings.set('presale_css_checksum', checksum)
@app.task(base=ProfiledTask)
def regenerate_organizer_css(organizer_id: int):
organizer = Organizer.objects.get(pk=organizer_id)
css, checksum = compile_scss(organizer)
fname = '{}/presale.{}.css'.format(
organizer.slug, checksum[:16]
)
if organizer.settings.get('presale_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
organizer.settings.set('presale_css_file', newname)
organizer.settings.set('presale_css_checksum', checksum)
non_inherited_events = set(Event_SettingsStore.objects.filter(
object__organizer=organizer, key__in=affected_keys
).values_list('object_id', flat=True))
for event in organizer.events.all():
if event.pk not in non_inherited_events:
regenerate_css.apply_async(args=(event.pk,))
register_fonts = Signal()
"""
Return a dictionaries of the following structure. Paths should be relative to static root.

View File

@@ -40,7 +40,7 @@
{% if request.event.settings.locales|length > 1 %}
<div class="locales">
{% for l in languages %}
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}%3F{{ request.META.QUERY_STRING|urlencode }}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}">
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}%3F{{ request.META.QUERY_STRING|urlencode }}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
{{ l.name_local }}
</a>
{% endfor %}

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load eventurl %}
{% load eventsignal %}
{% block title %}{% trans "Confirm order" %}{% endblock %}
{% block content %}
<h2>{% trans "Confirm order" %}</h2>
@@ -57,6 +58,7 @@
{{ payment }}
</div>
</div>
{% eventsignal event "pretix.presale.signals.checkout_confirm_page_content" request=request %}
<div class="row">
{% if request.event.settings.invoice_address_asked %}
<div class="col-md-6 col-xs-12">

View File

@@ -60,7 +60,8 @@
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-default btn-sm" data-asyncdownload>
class="btn btn-default btn-sm {% if b.identifier == "pdf" %}btn-primary{% endif %}"
data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
@@ -146,7 +147,8 @@
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-default btn-sm" data-asyncdownload>
class="btn btn-default btn-sm {% if b.identifier == "pdf" %}btn-primary{% endif %}"
data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
@@ -159,7 +161,7 @@
{% for fee in cart.fees %}
<div class="row cart-row">
<div class="col-md-4 col-xs-6">
<strong>{% trans "Payment method fee" %}</strong>
<strong>{{ fee.get_fee_type_display }}</strong>
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
{% if event.settings.display_net_prices %}

View File

@@ -79,7 +79,8 @@
{% for b in download_buttons %}
{% if b.multi %}
<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>
class="btn btn-default btn-sm {% if b.identifier == "pdf" %}btn-primary{% endif %}"
data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endif %}

View File

@@ -6,6 +6,7 @@ import pretix.presale.views.event
import pretix.presale.views.locale
import pretix.presale.views.order
import pretix.presale.views.organizer
import pretix.presale.views.robots
import pretix.presale.views.user
import pretix.presale.views.waiting
@@ -83,4 +84,5 @@ organizer_patterns = [
locale_patterns = [
url(r'^locale/set$', pretix.presale.views.locale.LocaleSet.as_view(), name='locale.set'),
url(r'^robots.txt$', pretix.presale.views.robots.robots_txt, name='robots.txt'),
]

View File

@@ -88,7 +88,8 @@ def _detect_event(request, require_live=True, require_plugin=None):
raise PermissionDenied(_('The selected ticket shop is currently not available.'))
if require_plugin:
if require_plugin not in request.event.get_plugins():
is_core = any(require_plugin.startswith(m) for m in settings.CORE_MODULES)
if require_plugin not in request.event.get_plugins() and not is_core:
raise Http404(_('This feature is not enabled.'))
for receiver, response in process_request.send(request.event, request=request):

View File

@@ -19,6 +19,11 @@ class CartMixin:
"""
return list(get_cart(self.request))
@cached_property
def cart_session(self):
from pretix.presale.views.cart import cart_session
return cart_session(self.request)
def get_cart(self, answers=False, queryset=None, order=None, downloads=False):
if queryset:
prefetch = []
@@ -102,7 +107,7 @@ class CartMixin:
if order:
fees = order.fees.all()
else:
iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk))
iapk = self.cart_session.get('invoice_address')
ia = None
if iapk:
try:
@@ -110,7 +115,7 @@ class CartMixin:
except InvoiceAddress.DoesNotExist:
pass
fees = get_fees(self.request.event, total, ia, self.request.session.get('payment'))
fees = get_fees(self.request.event, self.request, total, ia, self.cart_session.get('payment'))
total += sum([f.value for f in fees])
net_total += sum([f.net_value for f in fees])
@@ -137,9 +142,11 @@ class CartMixin:
def get_cart(request):
from pretix.presale.views.cart import get_or_create_cart_id
if not hasattr(request, '_cart_cache'):
request._cart_cache = CartPosition.objects.filter(
cart_id=request.session.session_key, event=request.event
cart_id=get_or_create_cart_id(request), event=request.event
).order_by(
'item', 'variation'
).select_related(
@@ -152,13 +159,15 @@ def get_cart(request):
def get_cart_total(request):
from pretix.presale.views.cart import get_or_create_cart_id
if not hasattr(request, '_cart_total_cache'):
if hasattr(request, '_cart_cache'):
request._cart_total_cache = sum(i.price for i in request._cart_cache)
else:
request._cart_total_cache = CartPosition.objects.filter(
cart_id=request.session.session_key, event=request.event
).aggregate(sum=Sum('price'))['sum']
cart_id=get_or_create_cart_id(request), event=request.event
).aggregate(sum=Sum('price'))['sum'] or 0
return request._cart_total_cache

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