mirror of
https://github.com/pretix/pretix.git
synced 2026-11-08 01:06:40 +00:00
Compare commits
48
Commits
| 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 |
No files matched your search
@@ -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
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-09-05 13:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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))
|
||||
|
||||
|
||||
# Functions from the following migrations need manual copying.
|
||||
# Move them and any dependencies into this file, then update the
|
||||
# RunPython operations to refer to the local versions:
|
||||
# pretix.base.migrations.0073_auto_20170716_1333
|
||||
|
||||
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')]
|
||||
|
||||
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 - EasLine truncated
|
||||
),
|
||||
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')]),
|
||||
),
|
||||
]
|
||||
-484
@@ -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',
|
||||
]
|
||||
@@ -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();
|
||||
|
||||
+3
-2
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
|
||||
+4
-26
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
+18
-9
@@ -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");
|
||||
|
||||
+2
-1
@@ -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