Compare commits

..

10 Commits

Author SHA1 Message Date
Raphael Michel
6f965edc22 Bump to 3.12.1 2020-12-22 11:36:30 +01:00
Raphael Michel
508538aa76 Correct backporting of migrations 2020-12-22 11:29:34 +01:00
Raphael Michel
4511963aca Reduce lifetime of export files 2020-12-22 11:16:42 +01:00
Raphael Michel
2c9277d11b [SECURITY] Rate limiting for login 2020-12-22 11:16:32 +01:00
Raphael Michel
93ee5450ec [SECURITY] Rate limiting for password change form 2020-12-22 11:16:32 +01:00
Raphael Michel
e1b3e20148 [SECURITY] Bind relevant cached file downloads to the current session 2020-12-22 11:16:30 +01:00
Raphael Michel
880e3fd93e [SECURITY] Fix unvalidated redirect 2020-12-22 11:14:51 +01:00
Raphael Michel
cea201af16 [SECURITY] Prevent phishing through misleading link titles 2020-12-22 11:14:50 +01:00
Raphael Michel
93252e1645 Clarify MANIFEST.in 2020-10-26 10:46:00 +01:00
Raphael Michel
65f8b68634 Fix packaging bugs 2020-10-26 10:35:33 +01:00
48 changed files with 3032 additions and 2125 deletions

View File

@@ -1 +1 @@
__version__ = "3.13.0.dev0"
__version__ = "3.12.1"

View File

@@ -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):

View File

@@ -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

View File

@@ -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'

View File

@@ -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'],

View File

@@ -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

View 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),
),
]

View File

@@ -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',
),
]

View 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 = [
]

View File

@@ -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)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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',

View File

@@ -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)

View File

@@ -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',
},
}),
])

View File

@@ -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 %}

View File

@@ -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))

View File

@@ -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()

View File

@@ -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 = ''

View File

@@ -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):

View File

@@ -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

View File

@@ -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." %}

View File

@@ -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 %}&nbsp;&nbsp;
<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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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(),

View File

@@ -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

View File

@@ -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'),

View File

@@ -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,

View File

@@ -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'

View File

@@ -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):

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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']

View File

@@ -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" %}

View File

@@ -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">

View File

@@ -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

View File

@@ -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">

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')); }

View File

@@ -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

View 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>'

View File

@@ -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

View File

@@ -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',

View File

@@ -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),