mirror of
https://github.com/pretix/pretix.git
synced 2026-02-20 09:02:27 +00:00
Compare commits
48 Commits
release/1.
...
new-placeh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece1b21298 | ||
|
|
8accaae6b1 | ||
|
|
d4259501af | ||
|
|
fd5d5ae98e | ||
|
|
457901ff82 | ||
|
|
e201be1c65 | ||
|
|
acde14372d | ||
|
|
79988a2325 | ||
|
|
784f6e703c | ||
|
|
29b157f287 | ||
|
|
c030bd35ca | ||
|
|
06fe076ce2 | ||
|
|
ae6cba067c | ||
|
|
72ae19a95d | ||
|
|
1f889be07a | ||
|
|
39061b659a | ||
|
|
d38f29ac7c | ||
|
|
1a8e67f4de | ||
|
|
8265c302ad | ||
|
|
110d7c6acf | ||
|
|
244b767f8f | ||
|
|
f40950efc9 | ||
|
|
0e0534c273 | ||
|
|
9b3ea3656f | ||
|
|
62b2a367ff | ||
|
|
ab9dd32902 | ||
|
|
43fc498297 | ||
|
|
ef3eee7873 | ||
|
|
9f0deea9dd | ||
|
|
e3798600ed | ||
|
|
00834cd5e0 | ||
|
|
ed35c4f74e | ||
|
|
9cd3e2d494 | ||
|
|
3345f48986 | ||
|
|
b611d63975 | ||
|
|
fb3866aa1a | ||
|
|
a9f131b645 | ||
|
|
e5728662c5 | ||
|
|
94a97fb0fd | ||
|
|
b5bea6fe7a | ||
|
|
fb9d677d76 | ||
|
|
7c4fc7bd0d | ||
|
|
de992cecf3 | ||
|
|
cd94549606 | ||
|
|
214a6eb5ce | ||
|
|
db5f0aa02d | ||
|
|
ba48ab3659 | ||
|
|
d1538e07d3 |
@@ -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
|
||||
--------------
|
||||
|
||||
@@ -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, fee_calculation_for_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, 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
|
||||
|
||||
@@ -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
|
||||
-----
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.7.0"
|
||||
__version__ = "1.8.0.dev0"
|
||||
|
||||
@@ -2,12 +2,15 @@ import logging
|
||||
from smtplib import SMTPRecipientsRefused, SMTPSenderRefused
|
||||
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.base.validators import PlaceholderValidator
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
|
||||
class CustomSMTPBackend(EmailBackend):
|
||||
|
||||
def test(self, from_addr):
|
||||
try:
|
||||
self.open()
|
||||
@@ -24,3 +27,72 @@ class CustomSMTPBackend(EmailBackend):
|
||||
raise SMTPRecipientsRefused(senderrs)
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
|
||||
class MailTemplateRenderer:
|
||||
def __init__(self, placeholders: list):
|
||||
self.placeholders = placeholders
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'required': False,
|
||||
'widget': I18nTextarea,
|
||||
'validators': [],
|
||||
'help_text': ''
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
if defaults['help_text']:
|
||||
defaults['help_text'] += ' '
|
||||
defaults['help_text'] += _('Available placeholders: {list}').format(
|
||||
list=', '.join(['{' + v + '}' for v in self.placeholders])
|
||||
)
|
||||
defaults['validators'].append(PlaceholderValidator(['{' + v + '}' for v in self.placeholders]))
|
||||
return I18nFormField(**defaults)
|
||||
|
||||
def preview(self, text, **kwargs):
|
||||
return text.format(**kwargs)
|
||||
|
||||
def render(self, text, values):
|
||||
set_placeholders = set(values.keys())
|
||||
expected_palceholders = set(self.placeholders)
|
||||
if set_placeholders != expected_palceholders:
|
||||
raise ValueError('Invalid placeholder set. Unknown placeholders: {}. Missing placeholders: {}'.format(
|
||||
set_placeholders - expected_palceholders, expected_palceholders - set_placeholders
|
||||
))
|
||||
|
||||
return text.format_map(values)
|
||||
|
||||
|
||||
mail_text_order_placed = MailTemplateRenderer(
|
||||
['event', 'total', 'currency', 'date', 'payment_info', 'url', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
mail_text_order_paid = MailTemplateRenderer(
|
||||
['event', 'url', 'invoice_name', 'invoice_company', 'payment_info']
|
||||
)
|
||||
mail_text_order_free = MailTemplateRenderer(
|
||||
['event', 'url', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
mail_text_order_changed = MailTemplateRenderer(
|
||||
['event', 'url', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
mail_text_resend_link = MailTemplateRenderer(
|
||||
['event', 'url', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
mail_text_resend_all_links = MailTemplateRenderer(
|
||||
['event', 'orders']
|
||||
)
|
||||
mail_text_order_expire_warning = MailTemplateRenderer(
|
||||
['event', 'url', 'expire_date', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
mail_text_waiting_list = MailTemplateRenderer(
|
||||
['event', 'url', 'product', 'hours', 'code']
|
||||
)
|
||||
mail_text_order_canceled = MailTemplateRenderer(
|
||||
['event', 'url', 'code']
|
||||
)
|
||||
mail_text_order_custom_mail = MailTemplateRenderer(
|
||||
['expire_date', 'event', 'code', 'date', 'url', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
mail_text_download_reminder = MailTemplateRenderer(
|
||||
['event', 'url']
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,8 +9,6 @@ from hierarkey.forms import HierarkeyForm
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
|
||||
from .validators import PlaceholderValidator # NOQA
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
||||
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
|
||||
class PlaceholderValidator(BaseValidator):
|
||||
"""
|
||||
Takes list of allowed placeholders,
|
||||
validates form field by checking for placeholders,
|
||||
which are not presented in taken list.
|
||||
"""
|
||||
|
||||
def __init__(self, limit_value):
|
||||
super().__init__(limit_value)
|
||||
self.limit_value = limit_value
|
||||
|
||||
def __call__(self, value):
|
||||
if isinstance(value, LazyI18nString):
|
||||
for l, v in value.data.items():
|
||||
self.__call__(v)
|
||||
return
|
||||
|
||||
data_placeholders = list(re.findall(r'({[\w\s]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
if placeholder not in self.limit_value:
|
||||
invalid_placeholders.append(placeholder)
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
code='invalid',
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
|
||||
def clean(self, x):
|
||||
return x
|
||||
@@ -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
@@ -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 - Pa’anga'), ('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')},
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -149,7 +149,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',
|
||||
@@ -591,7 +593,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -209,6 +209,10 @@ DEFAULTS = {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'confirm_text': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'mail_prefix': {
|
||||
'default': None,
|
||||
'type': str
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
|
||||
class PlaceholderValidator(BaseValidator):
|
||||
"""
|
||||
Takes list of allowed placeholders,
|
||||
validates form field by checking for placeholders,
|
||||
which are not presented in taken list.
|
||||
"""
|
||||
|
||||
def __init__(self, limit_value):
|
||||
super().__init__(limit_value)
|
||||
self.limit_value = limit_value
|
||||
|
||||
def __call__(self, value):
|
||||
if isinstance(value, LazyI18nString):
|
||||
for l, v in value.data.items():
|
||||
self.__call__(v)
|
||||
return
|
||||
|
||||
data_placeholders = list(re.findall(r'({[\w\s]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
if placeholder not in self.limit_value:
|
||||
invalid_placeholders.append(placeholder)
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
code='invalid',
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
|
||||
def clean(self, x):
|
||||
return x
|
||||
|
||||
|
||||
class BlacklistValidator:
|
||||
@@ -32,6 +68,7 @@ class EventSlugBlacklistValidator(BlacklistValidator):
|
||||
'__debug__',
|
||||
'api',
|
||||
'events',
|
||||
'csp_report',
|
||||
]
|
||||
|
||||
|
||||
@@ -51,4 +88,5 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
|
||||
'__debug__',
|
||||
'about',
|
||||
'api',
|
||||
'csp_report',
|
||||
]
|
||||
|
||||
24
src/pretix/base/views/csp.py
Normal file
24
src/pretix/base/views/csp.py
Normal 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()
|
||||
@@ -8,13 +8,15 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
from pytz import common_timezones, timezone
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||
from pretix.base.forms import I18nModelForm, 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.base.validators import PlaceholderValidator
|
||||
from pretix.control.forms import ExtFileField, SlugWidget
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.style import get_fonts
|
||||
from pretix.base import email
|
||||
|
||||
|
||||
class EventWizardFoundationForm(forms.Form):
|
||||
@@ -339,6 +341,14 @@ class EventSettingsForm(SettingsForm):
|
||||
label=_("Imprint URL"),
|
||||
required=False,
|
||||
)
|
||||
confirm_text = forms.CharField(
|
||||
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=forms.Textarea(attrs={"rows": 3})
|
||||
)
|
||||
contact_mail = forms.EmailField(
|
||||
label=_("Contact address"),
|
||||
required=False,
|
||||
@@ -572,7 +582,6 @@ class MailSettingsForm(SettingsForm):
|
||||
label=_("Sender address"),
|
||||
help_text=_("Sender address for outgoing emails")
|
||||
)
|
||||
|
||||
mail_text_signature = I18nFormField(
|
||||
label=_("Signature"),
|
||||
required=False,
|
||||
@@ -580,50 +589,29 @@ class MailSettingsForm(SettingsForm):
|
||||
help_text=_("This will be attached to every email. Available placeholders: {event}"),
|
||||
validators=[PlaceholderValidator(['{event}'])]
|
||||
)
|
||||
|
||||
mail_text_order_placed = I18nFormField(
|
||||
mail_text_order_placed = email.mail_text_order_placed.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {payment_info}, {url}, "
|
||||
"{invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{payment_info}',
|
||||
'{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_order_paid = I18nFormField(
|
||||
mail_text_order_paid = email.mail_text_order_paid.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}', '{payment_info}'])]
|
||||
)
|
||||
mail_text_order_free = I18nFormField(
|
||||
mail_text_order_free = email.mail_text_order_free.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_order_changed = I18nFormField(
|
||||
mail_text_order_changed = email.mail_text_order_changed.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_resend_link = I18nFormField(
|
||||
mail_text_resend_link = email.mail_text_resend_link.formfield(
|
||||
label=_("Text (sent by admin)"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_resend_all_links = I18nFormField(
|
||||
mail_text_resend_all_links = email.mail_text_resend_all_links.formfield(
|
||||
label=_("Text (requested by user)"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {orders}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{orders}'])]
|
||||
)
|
||||
mail_days_order_expire_warning = forms.IntegerField(
|
||||
label=_("Number of days"),
|
||||
@@ -632,42 +620,25 @@ class MailSettingsForm(SettingsForm):
|
||||
help_text=_("This email will be sent out this many days before the order expires. If the "
|
||||
"value is 0, the mail will never be sent.")
|
||||
)
|
||||
mail_text_order_expire_warning = I18nFormField(
|
||||
mail_text_order_expire_warning = email.mail_text_order_expire_warning.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}', '{expire_date}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_waiting_list = I18nFormField(
|
||||
mail_text_waiting_list = email.mail_text_waiting_list.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}, {product}, {hours}, {code}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}', '{product}', '{hours}', '{code}'])]
|
||||
)
|
||||
mail_text_order_canceled = I18nFormField(
|
||||
mail_text_order_canceled = email.mail_text_order_canceled.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {code}, {url}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{code}', '{url}'])]
|
||||
)
|
||||
mail_text_order_custom_mail = I18nFormField(
|
||||
mail_text_order_custom_mail = email.mail_text_order_custom_mail.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
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}'])]
|
||||
)
|
||||
mail_text_download_reminder = I18nFormField(
|
||||
mail_text_download_reminder = email.mail_text_download_reminder.formfield(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{url}'])]
|
||||
)
|
||||
mail_days_download_reminder = forms.IntegerField(
|
||||
label=_("Number of days"),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -6,12 +6,14 @@ from django.utils.formats import localize
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.email import mail_text_order_custom_mail
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Item, ItemAddOn, Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.validators import PlaceholderValidator
|
||||
|
||||
|
||||
class ExtendForm(I18nModelForm):
|
||||
@@ -269,13 +271,9 @@ class OrderMailForm(forms.Form):
|
||||
initial=order.email
|
||||
)
|
||||
self.fields['sendto'].widget.attrs['readonly'] = 'readonly'
|
||||
self.fields['message'] = forms.CharField(
|
||||
self.fields['message'] = mail_text_order_custom_mail.formfield(
|
||||
label=_("Message"),
|
||||
required=True,
|
||||
widget=forms.Textarea,
|
||||
initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale),
|
||||
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}'])]
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% 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>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
{% 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>
|
||||
|
||||
@@ -52,6 +52,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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
@@ -332,7 +349,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 +377,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'
|
||||
@@ -396,7 +413,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'
|
||||
@@ -580,7 +597,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 +706,7 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
||||
return providers
|
||||
|
||||
|
||||
class EventPermissions(EventPermissionRequiredMixin, TemplateView):
|
||||
class EventPermissions(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/event/permissions.html'
|
||||
|
||||
|
||||
@@ -844,7 +861,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 +872,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 +903,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 +940,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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-09-05 09:55+0000\n"
|
||||
"POT-Creation-Date: 2017-09-27 11:21+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
@@ -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-09-27 11:21+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 ""
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
6
src/pretix/plugins/sendmail/email.py
Normal file
6
src/pretix/plugins/sendmail/email.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from pretix.base.email import MailTemplateRenderer
|
||||
|
||||
mail_text_sendmail = MailTemplateRenderer(
|
||||
['expire_date', 'event', 'code', 'date', 'url', 'invoice_name', 'invoice_company']
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@ from django import forms
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.plugins.sendmail.email import mail_text_sendmail
|
||||
|
||||
|
||||
class MailForm(forms.Form):
|
||||
@@ -25,13 +25,9 @@ class MailForm(forms.Form):
|
||||
widget=I18nTextInput, required=True,
|
||||
locales=event.settings.get('locales')
|
||||
)
|
||||
self.fields['message'] = I18nFormField(
|
||||
self.fields['message'] = mail_text_sendmail.formfield(
|
||||
widget=I18nTextarea, required=True,
|
||||
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}'])]
|
||||
)
|
||||
choices = list(Order.STATUS_CHOICE)
|
||||
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
|
||||
|
||||
@@ -17,6 +17,7 @@ from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.sendmail.email import mail_text_sendmail
|
||||
|
||||
from . import forms
|
||||
|
||||
@@ -82,8 +83,8 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
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(
|
||||
preview_text = mail_text_sendmail.preview(
|
||||
text=form.cleaned_data['message'].localize(l),
|
||||
code='ORDER1234',
|
||||
event=self.request.event.name,
|
||||
date=date_format(now(), 'SHORT_DATE_FORMAT'),
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
@@ -379,7 +381,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):
|
||||
|
||||
@@ -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.'))
|
||||
|
||||
|
||||
@@ -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=[]
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -110,7 +110,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.request.session.get('payment'))
|
||||
|
||||
total += sum([f.value for f in fees])
|
||||
net_total += sum([f.net_value for f in fees])
|
||||
@@ -158,7 +158,7 @@ def get_cart_total(request):
|
||||
else:
|
||||
request._cart_total_cache = CartPosition.objects.filter(
|
||||
cart_id=request.session.session_key, event=request.event
|
||||
).aggregate(sum=Sum('price'))['sum']
|
||||
).aggregate(sum=Sum('price'))['sum'] or 0
|
||||
return request._cart_total_cache
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
from pretix.presale.views.event import item_group_by_category
|
||||
from pretix.presale.views.robots import NoSearchIndexViewMixin
|
||||
|
||||
|
||||
class CartActionMixin:
|
||||
@@ -177,7 +178,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
|
||||
class RedeemView(EventViewMixin, TemplateView):
|
||||
class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/voucher.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -5,8 +5,10 @@ from django.http import HttpResponseRedirect
|
||||
from django.utils.http import is_safe_url
|
||||
from django.views.generic import View
|
||||
|
||||
from .robots import NoSearchIndexViewMixin
|
||||
|
||||
class LocaleSet(View):
|
||||
|
||||
class LocaleSet(NoSearchIndexViewMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
url = request.GET.get('next', request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
@@ -22,16 +22,17 @@ from pretix.base.services.orders import cancel_order
|
||||
from pretix.base.services.tickets import (
|
||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||
)
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
||||
from pretix.helpers.safedownload import check_token
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.checkout import InvoiceAddressForm
|
||||
from pretix.presale.views import CartMixin, EventViewMixin
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
from pretix.presale.views.questions import QuestionsViewMixin
|
||||
from pretix.presale.views.robots import NoSearchIndexViewMixin
|
||||
|
||||
|
||||
class OrderDetailMixin:
|
||||
class OrderDetailMixin(NoSearchIndexViewMixin):
|
||||
@cached_property
|
||||
def order(self):
|
||||
order = self.request.event.orders.filter(code=self.kwargs['order']).select_related('event').first()
|
||||
@@ -70,6 +71,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||
@cached_property
|
||||
def download_buttons(self):
|
||||
buttons = []
|
||||
|
||||
responses = register_ticket_outputs.send(self.request.event)
|
||||
for receiver, response in responses:
|
||||
provider = response(self.request.event)
|
||||
@@ -86,10 +88,11 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['order'] = self.order
|
||||
|
||||
can_download = all([r for rr, r in allow_ticket_download.send(self.request.event, order=self.order)])
|
||||
if self.request.event.settings.ticket_download_date:
|
||||
ctx['ticket_download_date'] = self.order.ticket_download_date
|
||||
ctx['can_download'] = (
|
||||
self.request.event.settings.ticket_download
|
||||
can_download and self.request.event.settings.ticket_download
|
||||
and (
|
||||
self.request.event.settings.ticket_download_date is None
|
||||
or now() > self.order.ticket_download_date
|
||||
@@ -546,6 +549,8 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
@cached_property
|
||||
def output(self):
|
||||
if not all([r for rr, r in allow_ticket_download.send(self.request.event, order=self.order)]):
|
||||
return None
|
||||
responses = register_ticket_outputs.send(self.request.event)
|
||||
for receiver, response in responses:
|
||||
provider = response(self.request.event)
|
||||
|
||||
26
src/pretix/presale/views/robots.py
Normal file
26
src/pretix/presale/views/robots.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
|
||||
class NoSearchIndexViewMixin:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
resp = super().dispatch(request, *args, **kwargs)
|
||||
resp['X-Robots-Tag'] = "noindex"
|
||||
return resp
|
||||
|
||||
|
||||
@cache_page(3600)
|
||||
def robots_txt(request):
|
||||
return HttpResponse(
|
||||
"""User-agent: *
|
||||
Disallow: */cart/*
|
||||
Disallow: */checkout/*
|
||||
Disallow: */order/*
|
||||
Disallow: */locale/set*
|
||||
Disallow: /control/
|
||||
Disallow: /download/
|
||||
Disallow: /redirect/
|
||||
Disallow: /api/
|
||||
Disallow: /download/
|
||||
""", content_type='text/plain'
|
||||
)
|
||||
@@ -24,17 +24,26 @@ def initialize():
|
||||
if _initialized:
|
||||
return
|
||||
|
||||
register_serializers()
|
||||
install_middleware()
|
||||
|
||||
handler = CustomSentryDjangoHandler()
|
||||
handler.install()
|
||||
|
||||
# instantiate client so hooks get registered
|
||||
get_client() # NOQA
|
||||
|
||||
_initialized = True
|
||||
|
||||
try:
|
||||
register_serializers()
|
||||
install_middleware(
|
||||
'raven.contrib.django.middleware.SentryMiddleware',
|
||||
(
|
||||
'raven.contrib.django.middleware.SentryMiddleware',
|
||||
'raven.contrib.django.middleware.SentryLogMiddleware'))
|
||||
install_middleware(
|
||||
'raven.contrib.django.middleware.DjangoRestFrameworkCompatMiddleware')
|
||||
|
||||
handler = CustomSentryDjangoHandler()
|
||||
handler.install()
|
||||
|
||||
# instantiate client so hooks get registered
|
||||
get_client() # NOQA
|
||||
except Exception:
|
||||
_initialized = False
|
||||
|
||||
|
||||
class App(RavenConfig):
|
||||
def ready(self):
|
||||
|
||||
@@ -451,6 +451,12 @@ LOGGING = {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'default'
|
||||
},
|
||||
'csp_file': {
|
||||
'level': loglevel,
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': os.path.join(LOG_DIR, 'csp.log'),
|
||||
'formatter': 'default'
|
||||
},
|
||||
'file': {
|
||||
'level': loglevel,
|
||||
'class': 'logging.FileHandler',
|
||||
@@ -474,6 +480,11 @@ LOGGING = {
|
||||
'level': loglevel,
|
||||
'propagate': True,
|
||||
},
|
||||
'pretix.security.csp': {
|
||||
'handlers': ['csp_file'],
|
||||
'level': loglevel,
|
||||
'propagate': False,
|
||||
},
|
||||
'django.security': {
|
||||
'handlers': ['file', 'console', 'mail_admins'],
|
||||
'level': loglevel,
|
||||
|
||||
@@ -37,7 +37,14 @@ $(function () {
|
||||
$("<span>").addClass("fa fa-calendar fa-fw")
|
||||
).append(" ").append(res.date_range)
|
||||
)
|
||||
)
|
||||
).on("mousedown", function (event) {
|
||||
if ($(this).length) {
|
||||
location.href = $(this).attr("href");
|
||||
}
|
||||
$(this).parent().addClass("active");
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -161,6 +161,9 @@
|
||||
.event-dropdown .event-name-full, .mobile-event-dropdown .event-name-full {
|
||||
white-space: normal;
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
|
||||
@@ -183,3 +183,10 @@ pre.mail-preview {
|
||||
padding-top: 7px;
|
||||
}
|
||||
}
|
||||
.inline-multiple-choice {
|
||||
.checkbox {
|
||||
display: inline-block;
|
||||
margin-top: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ $(function () {
|
||||
$(".js-hidden").hide();
|
||||
$(".variations-collapsed").hide();
|
||||
$("a[data-toggle=variations]").click(function (e) {
|
||||
$(this).parent().parent().parent().find(".variations").slideToggle();
|
||||
$(this).closest(".item-with-variations").find(".variations").slideToggle();
|
||||
e.preventDefault();
|
||||
});
|
||||
$("div.collapsed").removeClass("collapsed").addClass("collapse");
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.views.generic import RedirectView
|
||||
import pretix.control.urls
|
||||
import pretix.presale.urls
|
||||
|
||||
from .base.views import cachedfiles, health, js_catalog, metrics, redirect
|
||||
from .base.views import cachedfiles, csp, health, js_catalog, metrics, redirect
|
||||
|
||||
base_patterns = [
|
||||
url(r'^download/(?P<id>[^/]+)/$', cachedfiles.DownloadView.as_view(),
|
||||
@@ -16,6 +16,7 @@ base_patterns = [
|
||||
url(r'^jsi18n/(?P<lang>[a-zA-Z-_]+)/$', js_catalog.js_catalog, name='javascript-catalog'),
|
||||
url(r'^metrics$', metrics.serve_metrics,
|
||||
name='metrics'),
|
||||
url(r'^csp_report/$', csp.csp_report, name='csp.report'),
|
||||
url(r'^api/v1/', include('pretix.api.urls', namespace='api-v1')),
|
||||
url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version')
|
||||
]
|
||||
|
||||
@@ -360,6 +360,27 @@ def test_order_extend_overdue_quota_empty(client, env):
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == newdate[:10] + " 23:59:59"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_overdue_quota_blocked_by_waiting_list(client, env):
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.save()
|
||||
q = Quota.objects.create(event=env[0], size=1)
|
||||
q.items.add(env[3])
|
||||
env[0].waitinglistentries.create(item=env[3], email='foo@bar.com')
|
||||
|
||||
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
|
||||
'expires': newdate
|
||||
}, follow=True)
|
||||
assert 'alert-success' in response.rendered_content
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == newdate[:10] + " 23:59:59"
|
||||
assert o.status == Order.STATUS_PENDING
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_quota_left(client, env):
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
|
||||
@@ -94,8 +94,9 @@ def test_step_ignored(event, mocker, req_with_session):
|
||||
|
||||
flow = with_mocked_step(mocker, MockingStep, event)
|
||||
req_with_session.event = event
|
||||
assert flow[1].get_next_applicable(req_with_session) is flow[3]
|
||||
assert flow[1] is flow[3].get_prev_applicable(req_with_session)
|
||||
assert flow[1].get_next_applicable(req_with_session) is flow[4]
|
||||
# flow[3] is also skipped because no payment is required if there is no cart
|
||||
assert flow[1] is flow[4].get_prev_applicable(req_with_session)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
Reference in New Issue
Block a user