Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel faa97026e1 Tests 2026-07-03 12:14:08 +02:00
Raphael Michel e1736e8d2a Mail setup: Add DKIM + DMARC validation 2026-07-03 11:57:00 +02:00
183 changed files with 562563 additions and 497638 deletions
+6 -6
View File
@@ -53,8 +53,8 @@ dependencies = [
"django-oauth-toolkit==2.3.*",
"django-otp==1.7.*",
"django-phonenumber-field==8.4.*",
"django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-querytagger==0.0.2",
"django-redis==6.0.*",
"django-scopes==2.0.*",
"django-statici18n==2.7.*",
"djangorestframework==3.17.*",
@@ -77,7 +77,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.13.*",
"phonenumberslite==9.0.*",
"Pillow==12.3.*",
"Pillow==12.2.*",
"pretix-plugin-build",
"protobuf==7.35.*",
"psycopg2-binary",
@@ -92,8 +92,8 @@ dependencies = [
"pyuca",
"qrcode==8.2",
"redis==7.4.*",
"reportlab==5.0.*",
"requests==2.34.*",
"reportlab==4.5.*",
"requests==2.32.*",
"sentry-sdk==2.64.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
@@ -102,7 +102,7 @@ dependencies = [
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
"webauthn==3.0.*",
"webauthn==2.8.*",
"zeep==4.3.*"
]
-1
View File
@@ -747,7 +747,6 @@ class EventSettingsSerializer(SettingsSerializer):
'max_items_per_order',
'reservation_time',
'contact_mail',
'contact_url',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
+2 -5
View File
@@ -27,7 +27,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -492,9 +492,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.context['organizer'].name,
},
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
@@ -576,7 +574,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'contact_url',
'imprint_url',
'organizer_info_text',
'event_list_type',
+10 -16
View File
@@ -53,7 +53,6 @@ from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
@@ -325,21 +324,16 @@ class WrappedPhonePrefixSelect(Select):
initial = None
def __init__(self, initial=None):
def _get_choices():
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
return choices
choices = lazy(_get_choices, list)()
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code',
@@ -1404,7 +1398,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
elif self.validate_vat_id and vat_id_applicable:
try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id_validated = True
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
if self.all_optional:
@@ -1,29 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Do nothing. Useful for startup performance testing."
def handle(self, *args, **options):
pass
@@ -40,7 +40,6 @@ from django.core.cache import cache
from django.core.management.base import BaseCommand
from django.db import close_old_connections
from django.dispatch.dispatcher import NO_RECEIVERS
from django_querytagger.tagging import with_tag
from pretix.helpers.periodic import SKIPPED
@@ -83,8 +82,7 @@ class Command(BaseCommand):
try:
# Check if the DB connection is still good, it might be closed if the previous task took too long.
close_old_connections()
with with_tag(f"periodictask={name}"):
r = receiver(signal=periodic_task, sender=self)
r = receiver(signal=periodic_task, sender=self)
except Exception as err:
if isinstance(err, KeyboardInterrupt):
raise err
@@ -1,58 +0,0 @@
# Generated by Django 4.2.8 on 2024-07-01 09:27
import logging
from django.db import migrations
from django.db.models import Count
logger = logging.getLogger(__name__)
def clean_duplicate_secrets(apps, schema_editor):
# This will autofix all possible duplicate Order.code and OrderPosition.secret values,
# unless Order.code is already too long to append something. This would need to be fixed by
# sysadmins manually.
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
Order = apps.get_model("pretixbase", "Order")
qs = OrderPosition.all.values("secret", "order__event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
for row in qs:
affected = OrderPosition.all.filter(
**{k: v for k, v in row.items() if k != "c"}
).order_by("pk")
logger.error(f"Found {row['c']} tickets with with the same secret \"{row['secret']}\" in organizer {row['order__event__organizer_id']}, all except one will be changed")
for i, a in enumerate(affected):
if i > 0:
a.secret = a.secret + "__dupl__" + str(a.pk)
logger.info(
f"Ticket {a.pk} has new secret {a.secret}"
)
a.save(update_fields=["organizer_id", "secret"])
qs = Order.objects.values("code", "event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
for row in qs:
affected = Order.objects.filter(
**{k: v for k, v in row.items() if k != "c"}
).order_by("pk")
logger.error(f"Found {row['c']} orders with with the same code \"{row['code']}\" in organizer {row['event__organizer_id']}, all except one will be changed")
for i, a in enumerate(affected):
if i > 0:
if len(a.code) > 16 - len(str(a.pk)):
raise ValueError(f"Cannot auto-fix order with duplicate code {a.code}, order code is too long already")
a.code = a.code + str(a.pk).zfill(16 - len(a.code))
logger.info(
f"Order {a.pk} has new code {a.code}"
)
a.save(update_fields=["organizer_id", "code"])
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0301_reusablemedium_remove_orderposition",
),
]
operations = [
migrations.RunPython(clean_duplicate_secrets, migrations.RunPython.noop),
]
@@ -1,46 +0,0 @@
# Generated by Django 4.2.8 on 2024-07-01 09:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0302_resolve_duplicate_codes_and_secrets",
),
]
operations = [
migrations.RunSQL(
"UPDATE pretixbase_order "
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_event e WHERE e.id = pretixbase_order.event_id) "
"WHERE pretixbase_order.organizer_id IS NULL;",
migrations.RunSQL.noop,
),
migrations.RunSQL(
"UPDATE pretixbase_orderposition "
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_order o LEFT JOIN pretixbase_event e ON e.id = o.event_id WHERE o.id = pretixbase_orderposition.order_id) "
"WHERE pretixbase_orderposition.organizer_id IS NULL;",
migrations.RunSQL.noop,
),
migrations.AlterField(
model_name="order",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="orders",
to="pretixbase.organizer",
),
),
migrations.AlterField(
model_name="orderposition",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="order_positions",
to="pretixbase.organizer",
),
),
]
+5 -8
View File
@@ -373,7 +373,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
email or self.email,
_('Changes to your account'),
_('Account information changed'),
'pretixcontrol/email/security_notice.txt',
{
'user': self,
@@ -400,13 +400,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
with language(self.locale):
if reason == 'email_change':
msg = str(_('To change your email address from {old_email} to {new_email}, use the following code:').format(
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
old_email=self.email, new_email=email,
))
elif reason == 'email_verify':
msg = str(_('To verify your email address {email} on {instance}, use the following code:').format(
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
email=self.email,
instance=settings.PRETIX_INSTANCE_NAME,
))
else:
raise Exception('Invalid confirmation code reason')
@@ -419,7 +418,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
}
mail(
email or self.email,
_('Your confirmation code'),
_('pretix confirmation code'),
'pretixcontrol/email/confirmation_code.txt',
{
'user': self,
@@ -463,9 +462,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
from pretix.base.services.mail import mail
mail(
self.email,
_('Reset your password'),
'pretixcontrol/email/forgot.txt',
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
+6 -2
View File
@@ -224,6 +224,8 @@ class Order(LockModel, LoggedModel):
"Organizer",
related_name="orders",
on_delete=models.CASCADE,
null=True,
blank=True,
)
event = models.ForeignKey(
Event,
@@ -327,7 +329,7 @@ class Order(LockModel, LoggedModel):
default="line",
)
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
class Meta:
verbose_name = _("Order")
@@ -2539,6 +2541,8 @@ class OrderPosition(AbstractPosition):
"Organizer",
related_name="order_positions",
on_delete=models.CASCADE,
null=True,
blank=True,
)
order = models.ForeignKey(
Order,
@@ -2595,7 +2599,7 @@ class OrderPosition(AbstractPosition):
blank=True,
)
all = ScopedManager(organizer='organizer')
all = ScopedManager(organizer='order__event__organizer')
objects = ActivePositionManager()
def __init__(self, *args, **kwargs):
+1 -1
View File
@@ -834,7 +834,7 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
"""
After the user has confirmed their purchase, this method will be called to complete
the payment process. This is the place to actually move the money if applicable.
+5 -10
View File
@@ -77,7 +77,6 @@ from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -402,7 +401,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
("event_info_text", {}), # Placeholder to "reserve" position, defined later in `get_variables`
("event_info_text", {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
}),
("now_date", {
"label": _("Printing date"),
"editor_sample": _("2017-05-31"),
@@ -667,14 +670,6 @@ def get_images(event):
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
templating_context = PlaceholderContext(event=event)
v['event_info_text'] = {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev:
templating_context.format(str(order.event.settings.event_info_text))
}
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
+1 -3
View File
@@ -22,7 +22,6 @@
import logging
from decimal import Decimal
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
from django.utils.crypto import get_random_string
@@ -378,13 +377,12 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
confirmation_code = get_random_string(8, allowed_chars="01234567890")
mail(
user.email,
subject=gettext('Confirm event cancellation and bulk refund'),
subject=gettext('Bulk-refund confirmation'),
template='pretixbase/email/cancel_confirm.txt',
context={
"event": str(event),
"amount": money_filter(refund_total, event.currency),
"confirmation_code": confirmation_code,
"instance": settings.PRETIX_INSTANCE_NAME,
},
locale=user.locale,
)
+1 -2
View File
@@ -340,13 +340,12 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
if schedule.owner.is_active:
mail(
email=schedule.owner.email,
subject=gettext('Scheduled export failed'),
subject=gettext('Export failed'),
template='pretixbase/email/export_failed.txt',
context={
'configuration_url': config_url,
'reason': msg,
'soft': soft,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
+3 -4
View File
@@ -44,7 +44,7 @@ from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name
@@ -171,16 +171,15 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
if user:
with language(user.locale):
event_name = str(event.name)
mail(
user.email,
gettext('Data shredding completed for %(event)s') % {'event': event_name},
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': event_name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
+1 -12
View File
@@ -1276,7 +1276,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict(
label=_("Allow updating existing invoices"),
label=_("Allow to update existing invoices"),
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
"recommend to leave this option turned off and always issue a new invoice if a change needs "
"to be made."),
@@ -2284,17 +2284,6 @@ DEFAULTS = {
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
},
'contact_url': {
'default': None,
'type': str,
'serializer_class': serializers.URLField,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Contact URL"),
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
},
'imprint_url': {
'default': None,
'type': str,
@@ -1,14 +1,10 @@
{% load i18n %}
{% trans "You requested to cancel an event that involves a large bulk refund:" %}
{% trans "You have requested us to cancel an event which includes a larger bulk-refund:" %}
- {% trans "Event" %}: {{ event }}
- {% trans "Estimated refund" %}: **{{ amount }}**
{% trans "Event" %}: {{ event }}
{% trans "To confirm, paste the following code into the cancellation form:" %}
{% trans "Estimated refund amount" %}: **{{ amount }}**
{{ confirmation_code }}
{% trans "Please confirm that you want to proceed by coping the following confirmation code into the cancellation form:" %}
{% blocktrans with instance=instance %}Don't share this code with anyone. The {{ instance }} team will never ask you for it.{% endblocktrans %}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
**{{ confirmation_code }}**
@@ -1,13 +1,12 @@
{% load i18n %}
{% trans "Your scheduled export failed." %}
{% trans "Your export failed." %}
- {% trans "Reason" %}: {{ reason }}
{% trans "Reason:" %} {{ reason }}
{% if not soft %}{% trans "If an export fails five times in a row, we'll stop sending it." %}{% endif %}
{% if not soft %}
{% trans "If your export fails five times in a row, it will no longer be sent." %}
{% endif %}
{% trans "You can adjust or remove this export here:" %}
{% trans "Configuration link:" %}
{{ configuration_url }}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
@@ -52,13 +52,13 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You're receiving this email based on your notification settings." %}<br>
{% trans "You receive these emails based on your notification settings." %}<br>
<a href="{{ settings_url }}">
{% trans "Manage settings" %}
{% trans "Click here to view and change your notification settings" %}
</a>
{% if disable_url %}<br>
<a href="{{ disable_url }}">
{% trans "Disable all notifications" %}
{% trans "Click here disable all notifications immediately." %}
</a>
{% endif %}
</div>
@@ -1,21 +1,19 @@
{% load i18n %}
{{ notification.title }}{% if notification.detail %}
{{ notification.detail }}{% endif %}{% if notification.url %}
{{ notification.detail }}
{% endif %}{% if notification.url %}
{{ notification.url }}{% endif %}{% if notification.attributes %}
{{ notification.url }}{% endif %}{% for attr in notification.attributes %}
{% for attr in notification.attributes %}- {{ attr.title }}: {{ attr.value }}
{% endfor %}{% endif %}{% for action in notification.actions %}
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %}
{{ action.label }}:
{{ action.label }}
{{ action.url }}{% endfor %}
{{ action.url }}{% endfor %}
---
{% trans "You're receiving this email based on your notification settings." %}
- {% trans "Manage settings" %}: {{ settings_url }}
{% if disable_url %}- {% trans "Disable all notifications" %}: {{ disable_url }}
{% trans "You receive these emails based on your notification settings." %}
{% trans "Click here to view and change your notification settings:" %}
{{ settings_url }}
{% if disable_url %}{% trans "Click here disable all notifications immediately:" %}
{{ disable_url }}
{% endif %}
@@ -1,14 +1,17 @@
{% load i18n %}{% blocktrans %}Hello,
{% load i18n %}
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
The following data shredding job has been completed:
we hereby confirm that the following data shredding job has been completed:
- Organizer: {{ organizer }}
- Event: {{ event }}
- Data selection: {{ shredders }}
- Start time: {{ start_time }}
Organizer: {{ organizer }}
Any data added to the event after the start time may not have been deleted.
Event: {{ event }}
Thanks,
The {{ instance }} Team
Data selection: {{ shredders }}
Start time: {{ start_time }} (new data added after this time might not have been deleted)
Best regards,
Your {{ instance }} team
{% endblocktrans %}
+1 -43
View File
@@ -19,56 +19,14 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import os
from celery import Celery, signals
from django.dispatch import receiver
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
logger = logging.getLogger(__name__)
from django.conf import settings
app = Celery('pretix')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
@receiver(signals.before_task_publish)
def on_before_task_publish(sender, body, exchange, routing_key, headers, properties, declare, retry_policy, **kwargs):
from pretix.helpers.logs import local
trace = getattr(local, 'trace', [])
request_id = getattr(local, 'request_id', None)
if request_id:
trace.append(request_id)
headers["X-Pretix-Trace"] = " ".join(trace)
@receiver(signals.task_received)
def on_task_received(sender, request, **kwargs):
trace = request._request_dict.get("X-Pretix-Trace")
if trace:
logger.info(f"Task {request.id} has trace {trace}")
@receiver(signals.task_prerun)
def on_task_prerun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = task_id
if "X-Pretix-Trace" in task.request.headers:
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
else:
local.trace = []
local.trace.append(task_id)
@receiver(signals.task_postrun)
def on_task_postrun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = None
local.trace = []
+1 -13
View File
@@ -625,7 +625,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'max_items_per_order',
'reservation_time',
'contact_mail',
'contact_url',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
@@ -672,11 +671,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
base_context = {
'frontpage_text': ['event'],
'presale_has_ended_text': ['event'],
'voucher_explanation_text': ['event'],
'banner_text': ['event'],
'banner_text_bottom': ['event'],
'event_info_text': ['event'],
}
def _resolve_virtual_keys_input(self, data, prefix=''):
@@ -1679,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
choices=CountriesAndEUAndStates(),
required=False
)
address_type = forms.ChoiceField(
@@ -1905,12 +1899,6 @@ class QuickSetupForm(I18nForm):
required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
contact_url = forms.URLField(
label=_("Contact URL"),
required=False,
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
total_quota = forms.IntegerField(
label=_("Total capacity"),
min_value=0,
-1
View File
@@ -604,7 +604,6 @@ class OrganizerSettingsForm(SettingsForm):
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'contact_url',
'imprint_url',
'organizer_info_text',
'event_list_type',
-5
View File
@@ -106,11 +106,6 @@ class VoucherForm(I18nModelForm):
pass
super().__init__(*args, **kwargs)
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
})
if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all()
self.fields['subevent'].widget = Select2(
@@ -1,13 +1,13 @@
{% load i18n %}{% blocktrans with code=code reason=reason instance=instance %}Hello,
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
{{ reason }}
{{ code }}
Don't share this code with anyone. The {{ instance }} team will never ask you for it.
Please do never give this code to another person. Our support team will never ask for this code.
If you didn't request this code, please contact us immediately.
If this code was not requested by you, please contact us immediately.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,15 +1,14 @@
{% load i18n %}{% blocktrans with code=code address=address instance=instance %}Hello,
{% load i18n %}{% blocktrans with code=code instance=instance %}Hello,
Someone requested to use {{ address }} as a sender address on {{ instance }}. Once verified, emails sent from {{ instance }} can show this address as the sender.
someone requested to use {{ address }} as a sender address on {{ instance }}.
This will allow them to send emails that are shown to originate from this email address.
If that was you, please enter the following confirmation code:
If this was you, enter the following code in the setup form:
{{ code }}
{{ code }}
If this was not requested by you, you can safely ignore this email.
Don't share this code with anyone unless you want to authorize them to use this address for this purpose. The {{ instance }} team will never ask you for it.
Best regards,
If you didn't request this, you can safely ignore this email.
Thanks,
The {{ instance }} Team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}
@@ -1,11 +1,9 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
We received a request to reset the password for your {{ instance }} account. To choose a new password, follow the link below:
you requested a new password. Please go to the following page to reset your password:
{{ url }}
If you didn't request this, you can safely ignore this email — your password won't change.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,16 +1,17 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
You've been invited to join a team on {{ instance }}, an event ticket sales platform.
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
- Organizer: {{ organizer }}
- Team: {{ team }}
To accept, follow the link below:
Organizer: {{ organizer }}
Team: {{ team }}
If you want to join that team, just click on the following link:
{{ url }}
If you don't want to join, you can safely ignore this email.
If you do not want to join, you can safely ignore or delete this email.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,19 +1,13 @@
{% load i18n %}{% blocktrans %}Hello,
{% load i18n %}{% blocktrans with url=url|safe os=source.os_type agent=source.agent_type %}Hello,
We noticed a new sign-in to your {{ instance }} account:
{% endblocktrans %}
- {% trans "Time" %}: {{ when }}
- {% trans "Browser" %}: {{ agent }}
- {% trans "Operating system" %}: {{ os }}
{% if device %}- {% trans "Device" %}: {{ device }}
{% endif %}{% if country %}- {% trans "Country" %}: {{ country }}
{% endif %}
{% blocktrans with url=url|safe %}If it was you, no action is needed.
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}.
If you don't recognize this sign-in, please change your password immediately:
If this was you, you can safely ignore this email.
If this was not you, we recommend that you change your password in your account settings:
{{ url }}
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,15 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
The following changes were made to your {{ instance }} account:
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
If you didn't make these changes, please contact the {{ instance }} support team immediately.
If this change was not performed by you, please contact us immediately.
You can review your account settings here:
You can review and change your account settings here:
{{ url }}
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -50,6 +50,46 @@
{% endblocktrans %}
</div>
{% endif %}
{% if dkim_warning %}
<div class="alert alert-danger">
<p>
{{ dkim_warning }}
</p>
<p>
{% trans "Your new DKIM record should be set up as a CNAME record like this:" %}
</p>
<pre><code>{{ dkim_hostname }} CNAME {{ dkim_cname }}</code></pre>
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif dkim_cname %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found a DKIM record on your domain for this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if dmarc_warning %}
<div class="alert alert-danger">
<p>
{{ dmarc_warning }}
</p>
<p>
{% trans "Your new DMARC record could look like this:" %}
</p>
<pre><code>_dmarc.{{ hostname }} TXT "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"</code></pre>
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif dkim_cname %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found a DMARC record on your domain for this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if verification %}
<h3>{% trans "Verification" %}</h3>
<p>
@@ -70,7 +110,7 @@
</div>
</div>
{% if spf_warning %}
{% if spf_warning or dkim_warning or dmarc_warning %}
<div class="form-group submit-group">
<a href="" class="btn btn-default btn-save">
{% trans "Cancel" %}
@@ -193,7 +193,6 @@
{% endblocktrans %}
</p>
{% bootstrap_field form.contact_mail layout="control" %}
{% bootstrap_field form.contact_url layout="control" %}
{% bootstrap_field form.imprint_url layout="control" %}
</div>
</fieldset>
@@ -30,7 +30,6 @@
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% bootstrap_field form.all_sales_channels layout="control" %}
@@ -34,7 +34,6 @@
{% endif %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.organizer_info_text layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %}
{% if sform.allowed_restricted_plugins %}
-1
View File
@@ -370,7 +370,6 @@ urlpatterns = [
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
re_path(r'^vouchers/tags/typeahead$', typeahead.voucher_tag_typeahead, name='event.vouchers.tags.typeahead'),
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),
re_path(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'),
re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
+79 -9
View File
@@ -28,7 +28,7 @@ from django.core.mail import get_connection
from django.shortcuts import redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from pretix.base import email
@@ -41,6 +41,30 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
logger = logging.getLogger(__name__)
def get_cname_record(hostname):
try:
r = dns.resolver.Resolver()
answers = r.resolve(hostname, 'CNAME')
answers = list(answers)
if len(answers) != 1:
logger.exception('Found multiple CNAME records for {}'.format(hostname))
return
return str(answers[0].target).lower()
except:
logger.exception('Could not fetch CNAME record for {}'.format(hostname))
def get_dmarc_record(hostname):
try:
r = dns.resolver.Resolver()
for resp in r.resolve("_dmarc." + hostname, 'TXT'):
data = b''.join(resp.strings).decode()
if 'DMARC1' in data.strip():
return data
except:
logger.exception("Could not fetch DMARC record for {}".format(hostname))
def get_spf_record(hostname):
try:
r = dns.resolver.Resolver()
@@ -49,7 +73,7 @@ def get_spf_record(hostname):
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
return data
except:
logger.exception("Could not fetch SPF record")
logger.exception("Could not fetch SPF record for {}".format(hostname))
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
@@ -168,10 +192,15 @@ class MailSettingsSetupView(TemplateView):
return super().get(request, *args, **kwargs)
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
verify_dns = (
settings.MAIL_CUSTOM_SENDER_SPF_STRING or
(settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED) or
settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED
)
allow_save = (
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
(not verify_dns or self.request.POST.get('state') == 'save')
)
if allow_save:
@@ -192,8 +221,8 @@ class MailSettingsSetupView(TemplateView):
spf_warning = None
spf_record = None
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
spf_record = get_spf_record(hostname)
if not spf_record:
spf_warning = _(
@@ -210,20 +239,55 @@ class MailSettingsSetupView(TemplateView):
'this system in the SPF record.'
)
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
dkim_warning = None
dkim_hostname = None
dkim_cname = None
if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR:
dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname
cname_target = get_cname_record(dkim_hostname)
dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME
if not dkim_cname.endswith("."):
dkim_cname += "."
if "%s" in dkim_cname:
dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower())
if not cname_target:
dkim_warning = _(
'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. '
'This means that there is a very high change most of the emails will be rejected or marked as '
'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the '
'DNS settings at the provider you registered your domain with.'
)
elif cname_target != dkim_cname:
dkim_warning = _(
'We found a CNAME record for a DKIM key, but it is not pointing to the right location. '
'This means that there is a very high chance most of the emails will be rejected or marked as '
'spam. You should update the DNS settings of your domain.'
)
dmarc_warning = None
dmarc_record = None
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
dmarc_record = get_dmarc_record(hostname)
if not dmarc_record:
spf_warning = _(
'We did not find DMARC record for your domain. This means that there is a very high chance '
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
'of your domain.'
)
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning
if verification:
if 'verification' in self.request.POST:
messages.error(request, _('The verification code was incorrect, please try again.'))
else:
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
sender_address = self.simple_form.cleaned_data.get('mail_from')
mail(
sender_address,
gettext('Confirm %(address)s as a sender address') % {'address': sender_address},
self.simple_form.cleaned_data.get('mail_from'),
_('Sender address verification'),
'pretixcontrol/email/email_setup.txt',
{
'code': self.request.session[session_key],
'address': sender_address,
'address': self.simple_form.cleaned_data.get('mail_from'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
None,
@@ -241,6 +305,12 @@ class MailSettingsSetupView(TemplateView):
'spf_warning': spf_warning,
'spf_record': spf_record,
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
'dkim_warning': dkim_warning,
'dkim_hostname': dkim_hostname,
'dkim_cname': dkim_cname,
'dmarc_warning': dmarc_warning,
'dmarc_record': dmarc_record,
'hostname': hostname,
'recp': self.simple_form.cleaned_data.get('mail_from')
},
using=self.template_engine,
+1 -3
View File
@@ -1032,9 +1032,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.request.organizer.name,
},
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
-15
View File
@@ -975,21 +975,6 @@ def subevent_meta_values(request, organizer, event):
})
@event_permission_required('event.vouchers:read')
def voucher_tag_typeahead(request, **kwargs):
q = request.GET.get('q', '')
tags = request.event.vouchers.filter(
tag__isnull=False,
waitinglistentries__isnull=True,
).filter(
tag__icontains=q,
).values_list('tag', flat=True).distinct().order_by('tag')[:10]
return JsonResponse({
'results': [{'name': t} for t in tags]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
+6 -31
View File
@@ -29,61 +29,36 @@ class EnvOrParserConfig:
self.cp = configparser
def _envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"PRETIX_{section}_{option}"
def _file_envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"FILE__PRETIX_{section}_{option}"
section = re.sub('[^a-zA-Z0-9]', '_', section.upper())
option = re.sub('[^a-zA-Z0-9]', '_', option.upper())
return f'PRETIX_{section}_{option}'
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return f.read().strip()
if self._envkey(section, option) in os.environ:
return os.environ[self._envkey(section, option)]
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return int(f.read().strip())
if self._envkey(section, option) in os.environ:
return int(os.environ[self._envkey(section, option)])
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return float(f.read().strip())
if self._envkey(section, option) in os.environ:
return float(os.environ[self._envkey(section, option)])
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return self.cp._convert_to_boolean(f.read().strip())
if self._envkey(section, option) in os.environ:
return self.cp._convert_to_boolean(
os.environ[self._envkey(section, option)]
)
return self.cp.getboolean(
section, option, raw=raw, vars=vars, fallback=fallback
)
return self.cp._convert_to_boolean(os.environ[self._envkey(section, option)])
return self.cp.getboolean(section, option, raw=raw, vars=vars, fallback=fallback)
def has_section(self, section):
if any(k.startswith(self._file_envkey(section, "")) for k in os.environ):
return True
if any(k.startswith(self._envkey(section, "")) for k in os.environ):
if any(k.startswith(self._envkey(section, '')) for k in os.environ):
return True
return self.cp.has_section(section)
def has_option(self, section, option):
if self._file_envkey(section, option) in os.environ:
return True
if self._envkey(section, option) in os.environ:
return True
return self.cp.has_option(section, option)
+1 -4
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import uuid
from django.core.signals import request_finished
from django.dispatch import receiver
@@ -66,9 +65,7 @@ class RequestIdMiddleware:
import sentry_sdk
sentry_sdk.set_tag("request_id", request.request_id)
else:
# Web server did not pass a request ID, we still generate one to correlate between django logs and
# celery logs
local.request_id = request.request_id = str(uuid.uuid4())
local.request_id = request.request_id = None
return self.get_response(request)
+1 -14
View File
@@ -25,19 +25,6 @@ import text_unidecode
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
EPC_QR_ALLOWED_CHARS = set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789/-?:().,'+ "
)
def epc_qr_field(value):
return ''.join(
char for char in text_unidecode.unidecode(str(value or ''))
if char in EPC_QR_ALLOWED_CHARS
)
def dotdecimal(value):
return str(value).replace(",", ".")
@@ -90,7 +77,7 @@ def euro_epc_qr(
return {
"id": "girocode",
"label": "EPC-QR",
"qr_data": "\n".join(epc_qr_field(d) for d in [
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [
"BCD", # Service Tag: BCD
"002", # Version: V2
"2", # Character set: ISO 8859-1
+3 -8
View File
@@ -27,7 +27,6 @@ from django.conf import settings
from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2
from django.core.cache import cache
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
@@ -170,15 +169,11 @@ def handle_login_source(user, request):
with language(user.locale):
mail(
user.email,
_('New sign-in to your account'),
_('Login from new source detected'),
'pretixcontrol/email/login_notice.txt',
{
'when': date_format(src.last_seen, 'DATETIME_FORMAT'),
'agent': src.agent_type,
'os': src.os_type,
# ua-parser returns "Other" for unidentified desktop devices.
'device': src.device_type if src.device_type and src.device_type != 'Other' else None,
'country': Country(str(country)).name if country else None,
'source': src,
'country': Country(str(country)).name if country else _('Unknown country'),
'instance': settings.PRETIX_INSTANCE_NAME,
'url': mainreverse_absolute('control:user.settings')
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-6
View File
@@ -74,7 +74,6 @@ Bokmål
Boleto
Branding-Informationen
Browsereinstellungen
Browserfenster
BSD-Lizenz
bspw
Bundles
@@ -135,7 +134,6 @@ Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-App
E-Mail-Renderer
Enterprise-Lizenz
Enterprise-Lizenzen
@@ -154,7 +152,6 @@ erstmalig
etc
EU-USt-ID-Nr
Event
Event-Absage
Event-Eigenschaft
Eventeingang
Event-Erstellung
@@ -209,7 +206,6 @@ innenname
innennamen
innergemeinschaftliche
Innergemeinschaftlicher
Instagram
Installations-ID
Integrationen
invalidieren
@@ -231,7 +227,6 @@ Linktext
lit
Log-ID
Logindaten
Loginvorgangs
Macau
MapQuest-API-Key
max
@@ -455,7 +450,6 @@ Strong
Swish
systemweiten
Tab
Tabs
tag
Teammitglied
Teamname
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -74,7 +74,6 @@ Bokmål
Boleto
Branding-Informationen
Browsereinstellungen
Browserfenster
BSD-Lizenz
bspw
Bundles
@@ -135,7 +134,6 @@ Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-App
E-Mail-Renderer
Enterprise-Lizenz
Enterprise-Lizenzen
@@ -154,7 +152,6 @@ erstmalig
etc
EU-USt-ID-Nr
Event
Event-Absage
Event-Eigenschaft
Eventeingang
Event-Erstellung
@@ -209,7 +206,6 @@ innenname
innennamen
innergemeinschaftliche
Innergemeinschaftlicher
Instagram
Installations-ID
Integrationen
invalidieren
@@ -231,7 +227,6 @@ Linktext
lit
Log-ID
Logindaten
Loginvorgangs
Macau
MapQuest-API-Key
max
@@ -455,7 +450,6 @@ Strong
Swish
systemweiten
Tab
Tabs
tag
Teammitglied
Teamname
+9999 -8719
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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