mirror of
https://github.com/pretix/pretix.git
synced 2026-06-10 01:15:05 +00:00
Compare commits
10 Commits
mails-to-a
...
email-chan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f51fbd7df3 | ||
|
|
2ad2b8515a | ||
|
|
48933056aa | ||
|
|
21225e7753 | ||
|
|
759ced7268 | ||
|
|
5920419e6b | ||
|
|
7c00383b62 | ||
|
|
4361641857 | ||
|
|
ac8f40353e | ||
|
|
d648c83e4c |
@@ -64,8 +64,8 @@ Backend
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
|
||||
item_formsets, order_search_filter_q, order_search_forms
|
||||
order_info, order_approve_info, event_settings_widget, oauth_application_registered,
|
||||
order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
|
||||
@@ -93,7 +93,7 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==4.5.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.60.*",
|
||||
"sentry-sdk==2.61.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
@@ -42,13 +40,12 @@ 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.metrics import pretix_failed_logins
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,45 +82,26 @@ 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:
|
||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||
logger.info("Backend login rejected due to rate limit.")
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
rate_limit_kwargs = dict(include_ip_from_request=self.request, max_num=10, expire_time=300)
|
||||
if rate_limit("login", **rate_limit_kwargs, increase=False):
|
||||
# Check rate limit without counting up, we increase below only on failed logins
|
||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||
logger.info("Backend login rejected due to rate limit.")
|
||||
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)
|
||||
logger.info("Backend login invalid.")
|
||||
pretix_failed_logins.inc(1, reason="invalid")
|
||||
# Count towards rate limit (result is ignored, we are checking above)
|
||||
rate_limit("login", **rate_limit_kwargs)
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
)
|
||||
else:
|
||||
rate_limit_reset("login", include_ip_from_request=self.request)
|
||||
self.confirm_login_allowed(self.user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
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,
|
||||
@@ -46,6 +45,7 @@ from pytz import common_timezones
|
||||
from pretix.base.models import User
|
||||
from pretix.control.forms import SingleLanguageWidget
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
|
||||
|
||||
class UserSettingsForm(forms.ModelForm):
|
||||
@@ -128,16 +128,11 @@ class UserPasswordChangeForm(forms.Form):
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
if 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 rate_limit("pwchange", self.user.pk, max_num=10, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if not check_password(old_pw, self.user.password):
|
||||
raise forms.ValidationError(
|
||||
@@ -175,19 +170,35 @@ class UserEmailChangeForm(forms.Form):
|
||||
error_messages = {
|
||||
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||
"Please choose a different one."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
|
||||
new_email = forms.EmailField(label=_('New email address'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user')
|
||||
self.request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_new_email(self):
|
||||
email = self.cleaned_data['new_email']
|
||||
|
||||
if rate_limit("emailchange_attempt", include_ip_from_request=self.request, max_num=5, expire_time=300):
|
||||
# Rate limit lookup for conflicting email addresses to make enumeration harder
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['duplicate_identifier'],
|
||||
code='duplicate_identifier',
|
||||
)
|
||||
|
||||
if rate_limit("emailchange", self.user.pk, max_num=1, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
return email
|
||||
|
||||
@@ -261,6 +261,16 @@ As with all event plugin signals, the ``sender`` keyword argument will contain t
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
"""
|
||||
|
||||
order_approve_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order approve page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
"""
|
||||
|
||||
order_position_buttons = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``position``, ``request``
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load eventsignal %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Approve order" %}
|
||||
@@ -7,6 +8,9 @@
|
||||
<h1>
|
||||
{% trans "Approve order" %}
|
||||
</h1>
|
||||
|
||||
{% eventsignal request.event "pretix.control.signals.order_approve_info" order=order request=request %}
|
||||
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to approve this order?
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
@@ -65,6 +65,7 @@ from pretix.base.forms.auth import (
|
||||
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||
from pretix.helpers.http import get_client_ip, redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
from pretix.helpers.security import handle_login_source, session_login
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -318,19 +319,12 @@ class Forgot(TemplateView):
|
||||
if self.form.is_valid():
|
||||
email = self.form.cleaned_data['email']
|
||||
|
||||
has_redis = settings.HAS_REDIS
|
||||
|
||||
try:
|
||||
user = User.objects.get(is_active=True, auth_backend='native', email__iexact=email)
|
||||
|
||||
if has_redis:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
if rc.exists('pretix_pwreset_%s' % (user.id)):
|
||||
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
|
||||
raise RepeatedResetDenied()
|
||||
else:
|
||||
rc.setex('pretix_pwreset_%s' % (user.id), 3600 * 24, '1')
|
||||
if rate_limit("pwreset", user.pk, max_num=1, expire_time=3600 * 24):
|
||||
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
|
||||
raise RepeatedResetDenied()
|
||||
|
||||
except User.DoesNotExist:
|
||||
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
||||
@@ -343,6 +337,7 @@ class Forgot(TemplateView):
|
||||
user.log_action('pretix.control.auth.user.forgot_password.mail_sent')
|
||||
|
||||
finally:
|
||||
has_redis = settings.HAS_REDIS
|
||||
if has_redis:
|
||||
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions. '
|
||||
'Please note that we will send at most one email every 24 hours.'))
|
||||
@@ -411,11 +406,7 @@ class Recover(TemplateView):
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
has_redis = settings.HAS_REDIS
|
||||
if has_redis:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
rc.delete('pretix_pwreset_%s' % user.id)
|
||||
rate_limit_reset("pwreset", user.pk)
|
||||
return redirect('control:auth.login')
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
@@ -80,6 +80,7 @@ from pretix.control.permissions import (
|
||||
)
|
||||
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
from pretix.helpers.security import session_reauth
|
||||
from pretix.helpers.u2f import websafe_encode
|
||||
|
||||
@@ -879,6 +880,7 @@ class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
|
||||
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
"request": self.request,
|
||||
"user": self.request.user,
|
||||
}
|
||||
|
||||
@@ -908,6 +910,10 @@ class UserEmailVerifyView(View):
|
||||
messages.success(self.request, _('Your email address was already verified.'))
|
||||
return redirect(reverse('control:user.settings', kwargs={}))
|
||||
|
||||
if rate_limit("emailverify", self.request.user.pk, max_num=2, expire_time=300):
|
||||
messages.error(self.request, _("For security reasons, please wait 5 minutes before you try again."))
|
||||
return redirect(reverse('control:user.settings', kwargs={}))
|
||||
|
||||
self.request.user.send_confirmation_code(
|
||||
session=self.request.session,
|
||||
reason='email_verify',
|
||||
|
||||
116
src/pretix/helpers/ratelimit.py
Normal file
116
src/pretix/helpers/ratelimit.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_key(key, parameters):
|
||||
return f'pretix:ratelimit:{key}:' + hashlib.sha256(','.join(str(p) for p in parameters).encode()).hexdigest()
|
||||
|
||||
|
||||
def _get_ip(request):
|
||||
client_ip = get_client_ip(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 and not settings.DEBUG:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return str(client_ip)
|
||||
|
||||
|
||||
def rate_limit(key: str, *parameters, include_ip_from_request: HttpRequest=None, max_num: int, expire_time: int, increase: bool = True):
|
||||
"""
|
||||
This is a shared utility to implement simple rate limiting in operations like
|
||||
password resets.
|
||||
|
||||
:param key: The key referring to the feature like "pwreset"
|
||||
:param parameters: Any number of things to be hashed as the bucket key
|
||||
:param include_ip_from_request: Add IP address from request to the bucket key. If IP address cannot be determined,
|
||||
rate limit is not applied.
|
||||
:param max_num: The maximum number of actions to performed within expire_time of the first action
|
||||
:param expire_time: The length of the time window in seconds
|
||||
:param increase: Whether to count the call as an event counted towards the rate, or just check
|
||||
:return:
|
||||
"""
|
||||
if not settings.HAS_REDIS:
|
||||
# No rate limiting
|
||||
return False
|
||||
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
|
||||
if include_ip_from_request:
|
||||
ip = _get_ip(include_ip_from_request)
|
||||
if not ip:
|
||||
# IP not discovered, can't rate limit
|
||||
return False
|
||||
parameters = (*parameters, ip)
|
||||
|
||||
redis_key = _get_key(key, parameters)
|
||||
|
||||
if increase:
|
||||
p = rc.pipeline()
|
||||
p.set(redis_key, 0, nx=True, ex=expire_time) # Start a rate limit window if none is running
|
||||
p.incr(redis_key)
|
||||
new_counter = p.execute()[1]
|
||||
else:
|
||||
new_counter = int(rc.get(redis_key) or 0)
|
||||
|
||||
if new_counter > max_num:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def rate_limit_reset(key: str, *parameters, include_ip_from_request: HttpRequest=None):
|
||||
"""
|
||||
Reset a rate limit bucket.
|
||||
"""
|
||||
if not settings.HAS_REDIS:
|
||||
# No rate limiting
|
||||
return
|
||||
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
|
||||
if include_ip_from_request:
|
||||
ip = _get_ip(include_ip_from_request)
|
||||
if not ip:
|
||||
# IP not discovered, can't rate limit
|
||||
return False
|
||||
parameters = (*parameters, ip)
|
||||
|
||||
redis_key = _get_key(key, parameters)
|
||||
rc.delete(redis_key)
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
|
||||
"PO-Revision-Date: 2026-05-01 21:00+0000\n"
|
||||
"Last-Translator: Paul Berschick <paul@plainschwarz.com>\n"
|
||||
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.17\n"
|
||||
"X-Generator: Weblate 2026.5\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:166
|
||||
@@ -620,16 +620,17 @@ msgstr ""
|
||||
"como variaciones o paquetes."
|
||||
|
||||
#: pretix/api/webhooks.py:413
|
||||
#, fuzzy
|
||||
#| msgid "Quota handling"
|
||||
msgid "Quota changed"
|
||||
msgstr "Gestión de cuotas"
|
||||
msgstr "Se ha modificado la cuota"
|
||||
|
||||
#: pretix/api/webhooks.py:414
|
||||
msgid ""
|
||||
"This includes related events like creation, deletion, opening or closing of "
|
||||
"quotas. No webhook is sent for changes to the resulting availability."
|
||||
msgstr ""
|
||||
"Esto incluye acciones relacionadas, como la creación, la eliminación, la "
|
||||
"apertura o el cierre de cuotas. No se envía ningún webhook cuando se "
|
||||
"producen cambios en la disponibilidad resultante."
|
||||
|
||||
#: pretix/api/webhooks.py:419
|
||||
msgid "Shop taken live"
|
||||
@@ -3418,11 +3419,13 @@ msgid ""
|
||||
"The field \"%(label)s\" may not contain special characters such as "
|
||||
"\"%(chars)s\"."
|
||||
msgstr ""
|
||||
"El campo «%(label)s» no puede contener caracteres especiales como «%(chars)s"
|
||||
"»."
|
||||
|
||||
#: pretix/base/forms/questions.py:305
|
||||
#, python-format
|
||||
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
|
||||
msgstr ""
|
||||
msgstr "El campo «%(label)s» no puede contener una URL (%(url)s)."
|
||||
|
||||
#: pretix/base/forms/questions.py:338
|
||||
msgctxt "phonenumber"
|
||||
@@ -8361,19 +8364,14 @@ msgid "Program times"
|
||||
msgstr "Horarios del programa"
|
||||
|
||||
#: pretix/base/pdf.py:503
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "2017-05-31 10:00 – 12:00\n"
|
||||
#| "2017-05-31 14:00 – 16:00\n"
|
||||
#| "2017-05-31 14:00 – 2017-06-01 14:00"
|
||||
msgid ""
|
||||
"2017-05-31 10:00 – 12:00, Room 1\n"
|
||||
"2017-05-31 14:00 – 16:00, Room 2\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00, Building A"
|
||||
msgstr ""
|
||||
"2017-05-31 10:00 – 12:00\n"
|
||||
"2017-05-31 14:00 – 16:00\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00"
|
||||
"31 de mayo de 2017, de 10:00 a 12:00, Sala 1\n"
|
||||
"31 de mayo de 2017, de 14:00 a 16:00, Sala 2\n"
|
||||
"31 de mayo de 2017, de 14:00 a 1 de junio de 2017, 14:00, Edificio A"
|
||||
|
||||
#: pretix/base/pdf.py:507
|
||||
msgid "Reusable Medium ID"
|
||||
@@ -8903,13 +8901,7 @@ msgid "This voucher code is not known in our database."
|
||||
msgstr "Este vale de compra no se conoce en nuestra base de datos."
|
||||
|
||||
#: pretix/base/services/cart.py:165
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products."
|
||||
#| msgid_plural ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching product."
|
||||
@@ -8917,22 +8909,14 @@ msgid_plural ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching products."
|
||||
msgstr[0] ""
|
||||
"El vale de compra \"%(voucher)s\" solo se puede utilizar si selecciona al "
|
||||
"menos %(number)s productos coincidentes."
|
||||
"El código de descuento «%(voucher)s» solo se puede utilizar si seleccionas "
|
||||
"al menos%(number)s productos que cumplan los requisitos."
|
||||
msgstr[1] ""
|
||||
"Los vales de compra \"%(voucher)s\" solo se pueden utilizar si selecciona al "
|
||||
"menos %(number)s productos coincidentes."
|
||||
"El código de descuento «%(voucher)s» solo se puede utilizar si seleccionas "
|
||||
"al menos %(number)s productos que cumplan los requisitos."
|
||||
|
||||
#: pretix/base/services/cart.py:170
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products. We have therefore removed some positions "
|
||||
#| "from your cart that can no longer be purchased like this."
|
||||
#| msgid_plural ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products. We have therefore removed some positions "
|
||||
#| "from your cart that can no longer be purchased like this."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching product. We have therefore removed some positions from "
|
||||
@@ -8942,13 +8926,15 @@ msgid_plural ""
|
||||
"%(number)s matching products. We have therefore removed some positions from "
|
||||
"your cart that can no longer be purchased like this."
|
||||
msgstr[0] ""
|
||||
"El vale de compra \"%(voucher)s\" solo se puede utilizar si selecciona al "
|
||||
"menos %(number)s productos coincidentes. Por lo tanto, hemos eliminado "
|
||||
"algunas posiciones de su carrito que ya no se pueden comprar así."
|
||||
"El código promocional «%(voucher)s» solo se puede utilizar si seleccionas al "
|
||||
"menos %(number)s producto que cumpla los requisitos. Por lo tanto, hemos "
|
||||
"eliminado de tu carrito algunos artículos que ya no se pueden comprar de "
|
||||
"esta forma."
|
||||
msgstr[1] ""
|
||||
"Los vale de compra \"%(voucher)s\" solo se pueden utilizar si selecciona al "
|
||||
"menos %(number)s productos coincidentes. Por lo tanto, hemos eliminado "
|
||||
"algunas posiciones de su carrito que ya no se pueden comprar así."
|
||||
"El código promocional «%(voucher)s» solo se puede utilizar si seleccionas al "
|
||||
"menos %(number)s productos que cumplan los requisitos. Por lo tanto, hemos "
|
||||
"eliminado de tu carrito algunos artículos que ya no se pueden comprar de "
|
||||
"esta forma."
|
||||
|
||||
#: pretix/base/services/cart.py:176
|
||||
msgid ""
|
||||
@@ -14254,6 +14240,8 @@ msgid ""
|
||||
"You entered an URL, which is not allowed. Please remove %(match)s from your "
|
||||
"input."
|
||||
msgstr ""
|
||||
"Ha introducido una URL que no está permitida. Elimina %(match)s de su "
|
||||
"entrada."
|
||||
|
||||
#: pretix/base/views/errors.py:48
|
||||
msgid ""
|
||||
@@ -16194,14 +16182,8 @@ msgid "inactive"
|
||||
msgstr "inactivo"
|
||||
|
||||
#: pretix/control/forms/item.py:1414
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Sample Conference Center\n"
|
||||
#| "Heidelberg, Germany"
|
||||
msgid "Sample Conference Center, Heidelberg, Germany"
|
||||
msgstr ""
|
||||
"Ejemplo de Centro de Conferencia \n"
|
||||
"Heidelberg, Alemania"
|
||||
msgstr "Ejemplo de Centro de Conferencia : Heidelberg, Alemania"
|
||||
|
||||
#: pretix/control/forms/mailsetup.py:42
|
||||
msgid "Hostname"
|
||||
@@ -23659,11 +23641,8 @@ msgid "Quota history"
|
||||
msgstr "Historial de cuotas"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:6
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "Change multiple dates"
|
||||
msgid "Change multiple quotas"
|
||||
msgstr "Cambiar varias fechas"
|
||||
msgstr "Modificar varias cuotas"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:8
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:8
|
||||
@@ -23713,18 +23692,15 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:4
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:6
|
||||
#, fuzzy
|
||||
#| msgid "Delete quota"
|
||||
msgid "Delete quotas"
|
||||
msgstr "Borrar cuota"
|
||||
msgstr "Eliminar cuotas"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:10
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Are you sure you want to delete the following dates?"
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the following quota?"
|
||||
msgid_plural "Are you sure you want to delete the following %(num)s quotas?"
|
||||
msgstr[0] "¿Está seguro de que desea borrar las fechas siguientes?"
|
||||
msgstr[1] "¿Está seguro de que desea borrar las fechas siguientes?"
|
||||
msgstr[0] "¿Está seguro de que desea eliminar la siguiente cuota?"
|
||||
msgstr[1] "¿Está seguro de que desea eliminar las siguientes %(num)s cuotas?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quotas.html:9
|
||||
msgid ""
|
||||
@@ -24329,12 +24305,15 @@ msgid ""
|
||||
"generated once the customer pays the invoice or selects a payment method "
|
||||
"that requires an invoice."
|
||||
msgstr ""
|
||||
"Este pedido se modificó después de que se generara la última factura. Aún no "
|
||||
"se ha generado una nueva factura, ya que las facturas están configuradas "
|
||||
"para generarse al realizar el pago o si así lo exige la forma de pago. Se "
|
||||
"generará una nueva factura una vez que el cliente abone la factura o "
|
||||
"seleccione una forma de pago que requiera una factura."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:152
|
||||
#, fuzzy
|
||||
#| msgid "Request invoice"
|
||||
msgid "Reissue invoice"
|
||||
msgstr "Solicitar factura"
|
||||
msgstr "Reemitir factura"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:161
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:413
|
||||
@@ -24765,23 +24744,16 @@ msgid "How should the refund be sent?"
|
||||
msgstr "¿Cómo se debe de realizar este reembolso?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:25
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Any payments that you selected for automatical refunds will be "
|
||||
#| "immediately communicate the refund request to the respective payment "
|
||||
#| "provider. Manual refunds will be created as pending refunds, you can then "
|
||||
#| "later mark them as done once you actually transferred the money back to "
|
||||
#| "the customer."
|
||||
msgid ""
|
||||
"Any payments you selected for automatic refunds will have the refund request "
|
||||
"sent immediately to the respective payment provider. Manual refunds will be "
|
||||
"created as pending refunds, which you can later mark as done once you have "
|
||||
"actually transferred the money back to the customer."
|
||||
msgstr ""
|
||||
"Cualquier pago que haya seleccionado de manera automática para reembolso "
|
||||
"será comunicado inmediatamente a la entidad de pago correspondiente. Los "
|
||||
"devoluciones manuales se crearán como reembolsos pendientes, podrá marcarlos "
|
||||
"como hechos una vez que se haya transferido el dinero al cliente."
|
||||
"Los pagos que hayas seleccionado para reembolsos automáticos se enviarán "
|
||||
"inmediatamente al proveedor de pagos correspondiente. Los reembolsos "
|
||||
"manuales se crearán como reembolsos pendientes, que podrás marcar como "
|
||||
"completados más adelante, una vez que hayas devuelto el dinero al cliente."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:32
|
||||
msgid "Refund to original payment method"
|
||||
@@ -29337,11 +29309,8 @@ msgid "The new question has been created."
|
||||
msgstr "La nueva pregunta ha sido creada."
|
||||
|
||||
#: pretix/control/views/item.py:918
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "The selected dates have been deleted or disabled."
|
||||
msgid "The selected quotas have been deleted or disabled."
|
||||
msgstr "Las fechas seleccionadas se han borrado o desactivado."
|
||||
msgstr "Las cuotas seleccionadas se han eliminado o desactivado."
|
||||
|
||||
#: pretix/control/views/item.py:1074
|
||||
msgid "The new quota has been created."
|
||||
@@ -30073,11 +30042,9 @@ msgstr ""
|
||||
"Este plugin no está permitido actualmente para su cuenta de organizador."
|
||||
|
||||
#: pretix/control/views/organizer.py:832
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "This plugin can be enabled or disabled for events individually."
|
||||
#, python-brace-format
|
||||
msgid "This plugin cannot be activated for event {}."
|
||||
msgstr ""
|
||||
"Este plugin se puede activar o desactivar para eventos de forma individual."
|
||||
msgstr "Este complemento no se puede activar para el evento {}."
|
||||
|
||||
#: pretix/control/views/organizer.py:901
|
||||
msgid "The team has been created. You can now add members to the team."
|
||||
@@ -31122,10 +31089,9 @@ msgid "{width} x {height} mm label"
|
||||
msgstr "etiqueta {width} x {height} mm"
|
||||
|
||||
#: pretix/plugins/badges/templates.py:265
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "{width} x {height} mm label"
|
||||
#, python-brace-format
|
||||
msgid "{width} x {height} inch label"
|
||||
msgstr "etiqueta {width} x {height} mm"
|
||||
msgstr "Etiqueta de {width} x {height} pulgadas"
|
||||
|
||||
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_info.html:16
|
||||
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:27
|
||||
|
||||
@@ -4,16 +4,16 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
|
||||
"PO-Revision-Date: 2026-05-08 04:00+0000\n"
|
||||
"Last-Translator: corentin-spec <corentin@spectentaculaire.fr>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fr/>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 5.17.1\n"
|
||||
"X-Generator: Weblate 2026.5\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:166
|
||||
@@ -618,16 +618,17 @@ msgstr ""
|
||||
"aux objets imbriqués tels que les variantes ou les lots."
|
||||
|
||||
#: pretix/api/webhooks.py:413
|
||||
#, fuzzy
|
||||
#| msgid "Quota handling"
|
||||
msgid "Quota changed"
|
||||
msgstr "Traitement des quotas"
|
||||
msgstr "Quota modifié"
|
||||
|
||||
#: pretix/api/webhooks.py:414
|
||||
msgid ""
|
||||
"This includes related events like creation, deletion, opening or closing of "
|
||||
"quotas. No webhook is sent for changes to the resulting availability."
|
||||
msgstr ""
|
||||
"Cela inclut les événements associés, tels que la création, la suppression, "
|
||||
"l'ouverture ou la suppression de quotas. Aucun webhook n'est envoyé en cas "
|
||||
"de modification de la disponibilité qui en résulte."
|
||||
|
||||
#: pretix/api/webhooks.py:419
|
||||
msgid "Shop taken live"
|
||||
@@ -3422,11 +3423,13 @@ msgid ""
|
||||
"The field \"%(label)s\" may not contain special characters such as "
|
||||
"\"%(chars)s\"."
|
||||
msgstr ""
|
||||
"Le champ « %(label)s » ne doit pas contenir de caractères spéciaux tels que "
|
||||
"«%(chars)s »."
|
||||
|
||||
#: pretix/base/forms/questions.py:305
|
||||
#, python-format
|
||||
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
|
||||
msgstr ""
|
||||
msgstr "Le champ « %(label)s » ne doit pas contenir d'URL (%(url)s)."
|
||||
|
||||
#: pretix/base/forms/questions.py:338
|
||||
msgctxt "phonenumber"
|
||||
@@ -8409,19 +8412,14 @@ msgid "Program times"
|
||||
msgstr "Horaires du programme"
|
||||
|
||||
#: pretix/base/pdf.py:503
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "2017-05-31 10:00 – 12:00\n"
|
||||
#| "2017-05-31 14:00 – 16:00\n"
|
||||
#| "2017-05-31 14:00 – 2017-06-01 14:00"
|
||||
msgid ""
|
||||
"2017-05-31 10:00 – 12:00, Room 1\n"
|
||||
"2017-05-31 14:00 – 16:00, Room 2\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00, Building A"
|
||||
msgstr ""
|
||||
"2017-05-31 10:00 – 12:00\n"
|
||||
"2017-05-31 14:00 – 16:00\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00"
|
||||
"31 mai 2017, de 10 h à 12 h, salle 1\n"
|
||||
"31 mai 2017, de 14 h à 16 h, salle 2\n"
|
||||
"Du 31 mai 2017 à 1 h du matin au 1er juin 2017 à 14 h, bâtiment A"
|
||||
|
||||
#: pretix/base/pdf.py:507
|
||||
msgid "Reusable Medium ID"
|
||||
@@ -8957,13 +8955,7 @@ msgid "This voucher code is not known in our database."
|
||||
msgstr "Ce code promotionnel n'est pas connu dans notre base de données."
|
||||
|
||||
#: pretix/base/services/cart.py:165
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products."
|
||||
#| msgid_plural ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching product."
|
||||
@@ -8971,22 +8963,14 @@ msgid_plural ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching products."
|
||||
msgstr[0] ""
|
||||
"Le code promo \"%(voucher)s\" ne peut être utilisé que si vous sélectionnez "
|
||||
"Le code promo « %(voucher)s » ne peut être utilisé que si vous sélectionnez "
|
||||
"au moins %(number)s produit correspondant."
|
||||
msgstr[1] ""
|
||||
"Le code promo \"%(voucher)s\" ne peut être utilisé que si vous sélectionnez "
|
||||
"Le code promo « %(voucher)s » ne peut être utilisé que si vous sélectionnez "
|
||||
"au moins %(number)s produits correspondants."
|
||||
|
||||
#: pretix/base/services/cart.py:170
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products. We have therefore removed some positions "
|
||||
#| "from your cart that can no longer be purchased like this."
|
||||
#| msgid_plural ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products. We have therefore removed some positions "
|
||||
#| "from your cart that can no longer be purchased like this."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching product. We have therefore removed some positions from "
|
||||
@@ -14379,6 +14363,8 @@ msgid ""
|
||||
"You entered an URL, which is not allowed. Please remove %(match)s from your "
|
||||
"input."
|
||||
msgstr ""
|
||||
"Vous avez saisi une URL, ce qui n'est pas autorisé. Veuillez supprimer %"
|
||||
"(match)s de votre saisie."
|
||||
|
||||
#: pretix/base/views/errors.py:48
|
||||
msgid ""
|
||||
@@ -16328,14 +16314,8 @@ msgid "inactive"
|
||||
msgstr "inactif"
|
||||
|
||||
#: pretix/control/forms/item.py:1414
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Sample Conference Center\n"
|
||||
#| "Heidelberg, Germany"
|
||||
msgid "Sample Conference Center, Heidelberg, Germany"
|
||||
msgstr ""
|
||||
"Exemple de centre de conférence\n"
|
||||
"Centre des Congrès, France"
|
||||
msgstr "Centre de conférences d'exemple, Heidelberg, Allemagne"
|
||||
|
||||
#: pretix/control/forms/mailsetup.py:42
|
||||
msgid "Hostname"
|
||||
@@ -23831,11 +23811,8 @@ msgid "Quota history"
|
||||
msgstr "Historique des quotas"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:6
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "Change multiple dates"
|
||||
msgid "Change multiple quotas"
|
||||
msgstr "Modifier plusieurs dates"
|
||||
msgstr "Modifier plusieurs quotas"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:8
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:8
|
||||
@@ -23883,18 +23860,15 @@ msgstr "Les produits suivants pourraient ne plus être disponibles à la vente
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:4
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:6
|
||||
#, fuzzy
|
||||
#| msgid "Delete quota"
|
||||
msgid "Delete quotas"
|
||||
msgstr "Supprimer le quota"
|
||||
msgstr "Supprimer les quotas"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:10
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Are you sure you want to delete the following dates?"
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the following quota?"
|
||||
msgid_plural "Are you sure you want to delete the following %(num)s quotas?"
|
||||
msgstr[0] "Voulez-vous vraiment supprimer les dates suivantes ?"
|
||||
msgstr[1] "Voulez-vous vraiment supprimer les dates suivantes ?"
|
||||
msgstr[0] "Êtes-vous sûr de vouloir supprimer le quota suivant ?"
|
||||
msgstr[1] "Êtes-vous sûr de vouloir supprimer les %(num)s quotas suivants ?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quotas.html:9
|
||||
msgid ""
|
||||
@@ -24503,12 +24477,15 @@ msgid ""
|
||||
"generated once the customer pays the invoice or selects a payment method "
|
||||
"that requires an invoice."
|
||||
msgstr ""
|
||||
"Cette commande a été modifiée après l'émission de la dernière facture. "
|
||||
"Aucune nouvelle facture n'a encore été générée, car les factures sont "
|
||||
"configurées pour être émises lors du paiement ou si le mode de paiement "
|
||||
"l'exige. Une nouvelle facture sera générée dès que le client aura réglé la "
|
||||
"facture ou choisi un mode de paiement nécessitant une facture."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:152
|
||||
#, fuzzy
|
||||
#| msgid "Request invoice"
|
||||
msgid "Reissue invoice"
|
||||
msgstr "Demande de facture"
|
||||
msgstr "Réémettre une facture"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:161
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:413
|
||||
@@ -24942,25 +24919,17 @@ msgid "How should the refund be sent?"
|
||||
msgstr "Comment le remboursement doit-il être envoyé ?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:25
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Any payments that you selected for automatical refunds will be "
|
||||
#| "immediately communicate the refund request to the respective payment "
|
||||
#| "provider. Manual refunds will be created as pending refunds, you can then "
|
||||
#| "later mark them as done once you actually transferred the money back to "
|
||||
#| "the customer."
|
||||
msgid ""
|
||||
"Any payments you selected for automatic refunds will have the refund request "
|
||||
"sent immediately to the respective payment provider. Manual refunds will be "
|
||||
"created as pending refunds, which you can later mark as done once you have "
|
||||
"actually transferred the money back to the customer."
|
||||
msgstr ""
|
||||
"Tous les paiements que vous avez sélectionnés pour des remboursements "
|
||||
"automatiques seront immédiatement communiqués à la demande de remboursement "
|
||||
"au fournisseur de paiement respectif. Les remboursements manuels seront "
|
||||
"créés en tant que remboursements en attente, vous pourrez ensuite les "
|
||||
"marquer comme terminés une fois que vous aurez effectivement transféré "
|
||||
"l’argent au client."
|
||||
"Pour tous les paiements que vous avez sélectionnés pour un remboursement "
|
||||
"automatique, la demande de remboursement sera immédiatement transmise au "
|
||||
"prestataire de paiement concerné. Les remboursements manuels seront "
|
||||
"enregistrés comme remboursements en attente ; vous pourrez les marquer comme "
|
||||
"effectués une fois que vous aurez effectivement reversé l'argent au client."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:32
|
||||
msgid "Refund to original payment method"
|
||||
@@ -29558,11 +29527,8 @@ msgid "The new question has been created."
|
||||
msgstr "La nouvelle question a été créée."
|
||||
|
||||
#: pretix/control/views/item.py:918
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "The selected dates have been deleted or disabled."
|
||||
msgid "The selected quotas have been deleted or disabled."
|
||||
msgstr "Les dates sélectionnées ont été supprimées ou désactivées."
|
||||
msgstr "Les quotas sélectionnés ont été supprimés ou désactivés."
|
||||
|
||||
#: pretix/control/views/item.py:1074
|
||||
msgid "The new quota has been created."
|
||||
@@ -30302,12 +30268,9 @@ msgstr ""
|
||||
"Ce plugin n'est actuellement pas autorisé pour ce compte d'organisateur."
|
||||
|
||||
#: pretix/control/views/organizer.py:832
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "This plugin can be enabled or disabled for events individually."
|
||||
#, python-brace-format
|
||||
msgid "This plugin cannot be activated for event {}."
|
||||
msgstr ""
|
||||
"Ce plugin peut être activé ou désactivé individuellement pour chaque "
|
||||
"événement."
|
||||
msgstr "Ce plugin ne peut pas être activé pour l'événement {}."
|
||||
|
||||
#: pretix/control/views/organizer.py:901
|
||||
msgid "The team has been created. You can now add members to the team."
|
||||
@@ -31362,10 +31325,9 @@ msgid "{width} x {height} mm label"
|
||||
msgstr "{width} x {height} mm étiquette"
|
||||
|
||||
#: pretix/plugins/badges/templates.py:265
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "{width} x {height} mm label"
|
||||
#, python-brace-format
|
||||
msgid "{width} x {height} inch label"
|
||||
msgstr "{width} x {height} mm étiquette"
|
||||
msgstr "{width} x {height} pouce étiquette"
|
||||
|
||||
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_info.html:16
|
||||
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:27
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
|
||||
"PO-Revision-Date: 2026-05-12 06:34+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-06-01 09:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.17.1\n"
|
||||
"X-Generator: Weblate 2026.5\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:166
|
||||
@@ -608,20 +608,20 @@ msgstr ""
|
||||
"更を含みます。"
|
||||
|
||||
#: pretix/api/webhooks.py:413
|
||||
#, fuzzy
|
||||
#| msgid "Quota handling"
|
||||
msgid "Quota changed"
|
||||
msgstr "クォータの処理"
|
||||
msgstr "クォータが変更されました"
|
||||
|
||||
#: pretix/api/webhooks.py:414
|
||||
msgid ""
|
||||
"This includes related events like creation, deletion, opening or closing of "
|
||||
"quotas. No webhook is sent for changes to the resulting availability."
|
||||
msgstr ""
|
||||
"これには、クォータの作成、削除、開始または終了といった関連イベントが含まれま"
|
||||
"す。結果として得られる可用性の変更については、Webhookが送信されません。"
|
||||
|
||||
#: pretix/api/webhooks.py:419
|
||||
msgid "Shop taken live"
|
||||
msgstr "ショップが公開中になりました"
|
||||
msgstr "ショップがオンラインになりました"
|
||||
|
||||
#: pretix/api/webhooks.py:423
|
||||
msgid "Shop taken offline"
|
||||
@@ -3394,11 +3394,13 @@ msgid ""
|
||||
"The field \"%(label)s\" may not contain special characters such as "
|
||||
"\"%(chars)s\"."
|
||||
msgstr ""
|
||||
"フィールド「%(label)s」には、\"%(chars)s\" のような特殊文字を含めることはでき"
|
||||
"ません。"
|
||||
|
||||
#: pretix/base/forms/questions.py:305
|
||||
#, python-format
|
||||
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
|
||||
msgstr ""
|
||||
msgstr "フィールド「%(label)s」には URL (%(url)s) を含めることができません。"
|
||||
|
||||
#: pretix/base/forms/questions.py:338
|
||||
msgctxt "phonenumber"
|
||||
@@ -8189,19 +8191,14 @@ msgid "Program times"
|
||||
msgstr "プログラム時間"
|
||||
|
||||
#: pretix/base/pdf.py:503
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "2017-05-31 10:00 – 12:00\n"
|
||||
#| "2017-05-31 14:00 – 16:00\n"
|
||||
#| "2017-05-31 14:00 – 2017-06-01 14:00"
|
||||
msgid ""
|
||||
"2017-05-31 10:00 – 12:00, Room 1\n"
|
||||
"2017-05-31 14:00 – 16:00, Room 2\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00, Building A"
|
||||
msgstr ""
|
||||
"2017-05-31 10:00 – 12:00\n"
|
||||
"2017-05-31 14:00 – 16:00\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00"
|
||||
"2017-05-31 10:00 – 12:00、部屋1\n"
|
||||
"2017-05-31 14:00 – 16:00、部屋2\n"
|
||||
"2017-05-31 14:00 – 2017-06-01 14:00、ビルA"
|
||||
|
||||
#: pretix/base/pdf.py:507
|
||||
msgid "Reusable Medium ID"
|
||||
@@ -8710,13 +8707,7 @@ msgid "This voucher code is not known in our database."
|
||||
msgstr "このバウチャーコードは、当社のデータベースには登録されていません。"
|
||||
|
||||
#: pretix/base/services/cart.py:165
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products."
|
||||
#| msgid_plural ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching product."
|
||||
@@ -8728,15 +8719,7 @@ msgstr[0] ""
|
||||
"した場合にのみ使用できます。"
|
||||
|
||||
#: pretix/base/services/cart.py:170
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products. We have therefore removed some positions "
|
||||
#| "from your cart that can no longer be purchased like this."
|
||||
#| msgid_plural ""
|
||||
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
#| "%(number)s matching products. We have therefore removed some positions "
|
||||
#| "from your cart that can no longer be purchased like this."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The voucher code \"%(voucher)s\" can only be used if you select at least "
|
||||
"%(number)s matching product. We have therefore removed some positions from "
|
||||
@@ -13837,6 +13820,8 @@ msgid ""
|
||||
"You entered an URL, which is not allowed. Please remove %(match)s from your "
|
||||
"input."
|
||||
msgstr ""
|
||||
"URL を入力しましたが、許可されていません。入力から %(match)s を削除してくださ"
|
||||
"い。"
|
||||
|
||||
#: pretix/base/views/errors.py:48
|
||||
msgid ""
|
||||
@@ -15733,14 +15718,8 @@ msgid "inactive"
|
||||
msgstr "無効"
|
||||
|
||||
#: pretix/control/forms/item.py:1414
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Sample Conference Center\n"
|
||||
#| "Heidelberg, Germany"
|
||||
msgid "Sample Conference Center, Heidelberg, Germany"
|
||||
msgstr ""
|
||||
"サンプル・カンファレンスセンター\n"
|
||||
"ドイツ、ハイデルベルク"
|
||||
msgstr "サンプル・カンファレンスセンター, ドイツ, ハイデルベルク"
|
||||
|
||||
#: pretix/control/forms/mailsetup.py:42
|
||||
msgid "Hostname"
|
||||
@@ -22981,11 +22960,8 @@ msgid "Quota history"
|
||||
msgstr "クォータ履歴"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:6
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "Change multiple dates"
|
||||
msgid "Change multiple quotas"
|
||||
msgstr "複数の日付を変更"
|
||||
msgstr "複数のクォータを変更"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:8
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:8
|
||||
@@ -23031,17 +23007,14 @@ msgstr "以下の製品は販売できなくなる可能性があります:"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:4
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:6
|
||||
#, fuzzy
|
||||
#| msgid "Delete quota"
|
||||
msgid "Delete quotas"
|
||||
msgstr "クォータを削除"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:10
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Are you sure you want to delete the following dates?"
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the following quota?"
|
||||
msgid_plural "Are you sure you want to delete the following %(num)s quotas?"
|
||||
msgstr[0] "以下の日付を削除してもよろしいですか?"
|
||||
msgstr[0] "以下の%(num)sのクォータを削除してもよろしいですか?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quotas.html:9
|
||||
msgid ""
|
||||
@@ -23634,12 +23607,14 @@ msgid ""
|
||||
"generated once the customer pays the invoice or selects a payment method "
|
||||
"that requires an invoice."
|
||||
msgstr ""
|
||||
"この注文は、最後の請求書が生成された後に変更されました。新しい請求書はまだ作"
|
||||
"成されていません。請求書は支払い時に生成されるか、支払方法によって必要とされ"
|
||||
"る場合に設定されているためです。お客様が請求書を支払うか、請求書が必要な支払"
|
||||
"方法を選択すると、新しい請求書が生成されます。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:152
|
||||
#, fuzzy
|
||||
#| msgid "Request invoice"
|
||||
msgid "Reissue invoice"
|
||||
msgstr "請求書を要求"
|
||||
msgstr "請求書を再発行する"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:161
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:413
|
||||
@@ -24064,22 +24039,15 @@ msgid "How should the refund be sent?"
|
||||
msgstr "どのように払い戻しますか?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:25
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Any payments that you selected for automatical refunds will be "
|
||||
#| "immediately communicate the refund request to the respective payment "
|
||||
#| "provider. Manual refunds will be created as pending refunds, you can then "
|
||||
#| "later mark them as done once you actually transferred the money back to "
|
||||
#| "the customer."
|
||||
msgid ""
|
||||
"Any payments you selected for automatic refunds will have the refund request "
|
||||
"sent immediately to the respective payment provider. Manual refunds will be "
|
||||
"created as pending refunds, which you can later mark as done once you have "
|
||||
"actually transferred the money back to the customer."
|
||||
msgstr ""
|
||||
"自動払い戻しに選択した支払いは、該当する決済プロバイダーに払い戻し要求が即座"
|
||||
"に通知されます。手動払い戻しは保留中の払い戻しとして作成され、実際に顧客に送"
|
||||
"金した後で完了済みとしてマークできます。"
|
||||
"自動返金をご選択いただいたすべての支払いについては、返金リクエストが直ちに該"
|
||||
"当する決済プロバイダーへ送信されます。手動返金は保留中の返金として作成され、"
|
||||
"実際に顧客に返金した後で完了としてマークできます。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:32
|
||||
msgid "Refund to original payment method"
|
||||
@@ -28504,11 +28472,8 @@ msgid "The new question has been created."
|
||||
msgstr "新しい質問が作成されました。"
|
||||
|
||||
#: pretix/control/views/item.py:918
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "The selected dates have been deleted or disabled."
|
||||
msgid "The selected quotas have been deleted or disabled."
|
||||
msgstr "選択した日付は削除されたか無効になっています。"
|
||||
msgstr "選択したクォータは削除されたか無効です。"
|
||||
|
||||
#: pretix/control/views/item.py:1074
|
||||
msgid "The new quota has been created."
|
||||
@@ -29215,10 +29180,9 @@ msgid "This plugin is currently not allowed for this organizer account."
|
||||
msgstr "このプラグインは現在、この主催者アカウントでは許可されていません。"
|
||||
|
||||
#: pretix/control/views/organizer.py:832
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "This plugin can be enabled or disabled for events individually."
|
||||
#, python-brace-format
|
||||
msgid "This plugin cannot be activated for event {}."
|
||||
msgstr "このプラグインは、イベントごとに個別に有効化または無効化できます。"
|
||||
msgstr "このプラグインは、イベント{}に対してアクティベートできません。"
|
||||
|
||||
#: pretix/control/views/organizer.py:901
|
||||
msgid "The team has been created. You can now add members to the team."
|
||||
@@ -30236,10 +30200,9 @@ msgid "{width} x {height} mm label"
|
||||
msgstr "{width} x {height} mm ラベル"
|
||||
|
||||
#: pretix/plugins/badges/templates.py:265
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "{width} x {height} mm label"
|
||||
#, python-brace-format
|
||||
msgid "{width} x {height} inch label"
|
||||
msgstr "{width} x {height} mm ラベル"
|
||||
msgstr "{width} x {height} インチラベル"
|
||||
|
||||
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_info.html:16
|
||||
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:27
|
||||
|
||||
@@ -8,16 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
|
||||
"PO-Revision-Date: 2026-02-01 21:00+0000\n"
|
||||
"Last-Translator: z3rrry <z3rrry@gmail.com>\n"
|
||||
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/ko/"
|
||||
">\n"
|
||||
"PO-Revision-Date: 2026-06-01 09:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ko/>\n"
|
||||
"Language: ko\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
"X-Generator: Weblate 2026.5\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:166
|
||||
@@ -48,7 +48,7 @@ msgstr "사전판매 시작하지 않음"
|
||||
#: pretix/control/templates/pretixcontrol/subevents/index.html:176
|
||||
#: pretix/control/views/dashboards.py:549
|
||||
msgid "On sale"
|
||||
msgstr ""
|
||||
msgstr "세일 중"
|
||||
|
||||
#: pretix/_base_settings.py:89
|
||||
msgid "English"
|
||||
@@ -427,10 +427,8 @@ msgstr ""
|
||||
|
||||
#: pretix/api/serializers/organizer.py:495
|
||||
#: pretix/control/views/organizer.py:1035
|
||||
#, fuzzy
|
||||
#| msgid "pretix account invitation"
|
||||
msgid "Account invitation"
|
||||
msgstr "프레틱스 계정 초대"
|
||||
msgstr "계정 초대"
|
||||
|
||||
#: pretix/api/serializers/organizer.py:516
|
||||
#: pretix/control/views/organizer.py:1134
|
||||
@@ -18087,10 +18085,8 @@ msgid "A payment has been performed."
|
||||
msgstr "수동 거래가 수행되었습니다."
|
||||
|
||||
#: pretix/control/logdisplay.py:807
|
||||
#, fuzzy
|
||||
#| msgid "A manual transaction has been performed."
|
||||
msgid "A refund has been performed. "
|
||||
msgstr "수동 거래가 수행되었습니다."
|
||||
msgstr "환불이 처리되었습니다. "
|
||||
|
||||
#: pretix/control/logdisplay.py:808
|
||||
#, python-brace-format
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
|
||||
"PO-Revision-Date: 2026-05-21 15:08+0000\n"
|
||||
"PO-Revision-Date: 2026-06-01 09:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/"
|
||||
"projects/pretix/pretix/zh_Hant/>\n"
|
||||
@@ -595,16 +595,16 @@ msgid ""
|
||||
msgstr "這包括新增或刪除的產品,以及對變體或捆綁等巢狀物件的更改。"
|
||||
|
||||
#: pretix/api/webhooks.py:413
|
||||
#, fuzzy
|
||||
#| msgid "Quota handling"
|
||||
msgid "Quota changed"
|
||||
msgstr "額度處理"
|
||||
msgstr "配額改變了"
|
||||
|
||||
#: pretix/api/webhooks.py:414
|
||||
msgid ""
|
||||
"This includes related events like creation, deletion, opening or closing of "
|
||||
"quotas. No webhook is sent for changes to the resulting availability."
|
||||
msgstr ""
|
||||
"這包括建立、刪除、開啟或關閉配額等相關事件。 沒有傳送webhook來更改結果的可用"
|
||||
"性。"
|
||||
|
||||
#: pretix/api/webhooks.py:419
|
||||
msgid "Shop taken live"
|
||||
@@ -650,7 +650,7 @@ msgstr "優惠券已更改"
|
||||
msgid ""
|
||||
"Only includes explicit changes to the voucher, not e.g. an increase of the "
|
||||
"number of redemptions."
|
||||
msgstr ""
|
||||
msgstr "僅包括對代金券的明確更改,例如不包括兌換次數的增加。"
|
||||
|
||||
#: pretix/api/webhooks.py:460
|
||||
msgid "Voucher deleted"
|
||||
@@ -669,22 +669,16 @@ msgid "Customer account anonymized"
|
||||
msgstr "客戶帳戶已匿名化"
|
||||
|
||||
#: pretix/api/webhooks.py:476
|
||||
#, fuzzy
|
||||
#| msgid "Gift card code"
|
||||
msgid "Gift card added"
|
||||
msgstr "禮品卡代碼"
|
||||
msgstr "添加了禮品卡"
|
||||
|
||||
#: pretix/api/webhooks.py:480
|
||||
#, fuzzy
|
||||
#| msgid "Gift card code"
|
||||
msgid "Gift card modified"
|
||||
msgstr "禮品卡代碼"
|
||||
msgstr "禮品卡修改了"
|
||||
|
||||
#: pretix/api/webhooks.py:484
|
||||
#, fuzzy
|
||||
#| msgid "Gift card transactions"
|
||||
msgid "Gift card used in transaction"
|
||||
msgstr "禮品卡交易"
|
||||
msgstr "交易中使用的禮品卡"
|
||||
|
||||
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
|
||||
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:1074
|
||||
|
||||
@@ -158,12 +158,7 @@ class OrderMailForm(BaseMailForm):
|
||||
),
|
||||
label=pgettext_lazy('sendmail_form', 'Restrict to products'),
|
||||
required=True,
|
||||
queryset=Item.objects.none(),
|
||||
help_text=pgettext_lazy(
|
||||
'sendmail_form',
|
||||
'There may be multiple mails sent out to the same mail address if one order contains multiple attendee '
|
||||
'products for it, if you restrict to products while also restricting mails to attendees only. '
|
||||
'This is intended, as every one of those get linked to their own separate order page restricted to only that product.')
|
||||
queryset=Item.objects.none()
|
||||
)
|
||||
filter_checkins = forms.BooleanField(
|
||||
label=_('Filter check-in status'),
|
||||
@@ -371,11 +366,6 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
del self.fields['subevent']
|
||||
|
||||
self.fields['limit_products'].queryset = Item.objects.filter(event=self.event)
|
||||
self.fields['limit_products'].help_text = pgettext_lazy(
|
||||
'sendmail_form',
|
||||
'There may be multiple mails sent out to the same mail address if one order contains multiple attendee '
|
||||
'products for it, if you restrict to products while also restricting mails to attendees only. '
|
||||
'This is intended, as every one of those get linked to their own separate order page restricted to only that product.')
|
||||
|
||||
self.fields['schedule_type'] = forms.ChoiceField(
|
||||
label=_('Type of schedule time'),
|
||||
|
||||
@@ -85,6 +85,8 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
)
|
||||
),
|
||||
).prefetch_related('addons', 'subevent'):
|
||||
if p.addon_to_id is not None:
|
||||
continue
|
||||
|
||||
if p.item_id not in items and not any(a.item_id in items for a in p.addons.all()):
|
||||
continue
|
||||
@@ -104,10 +106,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
|
||||
if p.attendee_email == o.email and send_to_order:
|
||||
continue
|
||||
# the amount of mails could be further restricted if we filter out those where the addon-attendee-email
|
||||
# is the same as the main-product-attendee-email, however, that bears many issues, e.g. if one of
|
||||
# those mail-addresses was only a placeholder or if restrictions are set and the main-product is
|
||||
# excluded -- for now it seems best not to filter them at this point in time
|
||||
|
||||
if subevent and p.subevent_id != subevent:
|
||||
continue
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import functools
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import random
|
||||
|
||||
from django import forms
|
||||
@@ -32,7 +30,6 @@ from django.contrib.auth.password_validation import (
|
||||
)
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core import signing
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
@@ -44,6 +41,7 @@ from pretix.base.forms.questions import (
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import Customer
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
|
||||
@@ -205,23 +203,6 @@ class RegistrationForm(forms.Form):
|
||||
min_value=0,
|
||||
)
|
||||
|
||||
@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_customer_registration_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
|
||||
@@ -255,17 +236,11 @@ class RegistrationForm(forms.Form):
|
||||
code='incomplete'
|
||||
)
|
||||
else:
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 600)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
if rate_limit("customer_signup", include_ip_from_request=self.request, max_num=10, expire_time=600):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
return self.cleaned_data
|
||||
|
||||
def create(self):
|
||||
@@ -370,13 +345,8 @@ class ResetPasswordForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('email') and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwreset_customer_%s' % self.customer.pk)
|
||||
rc.expire('pretix_pwreset_customer_%s' % self.customer.pk, 600)
|
||||
if cnt > 2:
|
||||
if d.get('email'):
|
||||
if rate_limit("customer_pwreset", self.customer.pk, max_num=2, expire_time=600):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
@@ -445,13 +415,8 @@ class ChangePasswordForm(forms.Form):
|
||||
def clean_password_current(self):
|
||||
old_pw = self.cleaned_data.get('password_current')
|
||||
|
||||
if old_pw and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_customer_%s' % self.customer.pk)
|
||||
rc.expire('pretix_pwchange_customer_%s' % self.customer.pk, 300)
|
||||
if cnt > 10:
|
||||
if old_pw:
|
||||
if rate_limit("customer_pwchange", self.customer.pk, max_num=10, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
@@ -521,17 +486,11 @@ class ChangeInfoForm(forms.ModelForm):
|
||||
old_pw = self.cleaned_data.get('password_current')
|
||||
|
||||
if old_pw:
|
||||
if settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_customer_%s' % self.instance.pk)
|
||||
rc.expire('pretix_pwchange_customer_%s' % self.instance.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
if rate_limit("customer_pwchange", self.instance.pk, max_num=10, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if not check_password(old_pw, self.instance.password):
|
||||
raise forms.ValidationError(
|
||||
|
||||
@@ -174,6 +174,7 @@ class OrderPositionDetailMixin(NoSearchIndexViewMixin):
|
||||
def position(self):
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
addon_to__isnull=True,
|
||||
order__code=self.kwargs['order'],
|
||||
positionid=self.kwargs['position']
|
||||
).select_related('order', 'order__event')
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -42,6 +41,7 @@ from django.views.generic import TemplateView
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.services.mail import INVALID_ADDRESS, mail
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.user import ResendLinkForm
|
||||
from pretix.presale.views import EventViewMixin
|
||||
@@ -61,17 +61,12 @@ class ResendLinkView(EventViewMixin, TemplateView):
|
||||
|
||||
user = self.link_form.cleaned_data.get('email')
|
||||
|
||||
if settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
if rc.exists('pretix_resend_{}_{}'.format(request.event.pk, user)):
|
||||
messages.error(request, _('If the email address you entered is valid and associated with a ticket, we have '
|
||||
'already sent you an email with a link to your ticket in the past {number} hours. '
|
||||
'If the email did not arrive, please check your spam folder and also double check '
|
||||
'that you used the correct email address.').format(number=24))
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link'))
|
||||
else:
|
||||
rc.setex('pretix_resend_{}_{}'.format(request.event.pk, user), 3600 * 24, '1')
|
||||
if rate_limit("order_resend", self.request.event.pk, user, max_num=1, expire_time=3600 * 24):
|
||||
messages.error(request, _('If the email address you entered is valid and associated with a ticket, we have '
|
||||
'already sent you an email with a link to your ticket in the past {number} hours. '
|
||||
'If the email did not arrive, please check your spam folder and also double check '
|
||||
'that you used the correct email address.').format(number=24))
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link'))
|
||||
|
||||
orders = self.request.event.orders.filter(email__iexact=user)
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ def fakeredis_client(monkeypatch):
|
||||
redis = get_redis_connection("default", True)
|
||||
redis.flushall()
|
||||
monkeypatch.setattr('django_redis.get_redis_connection', get_redis_connection, raising=False)
|
||||
monkeypatch.setattr('pretix.base.metrics.redis', redis, raising=False)
|
||||
yield redis
|
||||
|
||||
|
||||
|
||||
@@ -504,33 +504,8 @@ class Login2FAFormTest(TestCase):
|
||||
assert "recovery code" in djmail.outbox[0].body
|
||||
|
||||
|
||||
class FakeRedis(object):
|
||||
def get_redis_connection(self, connection_string):
|
||||
return self
|
||||
|
||||
def __init__(self):
|
||||
self.storage = {}
|
||||
|
||||
def pipeline(self):
|
||||
return self
|
||||
|
||||
def hincrbyfloat(self, rkey, key, amount):
|
||||
return self
|
||||
|
||||
def commit(self):
|
||||
return self
|
||||
|
||||
def exists(self, rkey):
|
||||
return rkey in self.storage
|
||||
|
||||
def setex(self, rkey, value, expiration):
|
||||
self.storage[rkey] = value
|
||||
|
||||
def execute(self):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
@pytest.mark.usefixtures("fakeredis_client")
|
||||
class PasswordRecoveryFormTest(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -560,11 +535,6 @@ class PasswordRecoveryFormTest(TestCase):
|
||||
|
||||
@override_settings(HAS_REDIS=True)
|
||||
def test_email_reset_twice_redis(self):
|
||||
fake_redis = FakeRedis()
|
||||
m = self.monkeypatch
|
||||
m.setattr('django_redis.get_redis_connection', fake_redis.get_redis_connection, raising=False)
|
||||
m.setattr('pretix.base.metrics.redis', fake_redis, raising=False)
|
||||
|
||||
djmail.outbox = []
|
||||
|
||||
response = self.client.post('/control/forgot', {
|
||||
|
||||
Reference in New Issue
Block a user