mirror of
https://github.com/pretix/pretix.git
synced 2026-01-08 22:02:28 +00:00
Compare commits
10 Commits
remove-rev
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f965edc22 | ||
|
|
508538aa76 | ||
|
|
4511963aca | ||
|
|
2c9277d11b | ||
|
|
93ee5450ec | ||
|
|
e1b3e20148 | ||
|
|
880e3fd93e | ||
|
|
cea201af16 | ||
|
|
93252e1645 | ||
|
|
65f8b68634 |
@@ -1 +1 @@
|
||||
__version__ = "3.13.0.dev0"
|
||||
__version__ = "3.12.1"
|
||||
|
||||
@@ -554,7 +554,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax')
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
|
||||
|
||||
class EventSettingsSerializer(serializers.Serializer):
|
||||
|
||||
@@ -250,7 +250,7 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
@@ -262,6 +262,6 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import hashlib
|
||||
import ipaddress
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
@@ -18,6 +23,7 @@ class LoginForm(forms.Form):
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
@@ -39,10 +45,36 @@ class LoginForm(forms.Form):
|
||||
else:
|
||||
move_to_end(self.fields, 'keep_logged_in')
|
||||
|
||||
@cached_property
|
||||
def ratelimit_key(self):
|
||||
if not settings.HAS_REDIS:
|
||||
return None
|
||||
client_ip = get_client_ip(self.request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.get(self.ratelimit_key)
|
||||
if cnt and int(cnt) > 10:
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
if self.ratelimit_key:
|
||||
rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 300)
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
@@ -19,6 +20,7 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"address or password."),
|
||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
|
||||
old_pw = forms.CharField(max_length=255,
|
||||
@@ -64,6 +66,18 @@ class UserSettingsForm(forms.ModelForm):
|
||||
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
if old_pw and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
|
||||
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if old_pw and not check_password(old_pw, self.user.password):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
|
||||
@@ -3,8 +3,7 @@ from urllib.parse import urlsplit
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse, Http404
|
||||
from django.middleware.common import CommonMiddleware
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import get_script_prefix
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
@@ -253,15 +252,3 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
class CustomCommonMiddleware(CommonMiddleware):
|
||||
|
||||
def get_full_path_with_slash(self, request):
|
||||
"""
|
||||
Raise an error regardless of DEBUG mode when in POST, PUT, or PATCH.
|
||||
"""
|
||||
new_path = super().get_full_path_with_slash(request)
|
||||
if request.method in ('POST', 'PUT', 'PATCH'):
|
||||
raise Http404('Please append a / at the end of the URL')
|
||||
return new_path
|
||||
|
||||
23
src/pretix/base/migrations/0162b_auto_20201218_1810.py
Normal file
23
src/pretix/base/migrations/0162b_auto_20201218_1810.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.11 on 2020-12-18 18:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0162_remove_seat_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cachedfile',
|
||||
name='session_key',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedfile',
|
||||
name='web_download',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -1,49 +0,0 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-30 21:09
|
||||
import json
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_tax_rules(apps, schema_editor):
|
||||
TaxRule = apps.get_model('pretixbase', 'TaxRule')
|
||||
for tr in TaxRule.objects.filter(eu_reverse_charge=True):
|
||||
if tr.custom_rules and tr.custom_rules != '[]':
|
||||
# Custom rules take precedence
|
||||
continue
|
||||
r = [{
|
||||
'country': str(tr.home_country),
|
||||
'address_type': '',
|
||||
'action': 'vat'
|
||||
}, {
|
||||
'country': 'EU',
|
||||
'address_type': 'business_vat_id',
|
||||
'action': 'reverse'
|
||||
}, {
|
||||
'country': 'EU',
|
||||
'address_type': '',
|
||||
'action': 'vat'
|
||||
}, {
|
||||
'country': 'ZZ',
|
||||
'address_type': '',
|
||||
'action': 'no'
|
||||
}]
|
||||
tr.custom_rules = json.dumps(r)
|
||||
tr.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0169_checkinlist_gates'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_tax_rules, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name='taxrule',
|
||||
name='eu_reverse_charge',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='taxrule',
|
||||
name='home_country',
|
||||
),
|
||||
]
|
||||
14
src/pretix/base/migrations/0170_merge_20201222_1028.py
Normal file
14
src/pretix/base/migrations/0170_merge_20201222_1028.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated by Django 3.0.11 on 2020-12-22 10:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0169_checkinlist_gates'),
|
||||
('pretixbase', '0162b_auto_20201218_1810'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@@ -28,6 +28,8 @@ class CachedFile(models.Model):
|
||||
filename = models.CharField(max_length=255)
|
||||
type = models.CharField(max_length=255)
|
||||
file = models.FileField(null=True, blank=True, upload_to=cachedfile_name, max_length=255)
|
||||
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
||||
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedFile)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -9,6 +10,7 @@ from i18nfield.fields import I18nCharField
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.countries import FastCountryField
|
||||
|
||||
|
||||
class TaxedPrice:
|
||||
@@ -105,6 +107,21 @@ class TaxRule(LoggedModel):
|
||||
verbose_name=_("The configured product prices include the tax amount"),
|
||||
default=True,
|
||||
)
|
||||
eu_reverse_charge = models.BooleanField(
|
||||
verbose_name=_("Use EU reverse charge taxation rules"),
|
||||
default=False,
|
||||
help_text=_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
|
||||
"taxation is the location of the event. This option disables charging VAT for all customers "
|
||||
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
|
||||
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
|
||||
"calculation. USE AT YOUR OWN RISK.")
|
||||
)
|
||||
home_country = FastCountryField(
|
||||
verbose_name=_('Merchant country'),
|
||||
blank=True,
|
||||
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
|
||||
'if configured above.'),
|
||||
)
|
||||
custom_rules = models.TextField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -130,13 +147,17 @@ class TaxRule(LoggedModel):
|
||||
eu_reverse_charge=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.eu_reverse_charge and not self.home_country:
|
||||
raise ValidationError(_('You need to set your home country to use the reverse charge feature.'))
|
||||
|
||||
def __str__(self):
|
||||
if self.price_includes_tax:
|
||||
s = _('incl. {rate}% {name}').format(rate=self.rate, name=self.name)
|
||||
else:
|
||||
s = _('plus {rate}% {name}').format(rate=self.rate, name=self.name)
|
||||
if self.has_custom_rules:
|
||||
s += ' ({})'.format(_('with custom rules'))
|
||||
if self.eu_reverse_charge:
|
||||
s += ' ({})'.format(_('reverse charge enabled'))
|
||||
return str(s)
|
||||
|
||||
@property
|
||||
@@ -237,12 +258,50 @@ class TaxRule(LoggedModel):
|
||||
if self._custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
return rule['action'] == 'reverse'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
return False
|
||||
|
||||
if not invoice_address or not invoice_address.country:
|
||||
return False
|
||||
|
||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
||||
return False
|
||||
|
||||
if invoice_address.country == self.home_country:
|
||||
return False
|
||||
|
||||
if invoice_address.is_business and invoice_address.vat_id and invoice_address.vat_id_validated:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _tax_applicable(self, invoice_address):
|
||||
if self._custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
return rule.get('action', 'vat') == 'vat'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
# No reverse charge rules? Always apply VAT!
|
||||
return True
|
||||
|
||||
if not invoice_address or not invoice_address.country:
|
||||
# No country specified? Always apply VAT!
|
||||
return True
|
||||
|
||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
||||
# Non-EU country? Never apply VAT!
|
||||
return False
|
||||
|
||||
if invoice_address.country == self.home_country:
|
||||
# Within same EU country? Always apply VAT!
|
||||
return True
|
||||
|
||||
if invoice_address.is_business and invoice_address.vat_id and invoice_address.vat_id_validated:
|
||||
# Reverse charge case
|
||||
return False
|
||||
|
||||
# Consumer in different EU country / invalid VAT
|
||||
return True
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
@@ -65,7 +65,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
|
||||
email_context = get_email_context(event_or_subevent=subevent or order.event,
|
||||
event=order.event,
|
||||
refund_amount=refund_amount,
|
||||
position_or_address=p,
|
||||
@@ -82,12 +82,11 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
keep_fee_fixed: str, keep_fee_per_ticket: str, keep_fee_percentage: str, keep_fees: list=None,
|
||||
manual_refund: bool=False, send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
|
||||
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
||||
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
|
||||
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None,
|
||||
subevents_from: str=None, subevents_to: str=None):
|
||||
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None):
|
||||
send_subject = LazyI18nString(send_subject)
|
||||
send_message = LazyI18nString(send_message)
|
||||
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
||||
@@ -103,20 +102,14 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
pcnt__gt=0
|
||||
).all()
|
||||
|
||||
if subevent or subevents_from:
|
||||
if subevent:
|
||||
subevents = event.subevents.filter(pk=subevent)
|
||||
subevent = subevents.first()
|
||||
subevent_ids = {subevent.pk}
|
||||
else:
|
||||
subevents = event.subevents.filter(date_from__gte=subevents_from, date_from__lt=subevents_to)
|
||||
subevent_ids = set(subevents.values_list('id', flat=True))
|
||||
if subevent:
|
||||
subevent = event.subevents.get(pk=subevent)
|
||||
|
||||
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
|
||||
subevent__in=subevents
|
||||
subevent=subevent
|
||||
)
|
||||
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
||||
subevent__in=subevents
|
||||
subevent=subevent
|
||||
)
|
||||
orders_to_change = orders_to_cancel.annotate(
|
||||
has_subevent=Exists(has_subevent),
|
||||
@@ -131,18 +124,15 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
has_subevent=True, has_other_subevent=False
|
||||
)
|
||||
|
||||
for se in subevents:
|
||||
se.log_action(
|
||||
'pretix.subevent.canceled', user=user,
|
||||
)
|
||||
se.active = False
|
||||
se.save(update_fields=['active'])
|
||||
se.log_action(
|
||||
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||
)
|
||||
subevent.log_action(
|
||||
'pretix.subevent.canceled', user=user,
|
||||
)
|
||||
subevent.active = False
|
||||
subevent.save(update_fields=['active'])
|
||||
subevent.log_action(
|
||||
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||
)
|
||||
else:
|
||||
subevents = None
|
||||
subevent_ids = set()
|
||||
orders_to_change = event.orders.none()
|
||||
event.log_action(
|
||||
'pretix.event.canceled', user=user,
|
||||
@@ -156,9 +146,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
)
|
||||
failed = 0
|
||||
total = orders_to_cancel.count() + orders_to_change.count()
|
||||
qs_wl = event.waitinglistentries.filter(voucher__isnull=True).select_related('subevent')
|
||||
if subevents:
|
||||
qs_wl = qs_wl.filter(subevent__in=subevents)
|
||||
qs_wl = event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True)
|
||||
if send_waitinglist:
|
||||
total += qs_wl.count()
|
||||
counter = 0
|
||||
@@ -182,10 +170,6 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
|
||||
if keep_fee_fixed:
|
||||
fee += Decimal(keep_fee_fixed)
|
||||
if keep_fee_per_ticket:
|
||||
for p in o.positions.all():
|
||||
if p.addon_to_id is None:
|
||||
fee += min(p.price, Decimal(keep_fee_per_ticket))
|
||||
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
|
||||
|
||||
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
|
||||
@@ -217,20 +201,16 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
with transaction.atomic():
|
||||
o = event.orders.select_for_update().get(pk=o)
|
||||
total = Decimal('0.00')
|
||||
fee = Decimal('0.00')
|
||||
positions = []
|
||||
|
||||
ocm = OrderChangeManager(o, user=user, notify=False)
|
||||
for p in o.positions.all():
|
||||
if p.subevent_id in subevent_ids:
|
||||
if p.subevent == subevent:
|
||||
total += p.price
|
||||
ocm.cancel(p)
|
||||
positions.append(p)
|
||||
|
||||
if keep_fee_per_ticket:
|
||||
if p.addon_to_id is None:
|
||||
fee += min(p.price, Decimal(keep_fee_per_ticket))
|
||||
|
||||
fee = Decimal('0.00')
|
||||
if keep_fee_fixed:
|
||||
fee += Decimal(keep_fee_fixed)
|
||||
if keep_fee_percentage:
|
||||
@@ -266,7 +246,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
|
||||
if send_waitinglist:
|
||||
for wle in qs_wl:
|
||||
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.subevent)
|
||||
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
|
||||
|
||||
counter += 1
|
||||
if not self.request.called_directly and counter % max(10, total // 100) == 0:
|
||||
|
||||
@@ -372,7 +372,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
backend.send_messages([email])
|
||||
except smtplib.SMTPResponseException as e:
|
||||
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
logger.exception('Error sending email')
|
||||
|
||||
if order:
|
||||
@@ -389,7 +389,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||
except Exception as e:
|
||||
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
|
||||
@@ -17,7 +17,7 @@ from pretix.celery_app import app
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask)
|
||||
def export(event: Event, shredders: List[str]) -> None:
|
||||
def export(event: Event, shredders: List[str], session_key=None) -> None:
|
||||
known_shredders = event.get_data_shredders()
|
||||
|
||||
with NamedTemporaryFile() as rawfile:
|
||||
@@ -55,6 +55,8 @@ def export(event: Event, shredders: List[str]) -> None:
|
||||
cf.date = now()
|
||||
cf.filename = event.slug + '.zip'
|
||||
cf.type = 'application/zip'
|
||||
cf.session_key = session_key
|
||||
cf.web_download = True
|
||||
cf.expires = now() + timedelta(hours=1)
|
||||
cf.save()
|
||||
cf.file.save(cachedfile_name(cf, cf.filename), rawfile)
|
||||
|
||||
@@ -2049,7 +2049,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
'_scheme': 'salutation_title_given_family',
|
||||
'_scheme': 'title_salutation_given_family',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
|
||||
{{ widget.input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
||||
{% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
import bleach
|
||||
@@ -71,6 +72,10 @@ EMAIL_RE = build_email_re(tlds=sorted(tld_set, key=len, reverse=True))
|
||||
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
"""
|
||||
Makes sure that all links to a different domain are passed through a redirection handler
|
||||
to ensure there's no passing of referers with secrets inside them.
|
||||
"""
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
@@ -80,7 +85,42 @@ def safelink_callback(attrs, new=False):
|
||||
return attrs
|
||||
|
||||
|
||||
def truelink_callback(attrs, new=False):
|
||||
"""
|
||||
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
|
||||
points somewhere else, e.g.
|
||||
|
||||
<a href="https://evilsite.com">https://google.com</a>
|
||||
|
||||
At the same time, custom texts are still allowed:
|
||||
|
||||
<a href="https://maps.google.com">Get to the event</a>
|
||||
|
||||
Suffixes are also allowed:
|
||||
|
||||
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
||||
"""
|
||||
text = re.sub('[^a-zA-Z0-9.-/_]', '', attrs.get('_text')) # clean up link text
|
||||
if URL_RE.match(text):
|
||||
# link text looks like a url
|
||||
if text.startswith('//'):
|
||||
text = 'https:' + text
|
||||
elif not text.startswith('http'):
|
||||
text = 'https://' + text
|
||||
|
||||
text_url = urllib.parse.urlparse(text)
|
||||
href_url = urllib.parse.urlparse(attrs[None, 'href'])
|
||||
if text_url.netloc != href_url.netloc or not href_url.path.startswith(href_url.path):
|
||||
# link text contains an URL that has a different base than the actual URL
|
||||
attrs['_text'] = attrs[None, 'href']
|
||||
return attrs
|
||||
|
||||
|
||||
def abslink_callback(attrs, new=False):
|
||||
"""
|
||||
Makes sure that all links will be absolute links and will be opened in a new page with no
|
||||
window.opener attribute.
|
||||
"""
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url)
|
||||
@@ -93,6 +133,7 @@ def markdown_compile_email(source):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
return linker.linkify(bleach.clean(
|
||||
@@ -145,7 +186,7 @@ def rich_text(text: str, **kwargs):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
|
||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text))
|
||||
@@ -161,7 +202,7 @@ def rich_text_snippet(text: str, **kwargs):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
|
||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text, snippet=True))
|
||||
|
||||
@@ -13,7 +13,11 @@ class DownloadView(TemplateView):
|
||||
@cached_property
|
||||
def object(self) -> CachedFile:
|
||||
try:
|
||||
return get_object_or_404(CachedFile, id=self.kwargs['id'])
|
||||
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
||||
if o.session_key:
|
||||
if o.session_key != self.request.session.session_key:
|
||||
raise Http404()
|
||||
return o
|
||||
except ValueError: # Invalid URLs
|
||||
raise Http404()
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.forms.utils import from_current_timezone
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ...base.forms import I18nModelForm
|
||||
@@ -78,8 +77,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if hasattr(self.file, 'display_name'):
|
||||
return self.file.display_name
|
||||
return self.file.name
|
||||
|
||||
@property
|
||||
@@ -87,8 +84,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
||||
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self.file, 'display_name'):
|
||||
return self.file.display_name
|
||||
return os.path.basename(self.file.name).split('.', 1)[-1]
|
||||
|
||||
@property
|
||||
@@ -98,48 +93,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
||||
def get_context(self, name, value, attrs):
|
||||
ctx = super().get_context(name, value, attrs)
|
||||
ctx['widget']['value'] = self.FakeFile(value)
|
||||
ctx['widget']['cachedfile'] = None
|
||||
return ctx
|
||||
|
||||
|
||||
class CachedFileInput(forms.ClearableFileInput):
|
||||
template_name = 'pretixbase/forms/widgets/thumbnailed_file_input.html'
|
||||
|
||||
class FakeFile(File):
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.file.filename
|
||||
|
||||
@property
|
||||
def is_img(self):
|
||||
return any(self.file.filename.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
|
||||
|
||||
def __str__(self):
|
||||
return self.file.filename
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.file.file.url
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
from ...base.models import CachedFile
|
||||
v = super().value_from_datadict(data, files, name)
|
||||
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
|
||||
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
return v
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
from ...base.models import CachedFile
|
||||
if isinstance(value, CachedFile):
|
||||
value = self.FakeFile(value)
|
||||
|
||||
ctx = super().get_context(name, value, attrs)
|
||||
ctx['widget']['value'] = value
|
||||
ctx['widget']['cachedfile'] = value.file if isinstance(value, self.FakeFile) else None
|
||||
ctx['widget']['hidden_name'] = name + '-cachedfile'
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -176,7 +129,7 @@ class ExtFileField(SizeFileField):
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
data = super().clean(*args, **kwargs)
|
||||
if isinstance(data, File):
|
||||
if data:
|
||||
filename = data.name
|
||||
ext = os.path.splitext(filename)[1]
|
||||
ext = ext.lower()
|
||||
@@ -185,49 +138,6 @@ class ExtFileField(SizeFileField):
|
||||
return data
|
||||
|
||||
|
||||
class CachedFileField(ExtFileField):
|
||||
widget = CachedFileInput
|
||||
|
||||
def to_python(self, data):
|
||||
from ...base.models import CachedFile
|
||||
|
||||
if isinstance(data, CachedFile):
|
||||
return data
|
||||
|
||||
return super().to_python(data)
|
||||
|
||||
def bound_data(self, data, initial):
|
||||
from ...base.models import CachedFile
|
||||
|
||||
if isinstance(data, File):
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
filename=data.name,
|
||||
type=data.content_type,
|
||||
)
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
return cf
|
||||
return super().bound_data(data, initial)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
from ...base.models import CachedFile
|
||||
|
||||
data = super().clean(*args, **kwargs)
|
||||
if isinstance(data, File):
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
filename=data.name,
|
||||
type=data.content_type,
|
||||
)
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
return cf
|
||||
return data
|
||||
|
||||
|
||||
class SlugWidget(forms.TextInput):
|
||||
template_name = 'pretixcontrol/slug_widget.html'
|
||||
prefix = ''
|
||||
|
||||
@@ -1164,7 +1164,7 @@ TaxRuleLineFormSet = formset_factory(
|
||||
class TaxRuleForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ['name', 'rate', 'price_includes_tax']
|
||||
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country']
|
||||
|
||||
|
||||
class WidgetCodeForm(forms.Form):
|
||||
|
||||
@@ -400,6 +400,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
self.fields['tax_rule'].label_from_instance = self.taxrule_label_from_instance
|
||||
|
||||
if not instance.seat and not (
|
||||
not instance.event.settings.seating_choice and
|
||||
instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists()
|
||||
):
|
||||
del self.fields['seat']
|
||||
@@ -516,20 +517,6 @@ class OrderMailForm(forms.Form):
|
||||
self._set_field_placeholders('message', ['event', 'order'])
|
||||
|
||||
|
||||
class OrderPositionMailForm(OrderMailForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
position = self.position = kwargs.pop('position')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['sendto'].initial = position.attendee_email
|
||||
self.fields['message'] = forms.CharField(
|
||||
label=_("Message"),
|
||||
required=True,
|
||||
widget=forms.Textarea,
|
||||
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
|
||||
)
|
||||
self._set_field_placeholders('message', ['event', 'order', 'position'])
|
||||
|
||||
|
||||
class OrderRefundForm(forms.Form):
|
||||
action = forms.ChoiceField(
|
||||
required=False,
|
||||
@@ -585,21 +572,7 @@ class EventCancelForm(forms.Form):
|
||||
all_subevents = forms.BooleanField(
|
||||
label=_('Cancel all dates'),
|
||||
initial=False,
|
||||
required=False,
|
||||
)
|
||||
subevents_from = forms.SplitDateTimeField(
|
||||
widget=SplitDateTimePickerWidget(attrs={
|
||||
'data-inverse-dependency': '#id_all_subevents',
|
||||
}),
|
||||
label=pgettext_lazy('subevent', 'All dates starting at or after'),
|
||||
required=False,
|
||||
)
|
||||
subevents_to = forms.SplitDateTimeField(
|
||||
widget=SplitDateTimePickerWidget(attrs={
|
||||
'data-inverse-dependency': '#id_all_subevents',
|
||||
}),
|
||||
label=pgettext_lazy('subevent', 'All dates starting before'),
|
||||
required=False,
|
||||
required=False
|
||||
)
|
||||
auto_refund = forms.BooleanField(
|
||||
label=_('Automatically refund money if possible'),
|
||||
@@ -640,12 +613,6 @@ class EventCancelForm(forms.Form):
|
||||
max_digits=10, decimal_places=2,
|
||||
required=False
|
||||
)
|
||||
keep_fee_per_ticket = forms.DecimalField(
|
||||
label=_("Keep a fixed cancellation fee per ticket"),
|
||||
help_text=_("Free tickets and add-on products are not counted"),
|
||||
max_digits=10, decimal_places=2,
|
||||
required=False
|
||||
)
|
||||
keep_fee_percentage = forms.DecimalField(
|
||||
label=_("Keep a percentual cancellation fee"),
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -750,7 +717,6 @@ class EventCancelForm(forms.Form):
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-inverse-dependency': '#id_all_subevents',
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
@@ -767,12 +733,6 @@ class EventCancelForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('subevent') and d.get('subevents_from'):
|
||||
raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.'))
|
||||
if d.get('all_subevents') and d.get('subevent_from'):
|
||||
raise ValidationError(pgettext_lazy('subevent', 'Please either select all subevents or a date range, not both.'))
|
||||
if bool(d.get('subevents_from')) != bool(d.get('subevents_to')):
|
||||
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
|
||||
if self.event.has_subevents and not d['subevent'] and not d['all_subevents'] and not d.get('subevents_from'):
|
||||
if self.event.has_subevents and not d['subevent'] and not d['all_subevents']:
|
||||
raise ValidationError(_('Please confirm that you want to cancel ALL dates in this event series.'))
|
||||
return d
|
||||
|
||||
@@ -34,9 +34,15 @@
|
||||
for more information. Note that we are not responsible for the correct handling
|
||||
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
</div>
|
||||
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
||||
{% bootstrap_field form.home_country layout="control" %}
|
||||
<h3>{% trans "Custom taxation rules" %}</h3>
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
The rules will be checked in order and once the first rule matches the order, it will be used and all further rules will
|
||||
These settings are intended for professional users with very specific taxation situations.
|
||||
If you create any rule here, the reverse charge settings above will be ignored. The rules will be
|
||||
checked in order and once the first rule matches the order, it will be used and all further rules will
|
||||
be ignored. If no rule matches, tax will be charged.
|
||||
{% endblocktrans %}
|
||||
{% trans "All of these rules will only apply if an invoice address is set." %}
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
{{ order.email|default_if_none:"" }}
|
||||
{% if order.email and order.email_known_to_work %}
|
||||
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
@@ -388,12 +388,8 @@
|
||||
{{ line.attendee_email }}
|
||||
{% if not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
|
||||
@@ -27,10 +27,8 @@
|
||||
{% if request.event.has_subevents %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Select date" context "subevents" %}</legend>
|
||||
{% bootstrap_field form.all_subevents layout="control" %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% bootstrap_field form.subevents_from layout="control" %}
|
||||
{% bootstrap_field form.subevents_to layout="control" %}
|
||||
{% bootstrap_field form.all_subevents layout="control" %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
@@ -41,7 +39,6 @@
|
||||
{% bootstrap_field form.gift_card_expires layout="control" %}
|
||||
{% bootstrap_field form.gift_card_conditions layout="control" %}
|
||||
{% bootstrap_field form.keep_fee_fixed layout="control" %}
|
||||
{% bootstrap_field form.keep_fee_per_ticket layout="control" %}
|
||||
{% bootstrap_field form.keep_fee_percentage layout="control" %}
|
||||
{% bootstrap_field form.keep_fees layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -2,36 +2,19 @@
|
||||
{% load bootstrap3 %}
|
||||
{% if order.status == "n" %}
|
||||
{% if order.require_approval %}
|
||||
<span class="label label-warning {{ class }}">
|
||||
<span class="fa fa-question-circle"></span>
|
||||
{% trans "Approval pending" %}
|
||||
</span>
|
||||
<span class="label label-warning {{ class }}">{% trans "Approval pending" %}</span>
|
||||
{% else %}
|
||||
<span data-toggle="tooltip" title="{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}"
|
||||
class="label label-warning {{ class }}">
|
||||
<span class="fa fa-money"></span>
|
||||
{% trans "Pending" %}
|
||||
</span>
|
||||
class="label label-warning {{ class }}">{% trans "Pending" %}</span>
|
||||
{% endif %}
|
||||
{% elif order.status == "p" %}
|
||||
{% if order.count_positions == 0 %}
|
||||
<span class="label label-info {{ class }}">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Canceled (paid fee)" %}
|
||||
</span>
|
||||
<span class="label label-info {{ class }}">{% trans "Canceled (paid fee)" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-success {{ class }}">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Paid" %}
|
||||
</span>
|
||||
<span class="label label-success {{ class }}">{% trans "Paid" %}</span>
|
||||
{% endif %}
|
||||
{% elif order.status == "e" %} {# expired #}
|
||||
<span class="label label-danger {{ class }}">
|
||||
<span class="fa fa-clock-o"></span>
|
||||
{% trans "Expired" %}</span>
|
||||
<span class="label label-danger {{ class }}">{% trans "Expired" %}</span>
|
||||
{% elif order.status == "c" %}
|
||||
<span class="label label-danger {{ class }}">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Canceled" %}
|
||||
</span>
|
||||
<span class="label label-danger {{ class }}">{% trans "Canceled" %}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -253,8 +253,6 @@ urlpatterns = [
|
||||
name='event.order.info'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(),
|
||||
name='event.order.sendmail'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/(?P<position>[0-9A-Z]+)/sendmail$', orders.OrderPositionSendMail.as_view(),
|
||||
name='event.order.position.sendmail'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/mail_history$', orders.OrderEmailHistory.as_view(),
|
||||
name='event.order.mail_history'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/payments/(?P<payment>\d+)/cancel$', orders.OrderPaymentCancel.as_view(),
|
||||
|
||||
@@ -74,13 +74,15 @@ def login(request):
|
||||
backend = [b for b in backends if b.visible][0]
|
||||
if request.user.is_authenticated:
|
||||
next_url = backend.get_next_url(request) or 'control:index'
|
||||
return redirect(next_url)
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect(next_url)
|
||||
return redirect(reverse('control:index'))
|
||||
if request.method == 'POST':
|
||||
form = LoginForm(backend=backend, data=request.POST)
|
||||
form = LoginForm(backend=backend, data=request.POST, request=request)
|
||||
if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier:
|
||||
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
|
||||
else:
|
||||
form = LoginForm(backend=backend)
|
||||
form = LoginForm(backend=backend, request=request)
|
||||
ctx['form'] = form
|
||||
ctx['can_register'] = settings.PRETIX_REGISTRATION
|
||||
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
|
||||
|
||||
@@ -80,8 +80,8 @@ from pretix.control.forms.orders import (
|
||||
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderRefundForm,
|
||||
OtherOperationsForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
@@ -1783,57 +1783,6 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
return ctx
|
||||
|
||||
|
||||
class OrderPositionSendMail(OrderSendMail):
|
||||
form_class = OrderPositionMailForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['position'] = get_object_or_404(
|
||||
OrderPosition,
|
||||
order__event=self.request.event,
|
||||
order__code=self.kwargs['code'].upper(),
|
||||
pk=self.kwargs['position'],
|
||||
attendee_email__isnull=False
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
position = get_object_or_404(
|
||||
OrderPosition,
|
||||
order__event=self.request.event,
|
||||
order__code=self.kwargs['code'].upper(),
|
||||
pk=self.kwargs['position'],
|
||||
attendee_email__isnull=False
|
||||
)
|
||||
self.preview_output = {}
|
||||
with language(position.order.locale):
|
||||
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
|
||||
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
|
||||
email_content = render_mail(email_template, email_context)
|
||||
if self.request.POST.get('action') == 'preview':
|
||||
self.preview_output = {
|
||||
'subject': _('Subject: {subject}').format(subject=email_subject),
|
||||
'html': markdown_compile_email(email_content)
|
||||
}
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
else:
|
||||
try:
|
||||
position.send_mail(
|
||||
form.cleaned_data['subject'],
|
||||
email_template,
|
||||
email_context,
|
||||
'pretix.event.order.position.email.custom_sent',
|
||||
self.request.user
|
||||
)
|
||||
messages.success(self.request,
|
||||
_('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))
|
||||
except SendMailException:
|
||||
messages.error(self.request,
|
||||
_('Failed to send mail to the following user: {}'.format(position.attendee_email)))
|
||||
return super(OrderSendMail, self).form_valid(form)
|
||||
|
||||
|
||||
class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
||||
template_name = 'pretixcontrol/order/mail_history.html'
|
||||
permission = 'can_view_orders'
|
||||
@@ -2009,9 +1958,9 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
cf = CachedFile()
|
||||
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(days=3)
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.save()
|
||||
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data)
|
||||
|
||||
@@ -2074,15 +2023,12 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
|
||||
return self.do(
|
||||
self.request.event.pk,
|
||||
subevent=form.cleaned_data['subevent'].pk if form.cleaned_data.get('subevent') else None,
|
||||
subevents_from=form.cleaned_data.get('subevents_from'),
|
||||
subevents_to=form.cleaned_data.get('subevents_to'),
|
||||
auto_refund=form.cleaned_data.get('auto_refund'),
|
||||
manual_refund=form.cleaned_data.get('manual_refund'),
|
||||
refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'),
|
||||
giftcard_expires=form.cleaned_data.get('gift_card_expires'),
|
||||
giftcard_conditions=form.cleaned_data.get('gift_card_conditions'),
|
||||
keep_fee_fixed=form.cleaned_data.get('keep_fee_fixed'),
|
||||
keep_fee_per_ticket=form.cleaned_data.get('keep_fee_per_ticket'),
|
||||
keep_fee_percentage=form.cleaned_data.get('keep_fee_percentage'),
|
||||
keep_fees=form.cleaned_data.get('keep_fees'),
|
||||
send=form.cleaned_data.get('send'),
|
||||
|
||||
@@ -1245,9 +1245,9 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
cf = CachedFile()
|
||||
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(days=3)
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.save()
|
||||
return self.do(
|
||||
organizer=self.request.organizer.id,
|
||||
|
||||
@@ -137,7 +137,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
buffer = BytesIO()
|
||||
p.write(buffer)
|
||||
buffer.seek(0)
|
||||
c = CachedFile()
|
||||
c = CachedFile(web_download=True)
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
@@ -162,7 +162,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
"status": "error",
|
||||
"error": error
|
||||
})
|
||||
c = CachedFile()
|
||||
c = CachedFile(web_download=True)
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
|
||||
@@ -75,7 +75,7 @@ class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequired
|
||||
if constr:
|
||||
return self.error(ShredError(self.get_error_url()))
|
||||
|
||||
return self.do(self.request.event.id, request.POST.getlist("shredder"))
|
||||
return self.do(self.request.event.id, request.POST.getlist("shredder"), self.request.session.session_key)
|
||||
|
||||
|
||||
class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
||||
"PO-Revision-Date: 2020-10-27 21:00+0000\n"
|
||||
"PO-Revision-Date: 2020-10-24 16:27+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fi/>\n"
|
||||
@@ -6219,7 +6219,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py:2009 pretix/base/settings.py:2020
|
||||
msgctxt "person_name_sample"
|
||||
msgid "John Doe"
|
||||
msgstr "Matti Meikäläinen"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:2015
|
||||
msgid "Calling name"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-10-24 20:02+0000\n"
|
||||
"PO-Revision-Date: 2020-10-27 06:00+0000\n"
|
||||
"Last-Translator: David Vaz <davidmgvaz@gmail.com>\n"
|
||||
"PO-Revision-Date: 2020-10-26 04:00+0000\n"
|
||||
"Last-Translator: Miguel Magalhães <magamig@gmail.com>\n"
|
||||
"Language-Team: Portuguese (Portugal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix-js/pt_PT/>\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -41,7 +41,7 @@ msgstr "Encomendas pagas"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
msgid "Total revenue"
|
||||
msgstr "Total de receitas"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
|
||||
msgid "Contacting Stripe …"
|
||||
@@ -66,13 +66,11 @@ msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
"O seu pedido está a ser processado. Dependendo do tamanho do seu evento, "
|
||||
"isto pode demorar alguns minutos."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:121
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "O seu pedido está na fila no servidor e em breve será processado."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:127
|
||||
@@ -81,9 +79,6 @@ msgid ""
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
"browser and try again."
|
||||
msgstr ""
|
||||
"O seu pedido chegou ao servidor, mas ainda aguardamos que seja processado. "
|
||||
"Se demorar mais de dois minutos, entre em contato connosco ou volte ao seu "
|
||||
"navegador e tente novamente."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:86
|
||||
#: pretix/static/pretixbase/js/asynctask.js:159
|
||||
@@ -97,8 +92,6 @@ msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"Atualmente não conseguimos chegar ao servidor, mas continuamos a tentar. "
|
||||
"Último código de erro: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:141
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
@@ -110,8 +103,6 @@ msgstr "O pedido demorou demasiado. Por favor tente novamente."
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"Atualmente não conseguimos chegar ao servidor. Por favor tente outra vez. "
|
||||
"Código de erro: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:189
|
||||
msgid "We are processing your request …"
|
||||
@@ -123,9 +114,6 @@ msgid ""
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
"Estamos neste momento a enviar o seu pedido para o servidor. Se demorar mais "
|
||||
"de um minuto, verifique a sua ligação à Internet e, em seguida, recarregue "
|
||||
"esta página e tente novamente."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:235
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:34
|
||||
@@ -167,23 +155,23 @@ msgstr "Data e hora atual"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:66
|
||||
msgid "Number of previous entries"
|
||||
msgstr "Número de entradas anteriores"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:70
|
||||
msgid "Number of previous entries since midnight"
|
||||
msgstr "Número de entradas prévias desde a meia noite"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:74
|
||||
msgid "Number of days with a previous entry"
|
||||
msgstr "Número de dias com entrada prévia"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:214
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr "Todas as condições abaixo (E)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:215
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr "Pelo menos uma das condições abaixo (Ou)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:223
|
||||
msgid "Event start"
|
||||
@@ -199,11 +187,11 @@ msgstr "Admissão ao evento"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:226
|
||||
msgid "custom time"
|
||||
msgstr "Hora personalisada"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:229
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr "Tolerância (minutos)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:237
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:441
|
||||
@@ -212,23 +200,23 @@ msgstr "Adicionar condição"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:43
|
||||
msgid "Lead Scan QR"
|
||||
msgstr "Scan QR de leads"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:45
|
||||
msgid "Check-in QR"
|
||||
msgstr "Check-in QR"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:269
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "O ficheiro de fundo PDF não pôde ser carregado pela seguinte razão:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:461
|
||||
msgid "Group of objects"
|
||||
msgstr "Grupo de objectos"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:467
|
||||
msgid "Text object"
|
||||
msgstr "Objecto de texto"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:469
|
||||
msgid "Barcode area"
|
||||
@@ -240,7 +228,7 @@ msgstr "Powered by pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:473
|
||||
msgid "Object"
|
||||
msgstr "Objecto"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:477
|
||||
msgid "Ticket design"
|
||||
@@ -248,16 +236,16 @@ msgstr "Design do Bilhete"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:734
|
||||
msgid "Saving failed."
|
||||
msgstr "Salvar falhou."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:783
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:821
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr "Erro ao carregar o seu ficheiro PDF, por favor tente novamente."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:806
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr "Quer mesmo deixar o editor sem guardar as suas alterações?"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:19
|
||||
msgid "An error has occurred."
|
||||
@@ -273,20 +261,17 @@ msgstr "Erro desconhecido."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:233
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr "A sua cor tem um grande contraste e é muito fácil de ler!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:237
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
"Sua cor tem contraste decente e é provavelmente bom o suficiente para ler!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:241
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
"A sua cor tem um mau contraste para texto no fundo branco, por favor escolha "
|
||||
"uma tonalidade mais escura."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:376
|
||||
msgid "All"
|
||||
@@ -331,8 +316,8 @@ msgstr "Não"
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
msgstr[0] "(mais uma data)"
|
||||
msgstr[1] "(mais {num} datas)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:39
|
||||
msgid "The items in your cart are no longer reserved for you."
|
||||
@@ -345,8 +330,8 @@ msgstr "Carrinho expirado"
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:46
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] "Os artigos no seu carrinho estão reservados para si por um minuto."
|
||||
msgstr[1] "Os artigos no seu carrinho estão reservados para si por {num} minutos."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:260
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
@@ -354,11 +339,11 @@ msgstr "Por favor insira a quantidade para um tipo de bilhetes."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "O organizador mantém %(currency)s %(amount)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "Recebes %(currency)s %(amount)s de volta"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
@@ -367,16 +352,16 @@ msgstr "Por favor insira o montante com que a organização pode ficar."
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr "Fuso horário:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr "Sua hora local:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:17
|
||||
msgctxt "widget"
|
||||
msgid "Sold out"
|
||||
msgstr "Esgotado"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:18
|
||||
msgctxt "widget"
|
||||
@@ -401,17 +386,17 @@ msgstr "GRÁTIS"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "from %(currency)s %(price)s"
|
||||
msgstr "de %(currency)s %(price)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
msgctxt "widget"
|
||||
msgid "incl. %(rate)s% %(taxname)s"
|
||||
msgstr "incl. %(rate)s% %(taxname)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr "mais %(rate)s% %(taxname)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
msgctxt "widget"
|
||||
@@ -438,7 +423,7 @@ msgstr "Apenas disponível com um voucher"
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr "montante mínimo a encomendar: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
@@ -453,7 +438,7 @@ msgstr "Não conseguimos carregar a bilheteira."
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "O carrinho não pôde ser criado. Por favor, tente de novo mais tarde"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
@@ -466,8 +451,6 @@ msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
"Atualmente tem um carrinho ativo para este evento. Se selecionar mais "
|
||||
"produtos, serão adicionados ao seu carrinho existente."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
|
||||
@@ -235,7 +235,7 @@ class OrderPrintDo(EventPermissionRequiredMixin, AsyncAction, View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
order = get_object_or_404(self.request.event.orders, code=request.GET.get("code"))
|
||||
cf = CachedFile()
|
||||
cf = CachedFile(web_download=True, session_key=self.request.session.session_key)
|
||||
cf.date = now()
|
||||
cf.type = 'application/pdf'
|
||||
cf.expires = now() + timedelta(days=3)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
@@ -7,9 +6,8 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import CheckinList, Item, Order, SubEvent
|
||||
from pretix.control.forms import CachedFileField
|
||||
from pretix.control.forms import ExtFileField
|
||||
from pretix.control.forms.widgets import Select2, Select2Multiple
|
||||
|
||||
|
||||
@@ -23,7 +21,7 @@ class MailForm(forms.Form):
|
||||
sendto = forms.MultipleChoiceField() # overridden later
|
||||
subject = forms.CharField(label=_("Subject"))
|
||||
message = forms.CharField(label=_("Message"))
|
||||
attachment = CachedFileField(
|
||||
attachment = ExtFileField(
|
||||
label=_("Attachment"),
|
||||
required=False,
|
||||
ext_whitelist=(
|
||||
@@ -55,24 +53,6 @@ class MailForm(forms.Form):
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
subevents_from = forms.SplitDateTimeField(
|
||||
widget=SplitDateTimePickerWidget(),
|
||||
label=pgettext_lazy('subevent', 'Only send to customers of dates starting at or after'),
|
||||
required=False,
|
||||
)
|
||||
subevents_to = forms.SplitDateTimeField(
|
||||
widget=SplitDateTimePickerWidget(),
|
||||
label=pgettext_lazy('subevent', 'Only send to customers of dates starting before'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('subevent') and d.get('subevents_from'):
|
||||
raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.'))
|
||||
if bool(d.get('subevents_from')) != bool(d.get('subevents_to')):
|
||||
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
|
||||
return d
|
||||
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
phs = [
|
||||
@@ -165,5 +145,3 @@ class MailForm(forms.Form):
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
del self.fields['subevents_from']
|
||||
del self.fields['subevents_to']
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
{% endif %}
|
||||
{% if log.pdata.subevent_obj %}
|
||||
<br/><span class="fa fa-calendar fa-fw"></span> {{ log.pdata.subevent_obj }}
|
||||
{% elif log.pdata.subevents_from %}
|
||||
<br/><span class="fa fa-calendar fa-fw"></span> {{ log.pdata.subevents_from }} – {{ log.pdata.subevents_to }}
|
||||
{% endif %}
|
||||
{% if log.pdata.recipients == "attendees" %}
|
||||
<br/><span class="fa fa-envelope fa-fw"></span> {% trans "Attendee contact addresses" %}
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
{% bootstrap_field form.sendto layout='horizontal' %}
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout='horizontal' %}
|
||||
{% bootstrap_field form.subevents_from layout='horizontal' %}
|
||||
{% bootstrap_field form.subevents_to layout='horizontal' %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.items layout='horizontal' %}
|
||||
<div class="row">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import bleach
|
||||
import dateutil
|
||||
from django.contrib import messages
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.http import Http404
|
||||
@@ -12,7 +12,7 @@ from django.views.generic import FormView, ListView
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.i18n import LazyI18nString, language
|
||||
from pretix.base.models import LogEntry, Order, OrderPosition
|
||||
from pretix.base.models import CachedFile, LogEntry, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.mail import TolerantDict
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
@@ -60,10 +60,6 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
)
|
||||
kwargs['initial']['filter_checkins'] = logentry.parsed_data.get('filter_checkins', False)
|
||||
kwargs['initial']['not_checked_in'] = logentry.parsed_data.get('not_checked_in', False)
|
||||
if logentry.parsed_data.get('subevents_from'):
|
||||
kwargs['initial']['subevents_from'] = dateutil.parser.parse(logentry.parsed_data['subevents_from'])
|
||||
if logentry.parsed_data.get('subevents_to'):
|
||||
kwargs['initial']['subevents_to'] = dateutil.parser.parse(logentry.parsed_data['subevents_to'])
|
||||
if logentry.parsed_data.get('subevent'):
|
||||
try:
|
||||
kwargs['initial']['subevent'] = self.request.event.subevents.get(
|
||||
@@ -109,10 +105,6 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
|
||||
if form.cleaned_data.get('subevent'):
|
||||
opq = opq.filter(subevent=form.cleaned_data.get('subevent'))
|
||||
if form.cleaned_data.get('subevents_from'):
|
||||
opq = opq.filter(subevent__date_from__gte=form.cleaned_data.get('subevents_from'))
|
||||
if form.cleaned_data.get('subevents_to'):
|
||||
opq = opq.filter(subevent__date_from__lt=form.cleaned_data.get('subevents_to'))
|
||||
|
||||
orders = orders.annotate(match_pos=Exists(opq)).filter(match_pos=True).distinct()
|
||||
|
||||
@@ -123,6 +115,7 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
|
||||
if self.request.POST.get("action") == "preview":
|
||||
for l in self.request.event.settings.locales:
|
||||
|
||||
with language(l):
|
||||
context_dict = TolerantDict()
|
||||
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
|
||||
@@ -144,6 +137,17 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
|
||||
attachment = None
|
||||
if 'attachment' in self.request.FILES:
|
||||
attachment = self.request.FILES['attachment']
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + timedelta(days=1),
|
||||
date=now(),
|
||||
filename=attachment.name,
|
||||
type=attachment.content_type,
|
||||
)
|
||||
cf.file.save(attachment.name, attachment.file)
|
||||
cf.save()
|
||||
kwargs = {
|
||||
'recipients': form.cleaned_data['recipients'],
|
||||
'event': self.request.event.pk,
|
||||
@@ -156,8 +160,8 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
'checkin_lists': [i.pk for i in form.cleaned_data.get('checkin_lists')],
|
||||
'filter_checkins': form.cleaned_data.get('filter_checkins'),
|
||||
}
|
||||
if form.cleaned_data.get('attachment') is not None:
|
||||
kwargs['attachments'] = [form.cleaned_data['attachment'].id]
|
||||
if attachment is not None:
|
||||
kwargs['attachments'] = [cf.id]
|
||||
|
||||
send_mails.apply_async(
|
||||
kwargs=kwargs
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h3>{% trans "Choose date to book a ticket" %}</h3>
|
||||
<h3>{% trans "Choose date to buy a ticket" %}</h3>
|
||||
{% endif %}
|
||||
<div class="panel panel-default subevent-list">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -192,7 +192,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
qs = qs.filter(category__pk__in=self.request.GET.get('categories').split(","))
|
||||
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.request.event, subevent=self.subevent, voucher=self.voucher, channel=self.request.sales_channel.identifier,
|
||||
self.request.event, subevent=self.subevent, voucher=self.voucher, channel='web',
|
||||
base_qs=qs
|
||||
)
|
||||
|
||||
@@ -536,7 +536,6 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
str(self.subevent.pk) if self.subevent else "",
|
||||
request.GET.urlencode(),
|
||||
get_language(),
|
||||
request.sales_channel.identifier,
|
||||
])
|
||||
if "cart_id" not in request.GET:
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
@@ -351,7 +351,7 @@ CORE_MODULES = {
|
||||
MIDDLEWARE = [
|
||||
'pretix.api.middleware.IdempotencyMiddleware',
|
||||
'pretix.multidomain.middlewares.MultiDomainMiddleware',
|
||||
'pretix.base.middleware.CustomCommonMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'pretix.multidomain.middlewares.SessionMiddleware',
|
||||
'pretix.multidomain.middlewares.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
@@ -375,7 +375,7 @@ except ImportError:
|
||||
|
||||
|
||||
if METRICS_ENABLED:
|
||||
MIDDLEWARE.insert(MIDDLEWARE.index('pretix.base.middleware.CustomCommonMiddleware') + 1,
|
||||
MIDDLEWARE.insert(MIDDLEWARE.index('django.middleware.common.CommonMiddleware') + 1,
|
||||
'pretix.helpers.metrics.middleware.MetricsMiddleware')
|
||||
|
||||
|
||||
@@ -433,7 +433,7 @@ LANGUAGES_OFFICIAL = {
|
||||
'en', 'de', 'de-informal'
|
||||
}
|
||||
LANGUAGES_INCUBATING = {
|
||||
'pl', 'fi', 'pt-br'
|
||||
'pt-br', 'pl', 'fi', 'pt-pt'
|
||||
} - set(config.get('languages', 'allow_incubating', fallback='').split(','))
|
||||
LANGUAGES_RTL = {
|
||||
'ar', 'hw'
|
||||
@@ -472,12 +472,6 @@ EXTRA_LANG_INFO = {
|
||||
'name': 'Latvian',
|
||||
'name_local': 'Latviešu'
|
||||
},
|
||||
'pt-pt': {
|
||||
'bidi': False,
|
||||
'code': 'pt-pt',
|
||||
'name': 'Portuguese',
|
||||
'name_local': 'Português',
|
||||
},
|
||||
}
|
||||
|
||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||
|
||||
@@ -210,7 +210,6 @@ pre[lang=pn], input[lang=pn], textarea[lang=pn], div[lang=pn] { background-image
|
||||
pre[lang=pr], input[lang=pr], textarea[lang=pr], div[lang=pr] { background-image: url(static('pretixbase/img/flags/pr.png')); }
|
||||
pre[lang=ps], input[lang=ps], textarea[lang=ps], div[lang=ps] { background-image: url(static('pretixbase/img/flags/ps.png')); }
|
||||
pre[lang=pt], input[lang=pt], textarea[lang=pt], div[lang=pt] { background-image: url(static('pretixbase/img/flags/pt.png')); }
|
||||
pre[lang=pt-pt], input[lang=pt-pt], textarea[lang=pt-pt], div[lang=pt-pt] { background-image: url(static('pretixbase/img/flags/pt.png')); }
|
||||
pre[lang=pw], input[lang=pw], textarea[lang=pw], div[lang=pw] { background-image: url(static('pretixbase/img/flags/pw.png')); }
|
||||
pre[lang=py], input[lang=py], textarea[lang=py], div[lang=py] { background-image: url(static('pretixbase/img/flags/py.png')); }
|
||||
pre[lang=qa], input[lang=qa], textarea[lang=qa], div[lang=qa] { background-image: url(static('pretixbase/img/flags/qa.png')); }
|
||||
|
||||
@@ -54,7 +54,7 @@ class EventCancelTests(TestCase):
|
||||
self.order.save()
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
)
|
||||
@@ -69,7 +69,7 @@ class EventCancelTests(TestCase):
|
||||
self.op1.save()
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -91,7 +91,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -119,7 +119,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -128,84 +128,6 @@ class EventCancelTests(TestCase):
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
assert not self.order.refunds.exists()
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_refund_paid_with_per_ticket_fees(self):
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
self.order.payments.create(
|
||||
amount=Decimal('46.00'),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
provider='giftcard',
|
||||
info='{"gift_card": %d}' % gc.pk
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
assert r.amount == Decimal('42.00')
|
||||
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_refund_paid_with_per_ticket_fees_ignore_free(self):
|
||||
self.op1.price = Decimal('46.00')
|
||||
self.op1.save()
|
||||
self.op2.price = Decimal('0.00')
|
||||
self.op2.save()
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
self.order.payments.create(
|
||||
amount=Decimal('46.00'),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
provider='giftcard',
|
||||
info='{"gift_card": %d}' % gc.pk
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
assert r.amount == Decimal('44.00')
|
||||
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_refund_paid_with_per_ticket_fees_ignore_addon(self):
|
||||
self.op2.addon_to = self.op1
|
||||
self.op2.save()
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
self.order.payments.create(
|
||||
amount=Decimal('46.00'),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
provider='giftcard',
|
||||
info='{"gift_card": %d}' % gc.pk
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
assert r.amount == Decimal('44.00')
|
||||
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_refund_paid_with_fees(self):
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
@@ -220,7 +142,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -248,7 +170,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -279,7 +201,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT],
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None
|
||||
)
|
||||
r = self.order.refunds.get()
|
||||
@@ -315,7 +237,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT],
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -344,7 +266,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None, manual_refund=True,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -380,7 +302,7 @@ class EventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None, manual_refund=False,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -400,7 +322,7 @@ class SubEventCancelTests(TestCase):
|
||||
with scope(organizer=self.o):
|
||||
self.event = Event.objects.create(organizer=self.o, name='Dummy', slug='dummy', date_from=now(),
|
||||
plugins='tests.testdummy', has_subevents=True)
|
||||
self.se1 = self.event.subevents.create(name='One', date_from=now() - timedelta(days=30))
|
||||
self.se1 = self.event.subevents.create(name='One', date_from=now())
|
||||
self.se2 = self.event.subevents.create(name='Two', date_from=now())
|
||||
self.order = Order.objects.create(
|
||||
code='FOO', event=self.event, email='dummy@dummy.test',
|
||||
@@ -429,7 +351,7 @@ class SubEventCancelTests(TestCase):
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -438,34 +360,13 @@ class SubEventCancelTests(TestCase):
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
assert self.order.positions.count() == 1
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_subevent_range(self):
|
||||
self.op2.subevent = self.se1
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_simple_order(self):
|
||||
self.op2.subevent = self.se1
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -476,7 +377,7 @@ class SubEventCancelTests(TestCase):
|
||||
def test_cancel_all_subevents(self):
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -496,7 +397,7 @@ class SubEventCancelTests(TestCase):
|
||||
self.order.save()
|
||||
cancel_event(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
)
|
||||
@@ -504,27 +405,6 @@ class SubEventCancelTests(TestCase):
|
||||
assert self.order.status == Order.STATUS_PAID
|
||||
assert '23.00' in djmail.outbox[0].body
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_mixed_order_range(self):
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
assert self.order.positions.count() == 2
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
assert self.order.positions.filter(subevent=self.se1, canceled=False).count() == 0
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_partially_keep_fees(self):
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
@@ -545,7 +425,7 @@ class SubEventCancelTests(TestCase):
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
@@ -562,31 +442,6 @@ class SubEventCancelTests(TestCase):
|
||||
f = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_CANCELLATION)
|
||||
assert f.value == Decimal('1.80')
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_partially_keep_fees_per_ticket(self):
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
self.order.payments.create(
|
||||
amount=Decimal('46.00'),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
provider='giftcard',
|
||||
info='{"gift_card": %d}' % gc.pk
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
)
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
assert r.amount == Decimal('21.00')
|
||||
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
|
||||
f = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_CANCELLATION)
|
||||
assert f.value == Decimal('2.00')
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_send_mail_waitinglist(self):
|
||||
v = Voucher.objects.create(event=self.event, block_quota=True, redeemed=1)
|
||||
@@ -598,7 +453,7 @@ class SubEventCancelTests(TestCase):
|
||||
)
|
||||
cancel_event(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
send_waitinglist=True, send_waitinglist_message="Event canceled", send_waitinglist_subject=":(",
|
||||
user=None
|
||||
|
||||
33
src/tests/base/test_rich_text.py
Normal file
33
src/tests/base/test_rich_text.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from pretix.base.templatetags.rich_text import rich_text, rich_text_snippet, markdown_compile_email
|
||||
|
||||
|
||||
@pytest.mark.parametrize("link", [
|
||||
# Test link detection
|
||||
("google.com",
|
||||
'<a href="http://google.com" rel="noopener" target="_blank">google.com</a>'),
|
||||
# Test abslink_callback
|
||||
("[Call](tel:+12345)",
|
||||
'<a href="tel:+12345" rel="nofollow">Call</a>'),
|
||||
("[Foo](/foo)",
|
||||
'<a href="http://example.com/foo" rel="noopener" target="_blank">Foo</a>'),
|
||||
("mail@example.org",
|
||||
'<a href="mailto:mail@example.org">mailto:mail@example.org</a>'),
|
||||
# Test truelink_callback
|
||||
('<a href="https://evilsite.com">Evil Site</a>',
|
||||
'<a href="https://evilsite.com" rel="noopener" target="_blank">Evil Site</a>'),
|
||||
('<a href="https://evilsite.com">evilsite.com</a>',
|
||||
'<a href="https://evilsite.com" rel="noopener" target="_blank">evilsite.com</a>'),
|
||||
('<a href="https://evilsite.com">goodsite.com</a>',
|
||||
'<a href="https://evilsite.com" rel="noopener" target="_blank">https://evilsite.com</a>'),
|
||||
('<a href="https://goodsite.com.evilsite.com">goodsite.com</a>',
|
||||
'<a href="https://goodsite.com.evilsite.com" rel="noopener" target="_blank">https://goodsite.com.evilsite.com</a>'),
|
||||
('<a href="https://evilsite.com/deep/path">evilsite.com</a>',
|
||||
'<a href="https://evilsite.com/deep/path" rel="noopener" target="_blank">evilsite.com</a>'),
|
||||
])
|
||||
def test_linkify_abs(link):
|
||||
input, output = link
|
||||
assert rich_text_snippet(input, safelinks=False) == output
|
||||
assert rich_text(input, safelinks=False) == f'<p>{output}</p>'
|
||||
assert markdown_compile_email(input) == f'<p>{output}</p>'
|
||||
@@ -1,81 +0,0 @@
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.base.secrets import (
|
||||
RandomTicketSecretGenerator, Sig1TicketSecretGenerator,
|
||||
)
|
||||
|
||||
schemes = (
|
||||
(RandomTicketSecretGenerator, False),
|
||||
(Sig1TicketSecretGenerator, True),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def event():
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(),
|
||||
plugins='pretix.plugins.banktransfer'
|
||||
)
|
||||
with scope(organizer=o):
|
||||
yield event
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("scheme", schemes)
|
||||
def test_force_invalidate(event, scheme):
|
||||
item = event.items.create(name="Foo", default_price=0)
|
||||
generator, input_dependent = scheme
|
||||
g = generator(event)
|
||||
|
||||
first = g.generate_secret(item, None, None, current_secret=None, force_invalidate=False)
|
||||
assert first
|
||||
second = g.generate_secret(item, None, None, current_secret=first, force_invalidate=True)
|
||||
assert first != second
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("scheme", schemes)
|
||||
def test_keep_same(event, scheme):
|
||||
item = event.items.create(name="Foo", default_price=0)
|
||||
generator, input_dependent = scheme
|
||||
g = generator(event)
|
||||
|
||||
first = g.generate_secret(item, None, None, current_secret=None, force_invalidate=False)
|
||||
assert first
|
||||
second = g.generate_secret(item, None, None, current_secret=first, force_invalidate=False)
|
||||
assert first == second
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("scheme", schemes)
|
||||
def test_change_if_required(event, scheme):
|
||||
item = event.items.create(name="Foo", default_price=0)
|
||||
item2 = event.items.create(name="Bar", default_price=0)
|
||||
generator, input_dependent = scheme
|
||||
g = generator(event)
|
||||
|
||||
first = g.generate_secret(item, None, None, current_secret=None, force_invalidate=False)
|
||||
assert first
|
||||
second = g.generate_secret(item2, None, None, current_secret=first, force_invalidate=False)
|
||||
if input_dependent:
|
||||
assert first != second
|
||||
else:
|
||||
assert first == second
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("scheme", schemes)
|
||||
def test_change_if_invalid(event, scheme):
|
||||
item = event.items.create(name="Foo", default_price=0)
|
||||
generator, input_dependent = scheme
|
||||
g = generator(event)
|
||||
|
||||
first = "blafasel"
|
||||
second = g.generate_secret(item, None, None, current_secret=first, force_invalidate=False)
|
||||
if input_dependent:
|
||||
assert first != second
|
||||
@@ -90,6 +90,10 @@ class LoginFormTest(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
|
||||
response = self.client.get('/control/login?next=//evilsite.com')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/', response['Location'])
|
||||
|
||||
def test_logout(self):
|
||||
response = self.client.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
|
||||
@@ -117,8 +117,6 @@ event_urls = [
|
||||
"orders/ABC/refunds/1/process",
|
||||
"orders/ABC/refunds/1/done",
|
||||
"orders/ABC/delete",
|
||||
"orders/ABC/sendmail",
|
||||
"orders/ABC/1/sendmail",
|
||||
"orders/ABC/",
|
||||
"orders/",
|
||||
"orders/import/",
|
||||
@@ -292,8 +290,6 @@ event_permission_urls = [
|
||||
("can_change_orders", "orders/FOO/delete", 302),
|
||||
("can_change_orders", "orders/FOO/comment", 405),
|
||||
("can_change_orders", "orders/FOO/locale", 200),
|
||||
("can_change_orders", "orders/FOO/sendmail", 200),
|
||||
("can_change_orders", "orders/FOO/1/sendmail", 404),
|
||||
("can_change_orders", "orders/import/", 200),
|
||||
("can_change_orders", "orders/import/0ab7b081-92d3-4480-82de-2f8b056fd32f/", 404),
|
||||
("can_view_orders", "orders/FOO/answer/5/", 404),
|
||||
|
||||
Reference in New Issue
Block a user