Compare commits

..

6 Commits

Author SHA1 Message Date
Mira Weller
f8e5c7867b add log message with json path on schema validation error 2025-11-18 18:15:23 +01:00
Mira Weller
7010752bb0 Fix schema 2025-11-18 18:15:23 +01:00
Mira Weller
7d8bcd4b10 Migrate log action types to registry 2025-11-18 18:15:23 +01:00
Mira Weller
2121566ae5 Fix incorrect or missing log action types 2025-11-05 20:27:56 +01:00
Mira Weller
9f6216e6f1 Add some example schemas 2025-11-05 20:27:56 +01:00
Mira Weller
c824663946 Implement schema validation and schema-based shredding 2025-11-05 19:41:47 +01:00
59 changed files with 689 additions and 1927 deletions

View File

@@ -19,7 +19,6 @@ at :ref:`plugin-docs`.
item_bundles
item_add-ons
item_meta_properties
item_program_times
questions
question_options
quotas

View File

@@ -1,222 +0,0 @@
Item program times
==================
Resource description
--------------------
Program times for products (items) that can be set in addition to event times, e.g. to display seperate schedules within an event.
The program times resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the program time
start datetime The start date time for this program time slot.
end datetime The end date time for this program time slot.
===================================== ========================== =======================================================
.. versionchanged:: TODO
The resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
Returns a list of all program times for a given item.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/program_times/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 3,
"next": null,
"previous": null,
"results": [
{
"id": 2,
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
},
{
"id": 3,
"start": "2025-08-12T22:00:00Z",
"end": "2025-08-13T22:00:00Z"
},
{
"id": 14,
"start": "2025-08-15T22:00:00Z",
"end": "2025-08-17T22:00:00Z"
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
Returns information on one program time, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"start": "2025-08-15T22:00:00Z",
"end": "2025-10-27T23:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:param id: The ``id`` field of the program time to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
Creates a new program time
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 17,
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for
:param event: The ``slug`` field of the event to create a program time for
:param item: The ``id`` field of the item to create a program time for
:statuscode 201: no error
:statuscode 400: The program time could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
Update a program time. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"start": "2025-08-14T10:00:00Z"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"start": "2025-08-14T10:00:00Z",
"end": "2025-08-15T12:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to modify
:param id: The ``id`` field of the program time to modify
:statuscode 200: no error
:statuscode 400: The program time could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/program_times/(id)/
Delete a program time.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to modify
:param id: The ``id`` field of the program time to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -139,9 +139,6 @@ has_variations boolean Shows whether
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
program_times list of objects A list with one object for each program time of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
├ id integer Internal ID of the variation
├ value multi-lingual string The "name" of the variation
├ default_price money (string) The price set directly for this variation or ``null``
@@ -228,10 +225,6 @@ meta_data object Values set fo
The ``hidden_if_item_available_mode`` attributes has been added.
.. versionchanged:: 2025.9
The ``program_times`` attribute has been added.
Notes
-----
@@ -239,9 +232,9 @@ Please note that an item either always has variations or never has. Once created
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
one variation.
Also note that ``variations``, ``bundles``, ``addons`` and ``program_times`` are only supported on ``POST``. To update/delete variations,
bundles, add-ons and program times please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles``, ``addons`` and/or ``program_times``.
Also note that ``variations``, ``bundles``, and ``addons`` are only supported on ``POST``. To update/delete variations,
bundles, and add-ons please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles`` and/or ``addons``.
Endpoints
---------
@@ -380,8 +373,7 @@ Endpoints
}
],
"addons": [],
"bundles": [],
"program_times": []
"bundles": []
}
]
}
@@ -533,8 +525,7 @@ Endpoints
}
],
"addons": [],
"bundles": [],
"program_times": []
"bundles": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -662,13 +653,7 @@ Endpoints
}
],
"addons": [],
"bundles": [],
"program_times": [
{
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
}
]
"bundles": []
}
**Example response**:
@@ -788,13 +773,7 @@ Endpoints
}
],
"addons": [],
"bundles": [],
"program_times": [
{
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
}
]
"bundles": []
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
@@ -810,9 +789,8 @@ Endpoints
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``has_variations``, ``variations``, ``addon`` and the
``program_times`` field. If you need to update/delete variations, add-ons or program times, please use the nested
dedicated endpoints.
You can change all fields of the resource except the ``has_variations``, ``variations`` and the ``addon`` field. If
you need to update/delete variations or add-ons please use the nested dedicated endpoints.
**Example request**:
@@ -946,8 +924,7 @@ Endpoints
}
],
"addons": [],
"bundles": [],
"program_times": []
"bundles": []
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -35,7 +35,7 @@ dependencies = [
"cryptography>=44.0.0",
"css-inline==0.18.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.26",
"Django[argon2]==4.2.*,>=4.2.24",
"django-bootstrap3==25.2",
"django-compressor==4.5.1",
"django-countries==7.6.*",

View File

@@ -47,9 +47,8 @@ from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SalesChannel,
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
)
@@ -188,12 +187,6 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
'position', 'price_included', 'multi_allowed')
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('start', 'end')
class ItemBundleSerializer(serializers.ModelSerializer):
class Meta:
model = ItemBundle
@@ -219,31 +212,6 @@ class ItemBundleSerializer(serializers.ModelSerializer):
return data
class ItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('id', 'start', 'end')
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
start = full_data.get('start')
if not start:
raise ValidationError(_("The program start must not be empty."))
end = full_data.get('end')
if not end:
raise ValidationError(_("The program end must not be empty."))
if start > end:
raise ValidationError(_("The program end must not be before the program start."))
return data
class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
@@ -282,7 +250,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True, required=False)
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
@@ -304,7 +271,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'program_times', 'original_price', 'require_approval', 'generate_tickets',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
@@ -327,9 +294,9 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data or 'program_times' in data):
raise ValidationError(_('Updating add-ons, bundles, program times or variations via PATCH/PUT is not '
'supported. Please use the dedicated nested endpoint.'))
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
'dedicated nested endpoint.'))
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
@@ -380,13 +347,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
return value
def validate_program_times(self, value):
if not self.instance:
for program_time_data in value:
ItemProgramTime.clean_start_end(self, start=program_time_data.get('start', None),
end=program_time_data.get('end', None))
return value
@cached_property
def item_meta_properties(self):
return {
@@ -404,7 +364,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
program_times_data = validated_data.pop('program_times') if 'program_times' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
require_membership_types = validated_data.pop('require_membership_types', [])
@@ -439,8 +398,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data)
for program_time_data in program_times_data:
ItemProgramTime.objects.create(item=item, **program_time_data)
# Meta data
if meta_data is not None:

View File

@@ -112,7 +112,6 @@ item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
item_router.register(r'bundles', item.ItemBundleViewSet)
item_router.register(r'program_times', item.ItemProgramTimeViewSet)
order_router = routers.DefaultRouter()
order_router.register(r'payments', order.PaymentViewSet)

View File

@@ -46,13 +46,13 @@ from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
QuestionSerializer, QuotaSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
ItemVariation, Question, QuestionOption, Quota,
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
@@ -279,57 +279,6 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
)
class ItemProgramTimeViewSet(viewsets.ModelViewSet):
serializer_class = ItemProgramTimeSerializer
queryset = ItemProgramTime.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id',)
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
@cached_property
def item(self):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
return self.item.program_times.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['item'] = self.item
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(item=item)
item.log_action(
'pretix.event.item.program_times.added',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
'pretix.event.item.program_times.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.item.log_action(
'pretix.event.item.program_times.removed',
user=self.request.user,
auth=self.request.auth,
data={'start': instance.start, 'end': instance.end}
)
class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer_class = ItemAddOnSerializer
queryset = ItemAddOn.objects.none()

View File

@@ -800,7 +800,7 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
identifier=serializer.instance.identifier,
)
inst.log_action(
'pretix.sales_channel.changed',
'pretix.saleschannel.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,

View File

@@ -149,7 +149,7 @@ class ItemDataExporter(ListExporter):
row += [
_("Yes") if i.active and v.active else "",
", ".join([str(sn.label) for sn in sales_channels]),
v.default_price if v.default_price is not None else i.default_price,
v.default_price or i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",

View File

@@ -214,38 +214,21 @@ class PasswordRecoverForm(forms.Form):
error_messages = {
'pw_mismatch': _("Please enter the same password twice"),
}
email = forms.EmailField(
max_length=255,
disabled=True,
label=_("Your email address"),
widget=forms.EmailInput(
attrs={'autocomplete': 'username'},
),
)
password = forms.CharField(
label=_('Password'),
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password',
}),
widget=forms.PasswordInput,
max_length=4096,
required=True
)
password_repeat = forms.CharField(
label=_('Repeat password'),
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password',
}),
widget=forms.PasswordInput,
max_length=4096,
)
def __init__(self, user_id=None, *args, **kwargs):
initial = kwargs.pop('initial', {})
try:
self.user = User.objects.get(id=user_id)
initial['email'] = self.user.email
except User.DoesNotExist:
self.user = None
super().__init__(*args, initial=initial, **kwargs)
self.user_id = user_id
super().__init__(*args, **kwargs)
def clean(self):
password1 = self.cleaned_data.get('password', '')
@@ -260,7 +243,11 @@ class PasswordRecoverForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
if validate_password(password1, user=self.user) is not None:
try:
user = User.objects.get(id=self.user_id)
except User.DoesNotExist:
user = None
if validate_password(password1, user=user) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
@@ -320,10 +307,3 @@ class ReauthForm(forms.Form):
self.error_messages['inactive'],
code='inactive',
)
class ConfirmationCodeForm(forms.Form):
code = forms.IntegerField(
label=_('Confirmation code'),
widget=forms.NumberInput(attrs={'class': 'confirmation-code-input', 'inputmode': 'numeric', 'type': 'text'}),
)

View File

@@ -39,16 +39,37 @@ from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.db.models import Q
from django.urls.base import reverse
from django.utils.translation import gettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User
from pretix.control.forms import SingleLanguageWidget
from pretix.helpers.format import format_map
class UserSettingsForm(forms.ModelForm):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
'pw_current': _("Please enter your current password if you want to change your email address "
"or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
old_pw = forms.CharField(max_length=255,
required=False,
label=_("Your current password"),
widget=forms.PasswordInput())
new_pw = forms.CharField(max_length=255,
required=False,
label=_("New password"),
widget=forms.PasswordInput())
new_pw_repeat = forms.CharField(max_length=255,
required=False,
label=_("Repeat new password"),
widget=forms.PasswordInput())
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
@@ -72,60 +93,11 @@ class UserSettingsForm(forms.ModelForm):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['email'].required = True
self.fields['email'].disabled = True
self.fields['email'].help_text = format_map('<a href="{link}"><span class="fa fa-edit"></span> {text}</a>', {
'text': _("Change email address"),
'link': reverse('control:user.settings.email.change')
})
class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))
class UserPasswordChangeForm(forms.Form):
error_messages = {
'pw_current': _("Please enter your current password if you want to change your email address "
"or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
email = forms.EmailField(max_length=255,
disabled=True,
label=_("Your email address"),
widget=forms.EmailInput(
attrs={'autocomplete': 'username'},
))
old_pw = forms.CharField(max_length=255,
required=False,
label=_("Your current password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'current-password'},
))
new_pw = forms.CharField(max_length=255,
required=False,
label=_("New password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'new-password'},
))
new_pw_repeat = forms.CharField(max_length=255,
required=False,
label=_("Repeat new password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'new-password'},
))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
initial = kwargs.pop('initial', {})
initial['email'] = self.user.email
super().__init__(*args, initial=initial, **kwargs)
if self.user.auth_backend != 'native':
del self.fields['old_pw']
del self.fields['new_pw']
del self.fields['new_pw_repeat']
self.fields['email'].disabled = True
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')
@@ -149,6 +121,15 @@ class UserPasswordChangeForm(forms.Form):
return old_pw
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.instance.pk)).exists():
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
return email
def clean_new_pw(self):
password1 = self.cleaned_data.get('new_pw', '')
if password1 and validate_password(password1, user=self.user) is not None:
@@ -167,24 +148,32 @@ class UserPasswordChangeForm(forms.Form):
code='pw_mismatch'
)
def clean(self):
password1 = self.cleaned_data.get('new_pw')
email = self.cleaned_data.get('email')
old_pw = self.cleaned_data.get('old_pw')
class UserEmailChangeForm(forms.Form):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
}
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')
super().__init__(*args, **kwargs)
def clean_new_email(self):
email = self.cleaned_data['new_email']
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
if (password1 or email != self.user.email) and not old_pw:
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
self.error_messages['pw_current'],
code='pw_current'
)
return email
if password1 and password1 == old_pw:
raise forms.ValidationError(
self.error_messages['pw_equal'],
code='pw_equal'
)
if password1:
self.instance.set_password(password1)
return self.cleaned_data
class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))

View File

@@ -23,7 +23,6 @@ import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
@@ -753,59 +752,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return dt.astimezone(tz).date()
total = Decimal('0.00')
if has_taxes:
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
colwidths = [a * doc.width for a in (.65, .20, .15)]
for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby(
all_lines,
key=_group_key,
is_addon=lambda l: l.description.startswith(" +"),
):
# split description into multiple Paragraphs so each fits in a table cell on a single page
# otherwise PDF-build fails
description_p_list = []
# normalize linebreaks to newlines instead of HTML so we can safely substring
description = description.replace('<br>', '<br />').replace('<br />\n', '\n').replace('<br />', '\n')
# start first line with different settings than the rest of the description
curr_description = description.split("\n", maxsplit=1)[0]
cellpadding = 6 # default cellpadding is only set on right side of column
max_width = colwidths[0] - cellpadding
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p_style
)
h = p.wrap(max_width, doc.height)[1]
if h <= max_height:
description_p_list.append(p)
if curr_description == description:
break
description = description[len(curr_description):].lstrip()
curr_description = description.split("\n", maxsplit=1)[0]
# use different settings for all except first line
max_width = sum(colwidths[0:3 if has_taxes else 2]) - cellpadding
max_height = self.stylesheet['Fineprint'].leading * 8
p_style = self.stylesheet['Fineprint']
continue
if not description_p_list:
# first "manual" line is larger than 5 "real" lines => only allow one line and set rest in Fineprint
max_height = self.stylesheet['Normal'].leading
if h > max_height * 1.1:
# quickly bring the text-length down to a managable length to then stepwise reduce
wrap_to = math.ceil(len(curr_description) * max_height * 1.1 / h)
else:
# trim to 95% length, but at most 10 chars to not have strangely short lines in the middle of a paragraph
wrap_to = max(len(curr_description) - 10, math.ceil(len(curr_description) * 0.95))
curr_description = textwrap.wrap(curr_description, wrap_to, replace_whitespace=False, drop_whitespace=False)[0]
# Try to be clever and figure out when organizers would want to show the period. This heuristic is
# not perfect and the only "fully correct" way would be to include the period on every line always,
# however this will cause confusion (a) due to useless repetition of the same date all over the invoice
@@ -859,10 +810,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description_p_list.append(FontFallbackParagraph(
period_line,
self.stylesheet['Fineprint']
))
description += "\n" + period_line
lines = list(lines)
if has_taxes:
@@ -871,13 +819,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
description = description + "\n" + single_price_line
tdata.append((
description_p_list.pop(0),
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
str(len(lines)),
localize(tax_rate) + " %",
FontFallbackParagraph(
@@ -889,52 +837,23 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['NormalRight']
),
))
for p in description_p_list:
tdata.append((p, "", "", "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(2, len(tdata) - 1),
))
else:
if len(lines) > 1:
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
description = description + "\n" + single_price_line
tdata.append((
description_p_list.pop(0),
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
str(len(lines)),
FontFallbackParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
))
for p in description_p_list:
tdata.append((p, "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(1, len(tdata) - 1),
))
tstyledata += [
(
'BOTTOMPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 2),
0
),
(
'TOPPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 1),
0
),
]
taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines)
grossvalue_map[tax_rate, tax_name] += gross_value * len(lines)
total += gross_value * len(lines)
@@ -944,11 +863,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .20, .15)]
if not self.invoice.is_cancellation:
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:

View File

@@ -19,15 +19,20 @@
# 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 json
import logging
from collections import defaultdict
from functools import cached_property
from typing import Optional
import jsonschema
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from pretix.base.signals import PluginAwareRegistry
logger = logging.getLogger(__name__)
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
if a_map:
@@ -105,12 +110,38 @@ They are annotated with their ``action_type`` and the defining ``plugin``.
log_entry_types = LogEntryTypeRegistry()
def prepare_schema(schema):
def handle_properties(t):
return {"shred_properties": [k for k, v in t["properties"].items() if v["shred"]]}
def walk_tree(schema):
if type(schema) is dict:
new_keys = {}
for k, v in schema.items():
if k == "properties":
new_keys = handle_properties(schema)
walk_tree(v)
if schema.get("type") == "object" and "additionalProperties" not in new_keys:
new_keys["additionalProperties"] = False
schema.update(new_keys)
elif type(schema) is list:
for v in schema:
walk_tree(v)
walk_tree(schema)
return schema
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
data_schema = None # {"type": "object", "properties": []}
def __init__(self, action_type=None, plain=None):
if self.data_schema:
print(self.__class__.__name__, "has schema", self._prepared_schema)
if action_type:
self.action_type = action_type
if plain:
@@ -147,12 +178,37 @@ class LogEntryType:
object_link_wrapper = '{val}'
def validate_data(self, parsed_data):
if not self._prepared_schema:
return
try:
jsonschema.validate(parsed_data, self._prepared_schema)
except jsonschema.exceptions.ValidationError as ex:
logger.warning("%s schema validation failed: %s %s", type(self).__name__, ex.json_path, ex.message)
raise
@cached_property
def _prepared_schema(self):
if self.data_schema:
return prepare_schema(self.data_schema)
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
raise NotImplementedError
if self._prepared_schema:
def shred_fun(validator, value, instance, schema):
for key in value:
instance[key] = "##########"
v = jsonschema.validators.extend(jsonschema.validators.Draft202012Validator,
validators={"shred_properties": shred_fun})
data = logentry.parsed_data
jsonschema.validate(data, self._prepared_schema, v)
logentry.data = json.dumps(data)
else:
raise NotImplementedError
class NoOpShredderMixin:

View File

@@ -1,25 +0,0 @@
# Generated by Django 4.2.19 on 2025-08-11 10:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0293_cartposition_price_includes_rounding_correction_and_more'),
]
operations = [
migrations.CreateModel(
name='ItemProgramTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('item',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_times',
to='pretixbase.item')),
],
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2.23 on 2025-09-04 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0294_item_program_time"),
]
operations = [
migrations.AddField(
model_name="user",
name="is_verified",
field=models.BooleanField(default=False),
),
]

View File

@@ -36,9 +36,8 @@ from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
itempicture_upload_to,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SubEventItem, SubEventItemVariation, itempicture_upload_to,
)
from .log import LogEntry
from .media import ReusableMedium

View File

@@ -35,7 +35,6 @@
import binascii
import json
import operator
import secrets
from datetime import timedelta
from functools import reduce
@@ -45,7 +44,6 @@ from django.contrib.auth.models import (
)
from django.contrib.auth.tokens import default_token_generator
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import IntegrityError, models, transaction
from django.db.models import Q
from django.utils.crypto import get_random_string, salted_hmac
@@ -241,11 +239,9 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
MAX_CONFIRMATION_CODE_ATTEMPTS = 10
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('Email'), max_length=190)
is_verified = models.BooleanField(default=False, verbose_name=_('Verified email address'))
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
@@ -357,77 +353,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
except SendMailException:
pass # Already logged
def send_confirmation_code(self, session, reason, email=None, state=None):
"""
Sends a confirmation code via email to the user. The code is only valid for the action specified by `reason`.
The email is either sent to the email address currently on file for the user, or to the one given in the optional `email` parameter.
A `state` value can be provided which is bound to this confirmation code, and returned on successfully checking the code.
:param session: the user's request session
:param reason: the action which should be confirmed using this confirmation code (currently, only `email_change` is allowed)
:param email: optional, the email address to send the confirmation code to
:param state: optional
"""
from pretix.base.services.mail import mail
with language(self.locale):
if reason == 'email_change':
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 confirm that your email address {email} belongs to your pretix account, use the following code:').format(
email=self.email,
))
else:
raise Exception('Invalid confirmation code reason')
code = "%07d" % secrets.SystemRandom().randint(0, 9999999)
session['user_confirmation_code:' + reason] = {
'code': code,
'state': state,
'attempts': 0,
}
mail(
email or self.email,
_('pretix confirmation code'),
'pretixcontrol/email/confirmation_code.txt',
{
'user': self,
'reason': msg,
'code': code,
},
event=None,
user=self,
locale=self.locale
)
def check_confirmation_code(self, session, reason, code):
"""
Checks a confirmation code entered by the user against the valid code stored in the session.
If the code is correct, an optional state bound to the code is returned.
If the code is incorrect, PermissionDenied is raised. If the code could not be validated, either because no
code for the given reason is stored, or the number of input attempts is exceeded, BadRequest is raised.
:param session: the user's request session
:param reason: the action which should be confirmed using this confirmation code
:param code: the code entered by the user
:return: optional state bound to this code using the state parameter of send_confirmation_code, None otherwise
"""
stored = session.get('user_confirmation_code:' + reason)
if not stored:
raise BadRequest
if stored['attempts'] > User.MAX_CONFIRMATION_CODE_ATTEMPTS:
raise BadRequest
if int(stored['code']) == int(code):
del session['user_confirmation_code:' + reason]
return stored['state']
else:
stored['attempts'] += 1
session['user_confirmation_code:' + reason] = stored
raise PermissionDenied
def send_password_reset(self):
from pretix.base.services.mail import mail

View File

@@ -80,6 +80,7 @@ class LoggingMixin:
from pretix.api.models import OAuthAccessToken, OAuthApplication
from pretix.api.webhooks import notify_webhooks
from ..logentrytype_registry import log_entry_types
from ..services.notifications import notify
from .devices import Device
from .event import Event
@@ -124,7 +125,13 @@ class LoggingMixin:
if (sensitivekey in k) and v:
data[k] = "********"
type, meta = log_entry_types.get(action_type=action)
if not type:
raise TypeError("Undefined log entry type '%s'" % action)
logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True)
type.validate_data(json.loads(logentry.data))
elif data:
raise TypeError("You should only supply dictionaries as log data.")
if save:

View File

@@ -847,7 +847,7 @@ class Event(EventMixin, LoggedModel):
from ..signals import event_copy_data
from . import (
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
ItemVariationMetaValue, Question, Quota,
)
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
@@ -990,11 +990,6 @@ class Event(EventMixin, LoggedModel):
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save(force_insert=True)
for ipt in ItemProgramTime.objects.filter(item__event=other).prefetch_related('item'):
ipt.pk = None
ipt.item = item_map[ipt.item.pk]
ipt.save(force_insert=True)
quota_map = {}
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
quota_map[q.pk] = q

View File

@@ -2294,27 +2294,3 @@ class ItemVariationMetaValue(LoggedModel):
class Meta:
unique_together = ('variation', 'property')
class ItemProgramTime(models.Model):
"""
This model can be used to add a program time to an item.
:param item: The item the program time applies to
:type item: Item
:param start: The date and time this program time starts
:type start: datetime
:param end: The date and time this program time ends
:type end: datetime
"""
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
start = models.DateTimeField(verbose_name=_("Start"))
end = models.DateTimeField(verbose_name=_("End"))
def clean(self):
self.clean_start_end(start=self.start, end=self.end)
super().clean()
def clean_start_end(self, start: datetime = None, end: datetime = None):
if start and end and start > end:
raise ValidationError(_("The program end must not be before the program start."))

View File

@@ -280,13 +280,13 @@ class Seat(models.Model):
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
sales_channel='web',
ignore_distancing=False, distance_ignore_cart_id=None, always_allow_blocked=False):
ignore_distancing=False, distance_ignore_cart_id=None):
from .orders import Order
from .organizer import SalesChannel
if isinstance(sales_channel, SalesChannel):
sales_channel = sales_channel.identifier
if not always_allow_blocked and self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
return False
opqs = self.orderposition_set.filter(
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID],

View File

@@ -84,7 +84,6 @@ 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
from pretix.base.templatetags.phone_format import phone_format
from pretix.helpers.daterange import datetimerange
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
@@ -491,12 +490,6 @@ DEFAULT_VARIABLES = OrderedDict((
"TIME_FORMAT"
) if op.valid_until else ""
}),
("program_times", {
"label": _("Program times: date and time"),
"editor_sample": _(
"2017-05-31 10:00 12:00\n2017-05-31 14:00 16:00\n2017-05-31 14:00 2017-06-01 14:00"),
"evaluate": lambda op, order, ev: get_program_times(op, ev)
}),
("medium_identifier", {
"label": _("Reusable Medium ID"),
"editor_sample": "ABC1234DEF4567",
@@ -741,16 +734,6 @@ def get_seat(op: OrderPosition):
return None
def get_program_times(op: OrderPosition, ev: Event):
return '\n'.join([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
) for pt in op.item.program_times.all()
])
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = [p for p in (

View File

@@ -1670,14 +1670,13 @@ class OrderChangeManager:
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
self.order = order
self.user = user
self.auth = auth
self.event = order.event
self.split_order = None
self.reissue_invoice = reissue_invoice
self.allow_blocked_seats = allow_blocked_seats
self._committed = False
self._totaldiff_guesstimate = 0
self._quotadiff = Counter()
@@ -2198,7 +2197,7 @@ class OrderChangeManager:
for seat, diff in self._seatdiff.items():
if diff <= 0:
continue
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True, always_allow_blocked=self.allow_blocked_seats) or diff > 1:
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True) or diff > 1:
raise OrderError(self.error_messages['seat_unavailable'].format(seat=seat.name))
if self.event.has_subevents:

View File

@@ -56,8 +56,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
Item, ItemCategory, ItemProgramTime, ItemVariation, Question,
QuestionOption, Quota,
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
)
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
@@ -573,8 +572,6 @@ class ItemCreateForm(I18nModelForm):
for b in self.cleaned_data['copy_from'].bundles.all():
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
count=b.count, designated_price=b.designated_price)
for pt in self.cleaned_data['copy_from'].program_times.all():
instance.program_times.create(start=pt.start, end=pt.end)
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
@@ -1324,49 +1321,3 @@ class ItemMetaValueForm(forms.ModelForm):
widgets = {
'value': forms.TextInput()
}
class ItemProgramTimeFormSet(I18nFormSet):
template = "pretixcontrol/item/include_program_times.html"
title = _('Program times')
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
self.is_valid()
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
locales=self.locales,
event=self.event
)
self.add_fields(form, None)
return form
class ItemProgramTimeForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
class Meta:
model = ItemProgramTime
localized_fields = '__all__'
fields = [
'start',
'end',
]
field_classes = {
'start': forms.SplitDateTimeField,
'end': forms.SplitDateTimeField,
}
widgets = {
'start': SplitDateTimePickerWidget(),
'end': SplitDateTimePickerWidget(),
}

View File

@@ -69,7 +69,6 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_verified',
'is_staff',
'needs_password_change',
'last_login'

View File

@@ -503,7 +503,6 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'pretix.event.order.valid_if_pending.set': _('The order has been set to be usable before it is paid.'),
'pretix.event.order.valid_if_pending.unset': _('The order has been set to require payment before use.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.reactivated': _('The order has been reactivated.'),
@@ -535,7 +534,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
'toggled.'),
'pretix.event.order.checkin_text': _('The order\'s check-in text has been changed.'),
'pretix.event.order.pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
'pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
'unpaid has been toggled.'),
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
@@ -575,6 +574,21 @@ class CoreOrderLogEntryType(OrderLogEntryType):
pass
@log_entry_types.new()
class OrderPaidLogEntryType(CoreOrderLogEntryType):
action_type = 'pretix.event.order.paid'
plain = _('The order has been marked as paid.')
data_schema = {
"type": "object",
"properties": {
"provider": {"type": ["null", "string"], "shred": False, },
"info": {"type": ["null", "string", "object"], "shred": True, },
"date": {"type": ["null", "string"], "shred": False, },
"force": {"type": "boolean", "shred": False, },
},
}
@log_entry_types.new_from_dict({
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
@@ -585,13 +599,63 @@ class CoreOrderLogEntryType(OrderLogEntryType):
'pretix.voucher.added.waitinglist': _('The voucher has been assigned to {email} through the waiting list.'),
})
class CoreVoucherLogEntryType(VoucherLogEntryType):
pass
data_schema = {
"type": "object",
"properties": {
"item": {"type": ["null", "number"], "shred": False, },
"variation": {"type": ["null", "number"], "shred": False, },
"tag": {"type": "string", "shred": False,},
"block_quota": {"type": "boolean", "shred": False, },
"valid_until": {"type": ["null", "string"], "shred": False, },
"min_usages": {"type": "number", "shred": False, },
"max_usages": {"type": "number", "shred": False, },
"subevent": {"type": ["null", "number", "object"], "shred": False, },
"source": {"type": "string", "shred": False,},
"allow_ignore_quota": {"type": "boolean", "shred": False, },
"code": {"type": "string", "shred": False,},
"comment": {"type": "string", "shred": True,},
"price_mode": {"type": "string", "shred": False,},
"seat": {"type": "string", "shred": False,},
"quota": {"type": ["null", "number"], "shred": False,},
"value": {"type": ["null", "string"], "shred": False,},
"redeemed": {"type": "number", "shred": False,},
"all_addons_included": {"type": "boolean", "shred": False, },
"all_bundles_included": {"type": "boolean", "shred": False, },
"budget": {"type": ["null", "number"], "shred": False, },
"itemvar": {"type": "string", "shred": False,},
"show_hidden_items": {"type": "boolean", "shred": False, },
# bulk create:
"bulk": {"type": "boolean", "shred": False,},
"seats": {"type": "array", "shred": False,},
"send": {"type": ["string", "boolean"], "shred": False,},
"send_recipients": {"type": "array", "shred": True,},
"send_subject": {"type": "string", "shred": False,},
"send_message": {"type": "string", "shred": True,},
# pretix.voucher.sent
"recipient": {"type": "string", "shred": True,},
"name": {"type": "string", "shred": True,},
"subject": {"type": "string", "shred": False,},
"message": {"type": "string", "shred": True,},
# pretix.voucher.added.waitinglist
"email": {"type": "string", "shred": True,},
"waitinglistentry": {"type": "number", "shred": False, },
},
}
@log_entry_types.new()
class VoucherRedeemedLogEntryType(VoucherLogEntryType):
action_type = 'pretix.voucher.redeemed'
plain = _('The voucher has been redeemed in order {order_code}.')
data_schema = {
"type": "object",
"properties": {
"order_code": {"type": "string", "shred": False, },
},
}
def display(self, logentry, data):
url = reverse('control:event.order', kwargs={
@@ -634,9 +698,16 @@ class TeamMembershipLogEntryType(LogEntryType):
'pretix.team.member.removed': _('{user} has been removed from the team.'),
'pretix.team.invite.created': _('{user} has been invited to the team.'),
'pretix.team.invite.resent': _('Invite for {user} has been resent.'),
'pretix.team.invite.deleted': _('Invite for {user} has been deleted.'),
})
class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
pass
data_schema = {
"type": "object",
"properties": {
"email": {"type": "string", "shred": True, },
"user": {"type": "number", "shred": False, },
},
}
@log_entry_types.new()
@@ -667,14 +738,6 @@ class UserSettingsChangedLogEntryType(LogEntryType):
return text
@log_entry_types.new_from_dict({
'pretix.user.email.changed': _('Your email address has been changed from {old_email} to {email}.'),
'pretix.user.email.confirmed': _('Your email address {email} has been confirmed.'),
})
class UserEmailChangedLogEntryType(LogEntryType):
pass
class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry, data):
return self.plain.format(data['other_email'])
@@ -841,6 +904,10 @@ class OrganizerPluginStateLogEntryType(LogEntryType):
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
'pretix.event.seats.blocks.changed': _('A seat in the seating plan has been blocked or unblocked.'),
'pretix.seatingplan.added': _('A seating plan has been added.'),
'pretix.seatingplan.changed': _('A seating plan has been changed.'),
'pretix.seatingplan.deleted': _('A seating plan has been deleted.'),
})
class CoreEventLogEntryType(EventLogEntryType):
pass
@@ -890,9 +957,6 @@ class EventPluginStateLogEntryType(EventLogEntryType):
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
'pretix.event.item.program_times.added': _('A program time has been added to this product.'),
'pretix.event.item.program_times.changed': _('A program time has been changed on this product.'),
'pretix.event.item.program_times.removed': _('A program time has been removed from this product.'),
})
class CoreItemLogEntryType(ItemLogEntryType):
pass

View File

@@ -72,7 +72,7 @@ class PermissionMiddleware:
)
EXCEPTIONS_FORCED_PW_CHANGE = (
"user.settings.password.change",
"user.settings",
"auth.logout"
)
@@ -139,7 +139,7 @@ class PermissionMiddleware:
return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
except SessionPasswordChangeRequired:
if url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
return redirect_to_url(reverse('control:user.settings.password.change') + '?next=' + quote(request.get_full_path()))
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
except Session2FASetupRequired:
if url_name not in self.EXCEPTIONS_2FA:
return redirect_to_url(reverse('control:user.settings.2fa'))

View File

@@ -7,7 +7,6 @@
<h3>{% trans "Set new password" %}</h3>
{% csrf_token %}
{% bootstrap_form_errors form type='all' layout='inline' %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
{% bootstrap_field form.password_repeat %}
<div class="form-group buttons">

View File

@@ -1,13 +0,0 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
{{ reason }}
{{ code }}
Please do never give this code to another person. Our support team will never ask for this code.
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
{% endblocktrans %}

View File

@@ -1,70 +0,0 @@
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
<p>
{% blocktrans trimmed %}
With program times, you can set specific dates and times for this product.
This is useful if this product represents access to parts of your event that happen at different times than your event in general.
This will not affect access control, but will affect calendar invites and ticket output.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Program time" %}</h3>
</div>
<div class="col-sm-4 text-right flip">
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.start layout="control" %}
{% bootstrap_field form.end layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Program time" %}</h3>
</div>
<div class="col-sm-4 text-right flip">
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.start layout="control" %}
{% bootstrap_field formset.empty_form.end layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a program time" %}</button>
</p>
</div>

View File

@@ -1,29 +0,0 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Change login email address" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Change login email address" %}
</h1>
{% csrf_token %}
{% bootstrap_form_errors form %}
<p class="text-muted">
{% trans "This changes the email address used to login to your account, as well as where we send email notifications." %}
</p>
{% bootstrap_field form.old_email %}
{% bootstrap_field form.new_email %}
<p>
{% trans "We will send a confirmation code to your new email address, which you need to enter in the next step to confirm the email address is correct." %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,24 +0,0 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Change password" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Change password" %}
</h1>
<br>
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.email %}
{% bootstrap_field form.old_pw %}
{% bootstrap_field form.new_pw %}
{% bootstrap_field form.new_pw_repeat %}
<div class="form-group submit-group">
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-cancel">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Change password" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,21 +0,0 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Enter confirmation code" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Enter confirmation code" %}
</h1>
{% csrf_token %}
{% bootstrap_form_errors form type='all' layout='inline' %}
<p>{{ message }}</p>
{% bootstrap_field form.code %}
<div class="form-group submit-group">
<a href="{{ cancel_url }}" class="btn btn-default btn-cancel">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -3,26 +3,8 @@
{% load bootstrap3 %}
{% block title %}{% trans "Account settings" %}{% endblock %}
{% block content %}
{% if not user.is_verified %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
Your email address is not confirmed yet. To secure your account, please confirm your email address using
a confirmation code we will send to your email address.
{% endblocktrans %}
</p>
<p>
<form action="{% url "control:user.settings.email.send_verification_code" %}" method="post" class="form-horizontal">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
{% trans "Send confirmation email" %}
</button>
</form>
</p>
</div>
{% endif %}
<h1>{% trans "Account settings" %}</h1>
<form action="" method="post" class="form-horizontal" data-testid="usersettingsform">
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
@@ -31,7 +13,7 @@
{% bootstrap_field form.locale layout='horizontal' %}
{% bootstrap_field form.timezone layout='horizontal' %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Notifications" %}</label>
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Notifications" %}</label>
<div class="col-md-9 static-form-row">
{% if request.user.notifications_send and request.user.notification_settings.exists %}
<span class="label label-success">
@@ -59,18 +41,8 @@
{% bootstrap_field form.new_pw layout='horizontal' %}
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
{% endif %}
{% if user.auth_backend == 'native' %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Password" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.password.change" %}">
<span class="fa fa-edit"></span> {% trans "Change password" %}
</a>
</div>
</div>
{% endif %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Two-factor authentication" %}</label>
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
<div class="col-md-9 static-form-row">
{% if user.require_2fa %}
<span class="label label-success">{% trans "Enabled" %}</span> &nbsp;
@@ -86,7 +58,7 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Authorized applications" %}</label>
<label class="col-md-3 control-label" for="">{% trans "Authorized applications" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.oauth.list" %}">
<span class="fa fa-plug"></span>
@@ -95,7 +67,7 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Account history" %}</label>
<label class="col-md-3 control-label" for="">{% trans "Account history" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.history" %}">
<span class="fa fa-history"></span>

View File

@@ -56,7 +56,6 @@
{% if form.new_pw %}
{% bootstrap_field form.new_pw layout='control' %}
{% bootstrap_field form.new_pw_repeat layout='control' %}
{% bootstrap_field form.is_verified layout='control' %}
{% endif %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}

View File

@@ -110,10 +110,6 @@ urlpatterns = [
name='user.settings.2fa.confirm.webauthn'),
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
name='user.settings.2fa.delete'),
re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
re_path(r'^settings/email/change$', user.UserEmailChangeView.as_view(), name='user.settings.email.change'),
re_path(r'^settings/email/verify', user.UserEmailVerifyView.as_view(), name='user.settings.email.send_verification_code'),
re_path(r'^settings/password/change$', user.UserPasswordChangeView.as_view(), name='user.settings.password.change'),
re_path(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
re_path(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
re_path(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),

View File

@@ -60,14 +60,13 @@ from django.views.generic.detail import DetailView, SingleObjectMixin
from django_countries.fields import Country
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
ItemVariationSerializer,
ItemAddOnSerializer, ItemBundleSerializer, ItemVariationSerializer,
)
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
SeatCategoryMapping, Voucher,
CartPosition, Item, ItemCategory, ItemVariation, Order, OrderPosition,
Question, QuestionAnswer, QuestionOption, Quota, SeatCategoryMapping,
Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
@@ -76,9 +75,9 @@ from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemUpdateForm,
ItemVariationForm, ItemVariationsFormSet, QuestionForm, QuestionOptionForm,
QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -1432,8 +1431,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
form.instance.position = i
setattr(form.instance, attr, self.get_object())
created = not form.instance.pk
if form.has_changed():
form.save()
form.save()
if form.has_changed() and any(a for a in form.changed_data if a != 'ORDER'):
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
if key == 'variations':
@@ -1499,16 +1497,6 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
'bundles', 'bundles', 'base_item', order=False,
serializer=ItemBundleSerializer
)
elif k == 'program_times':
self.save_formset(
'program_times', 'program_times', order=False,
serializer=ItemProgramTimeSerializer
)
if not change_data:
for f in v.forms:
if (f in v.deleted_forms and f.instance.pk) or f.has_changed():
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
break
else:
v.save()
@@ -1571,20 +1559,9 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
queryset=ItemBundle.objects.filter(base_item=self.get_object()),
event=self.request.event, item=self.item, prefix="bundles"
)),
('program_times', inlineformset_factory(
Item, ItemProgramTime,
form=ItemProgramTimeForm, formset=ItemProgramTimeFormSet,
can_order=False, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
queryset=ItemProgramTime.objects.filter(item=self.get_object()),
event=self.request.event, prefix="program_times"
)),
])
if not self.object.has_variations:
del f['variations']
if self.item.event.has_subevents:
del f['program_times']
i = 0
for rec, resp in item_formsets.send(sender=self.request.event, item=self.item, request=self.request):

View File

@@ -2146,8 +2146,7 @@ class OrderChange(OrderView):
self.order,
user=self.request.user,
notify=notify,
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True,
allow_blocked_seats=True,
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
)
form_valid = (self._process_add_fees(ocm) and
self._process_add_positions(ocm) and

View File

@@ -44,9 +44,7 @@ from pypdf.errors import PdfReadError
from reportlab.lib.units import mm
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, InvoiceAddress, ItemProgramTime, OrderPosition,
)
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
from pretix.base.pdf import get_images, get_variables
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -97,9 +95,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
description=_("Sample product description"))
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
ItemProgramTime.objects.create(start=now(), end=now(), item=item)
ItemProgramTime.objects.create(start=now(), end=now(), item=item2)
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',

View File

@@ -820,13 +820,12 @@ def organizer_select2(request):
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
display_slug = 'display_slug' in request.GET
doc = {
"results": [
{
'id': o.pk,
'text': '{}{}'.format(o.slug, o.name) if display_slug else str(o.name)
'text': str(o.name)
} for o in qs[offset:offset + pagesize]
],
"pagination": {

View File

@@ -44,13 +44,11 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -62,11 +60,8 @@ from django_scopes import scopes_disabled
from webauthn.helpers import generate_challenge, generate_user_handle
from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import ConfirmationCodeForm, ReauthForm
from pretix.base.forms.user import (
User2FADeviceAddForm, UserEmailChangeForm, UserPasswordChangeForm,
UserSettingsForm,
)
from pretix.base.forms.auth import ReauthForm
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.models import (
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
)
@@ -242,7 +237,25 @@ class UserSettings(UpdateView):
data = {}
for k in form.changed_data:
data[k] = form.cleaned_data[k]
if k not in ('old_pw', 'new_pw_repeat'):
if 'new_pw' == k:
data['new_pw'] = True
else:
data[k] = form.cleaned_data[k]
msgs = []
if 'new_pw' in form.changed_data:
self.request.user.needs_password_change = False
msgs.append(_('Your password has been changed.'))
if 'email' in form.changed_data:
msgs.append(_('Your email address has been changed to {email}.').format(email=form.cleaned_data['email']))
if msgs:
self.request.user.send_security_notice(msgs, email=form.cleaned_data['email'])
if self._old_email != form.cleaned_data['email']:
self.request.user.send_security_notice(msgs, email=self._old_email)
sup = super().form_valid(form)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
@@ -821,159 +834,3 @@ class EditStaffSession(StaffMemberRequiredMixin, UpdateView):
return get_object_or_404(StaffSession, pk=self.kwargs['id'])
else:
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)
class UserPasswordChangeView(FormView):
max_time = 300
form_class = UserPasswordChangeForm
template_name = 'pretixcontrol/user/change_password.html'
def get_form_kwargs(self):
if self.request.user.auth_backend != 'native':
raise PermissionDenied
return {
**super().get_form_kwargs(),
"user": self.request.user,
}
def form_valid(self, form):
with transaction.atomic():
self.request.user.set_password(form.cleaned_data['new_pw'])
self.request.user.needs_password_change = False
self.request.user.save()
msgs = []
msgs.append(_('Your password has been changed.'))
self.request.user.send_security_notice(msgs)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
update_session_auth_hash(self.request, self.request.user)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
def get_success_url(self):
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
return self.request.GET.get("next")
return reverse('control:user.settings')
class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
max_time = 300
form_class = UserEmailChangeForm
template_name = 'pretixcontrol/user/change_email.html'
def get_form_kwargs(self):
if self.request.user.auth_backend != 'native':
raise PermissionDenied
return {
**super().get_form_kwargs(),
"user": self.request.user,
}
def get_initial(self):
return {
"old_email": self.request.user.email
}
def form_valid(self, form):
self.request.user.send_confirmation_code(
session=self.request.session,
reason='email_change',
email=form.cleaned_data['new_email'],
state=form.cleaned_data['new_email'],
)
self.request.session['email_confirmation_destination'] = form.cleaned_data['new_email']
return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_change')
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class UserEmailVerifyView(View):
def post(self, request, *args, **kwargs):
if self.request.user.is_verified:
messages.success(self.request, _('Your email address was already verified.'))
return redirect(reverse('control:user.settings', kwargs={}))
self.request.user.send_confirmation_code(
session=self.request.session,
reason='email_verify',
email=self.request.user.email,
state=self.request.user.email,
)
self.request.session['email_confirmation_destination'] = self.request.user.email
return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_verify')
class UserEmailConfirmView(FormView):
form_class = ConfirmationCodeForm
template_name = 'pretixcontrol/user/confirmation_code_dialog.html'
def get_context_data(self, **kwargs):
return {
**super().get_context_data(**kwargs),
"cancel_url": reverse('control:user.settings', kwargs={}),
"message": format_html(
_("Please enter the confirmation code we sent to your email address <strong>{email}</strong>."),
email=self.request.session.get('email_confirmation_destination', ''),
),
}
@transaction.atomic()
def form_valid(self, form):
reason = self.request.GET['reason']
if reason not in ('email_change', 'email_verify'):
raise PermissionDenied
try:
new_email = self.request.user.check_confirmation_code(
session=self.request.session,
reason=reason,
code=form.cleaned_data['code'],
)
except PermissionDenied:
return self.form_invalid(form)
except BadRequest:
messages.error(self.request, _(
'We were unable to verify your confirmation code. Please try again.'
))
return redirect(reverse('control:user.settings', kwargs={}))
log_data = {
'email': new_email,
'email_verified': True,
}
if reason == 'email_change':
msgs = []
msgs.append(_('Your email address has been changed to {email}.').format(email=new_email))
log_data['old_email'] = old_email = self.request.user.email
self.request.user.send_security_notice(msgs, email=old_email)
self.request.user.send_security_notice(msgs, email=new_email)
log_action = 'pretix.user.email.changed'
else:
log_action = 'pretix.user.email.confirmed'
self.request.user.email = new_email
self.request.user.is_verified = True
self.request.user.save()
self.request.user.log_action(log_action, user=self.request.user, data=log_data)
update_session_auth_hash(self.request, self.request.user)
if reason == 'email_change':
messages.success(self.request, _('Your email address has been changed successfully.'))
else:
messages.success(self.request, _('Your email address has been confirmed successfully.'))
return redirect(reverse('control:user.settings', kwargs={}))
def form_invalid(self, form):
messages.error(self.request, _('The entered confirmation code is not correct. Please try again.'))
return super().form_invalid(form)

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-11-10 01:00+0000\n"
"PO-Revision-Date: 2025-11-04 10:25+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"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.14.2\n"
"X-Generator: Weblate 5.14\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -1096,7 +1096,7 @@ msgstr "Código de pedido y número de posición"
#: pretix/base/datasync/sourcefields.py:482
msgid "Ticket price"
msgstr "Precio de la entrada"
msgstr "Precio del billete"
#: pretix/base/datasync/sourcefields.py:491 pretix/base/notifications.py:204
#: pretix/control/forms/filter.py:216 pretix/control/forms/modelimport.py:90
@@ -1105,7 +1105,7 @@ msgstr "Estado del pedido"
#: pretix/base/datasync/sourcefields.py:500
msgid "Ticket status"
msgstr "Estado de la entrada"
msgstr "Estado del billete"
#: pretix/base/datasync/sourcefields.py:509
msgid "Order date and time"
@@ -1134,7 +1134,7 @@ msgstr "Enlace para realizar el pedido"
#: pretix/base/datasync/sourcefields.py:560
msgid "Ticket link"
msgstr "Enlace a la entrada"
msgstr "Enlace al billete"
#: pretix/base/datasync/sourcefields.py:578
#, python-brace-format
@@ -2427,7 +2427,7 @@ msgstr "Pagado con {method}"
#: pretix/base/exporters/orderlist.py:458
#: pretix/base/exporters/orderlist.py:914
msgid "Fee type"
msgstr "Tarifa por entrada"
msgstr "Tarifa por billete"
#: pretix/base/exporters/orderlist.py:460
#: pretix/base/exporters/orderlist.py:601
@@ -2542,7 +2542,7 @@ msgstr "ID Seudónimo"
#: pretix/base/exporters/orderlist.py:621 pretix/control/forms/filter.py:709
#: pretix/control/templates/pretixcontrol/order/change.html:280
msgid "Ticket secret"
msgstr "Secreto de la entrada"
msgstr "Secreto del billete"
#: pretix/base/exporters/orderlist.py:622 pretix/base/modelimport_orders.py:610
#: pretix/base/modelimport_vouchers.py:272
@@ -4431,7 +4431,7 @@ msgstr "Permitir volver a entrar después de un escaneo de salida"
#: pretix/base/models/checkin.py:94
msgid "Allow multiple entries per ticket"
msgstr "Permitir varios accesos por entrada"
msgstr "Permitir varias entradas por billete"
#: pretix/base/models/checkin.py:95
msgid ""
@@ -5151,7 +5151,7 @@ msgstr "Ningún valor debe contener el carácter delimitador."
#: pretix/base/models/giftcards.py:81
#: pretix/control/templates/pretixcontrol/organizers/giftcard.html:50
msgid "Owned by ticket holder"
msgstr "Propietario del titular de la entrada"
msgstr "Propietario del titular del billete"
#: pretix/base/models/giftcards.py:88
msgid "Owned by customer account"
@@ -5647,8 +5647,8 @@ msgstr ""
"normalmente NO es necesario cambiar este valor. El ajuste predeterminado "
"significa que el tiempo de validez de las entradas no lo decidirá el "
"producto, sino el evento y la configuración del check-in. Utilice las otras "
"opciones sólo si las necesita para realizar, p. una reserva de una entrada "
"de un año con fecha de inicio dinámica. Tenga en cuenta que la validez se "
"opciones sólo si las necesita para realizar, p. una reserva de un billete de "
"un año con fecha de inicio dinámica. Tenga en cuenta que la validez se "
"almacenará con la entrada, por lo que si cambia la configuración aquí más "
"adelante, las entradas existentes no se verán afectadas por el cambio, pero "
"mantendrán su validez actual."
@@ -8831,7 +8831,7 @@ msgstr "La posición de este pedido ha sido cancelado."
#: pretix/base/services/checkin.py:981
msgid "This ticket has been blocked."
msgstr "Esta entrada ha sido bloqueada."
msgstr "Este billete ha sido bloqueado."
#: pretix/base/services/checkin.py:990
msgid "This order is not yet approved."
@@ -8840,12 +8840,12 @@ msgstr "Este pedido aún no está aprobado."
#: pretix/base/services/checkin.py:999 pretix/base/services/checkin.py:1003
#, python-brace-format
msgid "This ticket is only valid after {datetime}."
msgstr "Esta entrada sólo es válida después del {datetime}."
msgstr "Este billete sólo es válido después del {datetime}."
#: pretix/base/services/checkin.py:1013 pretix/base/services/checkin.py:1017
#, python-brace-format
msgid "This ticket was only valid before {datetime}."
msgstr "Esta entrada sólo era válida antes del {datetime}."
msgstr "Este billete sólo era válido antes del {datetime}."
#: pretix/base/services/checkin.py:1048
msgid "This order position has an invalid product for this check-in list."
@@ -9082,7 +9082,7 @@ msgid ""
"a ticket that starts to be valid on {date}."
msgstr ""
"Ha seleccionado una suscripción valida de {start} a {end}, pero ha "
"seleccionado una entrada que empieza a ser valido el {date}."
"seleccionado un billete que empieza a ser valido el {date}."
#: pretix/base/services/memberships.py:188
#, python-brace-format
@@ -9843,7 +9843,7 @@ msgstr ""
#: pretix/base/settings.py:414
msgid "Ask for company per ticket"
msgstr "Preguntar por compañía por entrada"
msgstr "Preguntar por compañía por billete"
#: pretix/base/settings.py:423
msgid "Require company per ticket"
@@ -10135,7 +10135,7 @@ msgstr "Fuente"
#: pretix/base/settings.py:796
msgid "Length of ticket codes"
msgstr "Longitud de los códigos de las entradas"
msgstr "Longitud de los códigos de los billetes"
#: pretix/base/settings.py:823
msgid "Reservation period"
@@ -10421,7 +10421,7 @@ msgid ""
"Automatic based on ticket-specific validity, membership validity, event "
"series date, or event date"
msgstr ""
"Automático, según la validez específica de la entrada, la validez de la "
"Automático, según la validez específica del billete, la validez de la "
"membresía, la fecha de la serie de eventos o la fecha del evento"
#: pretix/base/settings.py:1135 pretix/base/settings.py:1146
@@ -10865,7 +10865,7 @@ msgstr ""
#: pretix/base/settings.py:1660
msgid "Generate tickets for all products"
msgstr "Generar entradas para todos los productos"
msgstr "Generar billetes para todos los productos"
#: pretix/base/settings.py:1661
msgid ""
@@ -10873,9 +10873,9 @@ msgid ""
"\"admission ticket\"in the product settings. You can also turn off ticket "
"issuing in every product separately."
msgstr ""
"Si está desactivado, las entrada solo se emiten para productos marcados como "
"Si está desactivado, los billete solo se emiten para productos marcados como "
"\"entradas\" en la configuración del producto. También puedes desactivar la "
"emisión de entradas en cada producto por separado."
"emisión de billetes en cada producto por separado."
#: pretix/base/settings.py:1673
msgid "Generate tickets for pending orders"
@@ -11095,7 +11095,7 @@ msgstr "No permitir cambios después"
#: pretix/base/settings.py:1884
msgid "Allow change even though the ticket has already been checked in"
msgstr "Permitir cambio aunque la entrada ya haya sido facturada"
msgstr "Permitir cambio aunque el billete ya haya sido facturado"
#: pretix/base/settings.py:1885
msgid ""
@@ -14685,7 +14685,7 @@ msgstr "Suma máxima de pagos y devoluciones"
#: pretix/control/forms/filter.py:624
msgid "At least one ticket with check-in"
msgstr "Al menos una entrada con check-in"
msgstr "Al menos un billete con check-in"
#: pretix/control/forms/filter.py:628
msgid "Affected quota"
@@ -15399,7 +15399,7 @@ msgstr "Tamaño"
#: pretix/control/forms/item.py:455
msgid "Number of tickets"
msgstr "Número de entradas"
msgstr "Número de billetes"
#: pretix/control/forms/item.py:587
msgid "Quota name is required."
@@ -15907,7 +15907,7 @@ msgstr "Precio nuevo (bruto)"
#: pretix/control/forms/orders.py:484
msgid "Ticket is blocked"
msgstr "La entrada está bloqueada"
msgstr "El billete está bloqueado"
#: pretix/control/forms/orders.py:489
msgid "Validity start"
@@ -15926,8 +15926,8 @@ msgid ""
"This affects both the ticket secret (often used as a QR code) as well as the "
"link used to individually access the ticket."
msgstr ""
"Esto afecta tanto al secreto de la entrada(a menudo utilizado como código QR)"
" así como al enlace utilizado para acceder individualmente a la entrada."
"Esto afecta tanto al secreto del billete (a menudo utilizado como código QR) "
"así como al enlace utilizado para acceder individualmente al billete."
#: pretix/control/forms/orders.py:512
msgid "Cancel this position"
@@ -19127,7 +19127,7 @@ msgstr "Su búsqueda no ha encontrado ningún registro de check-in."
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:52
msgid "You haven't scanned any tickets yet."
msgstr "Aún no ha escaneado ninguna entrada."
msgstr "Aún no ha escaneado ningún billete."
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:63
msgid "Time of scan"
@@ -22073,9 +22073,9 @@ msgid ""
msgstr ""
"Esta opción debe configurarse para la mayoría de las cosas que llamaría "
"\"una entrada\". Para productos complementarios o paquetes, esto debe "
"configurarse en la entrada principal, excepto si los productos "
"complementarios o productos combinados representan personas adicionales "
"(por ejemplo, paquetes grupales)."
"configurarse en el billete principal, excepto si los productos "
"complementarios o productos combinados representan personas adicionales (por "
"ejemplo, paquetes grupales)."
#: pretix/control/templates/pretixcontrol/item/create.html:50
#: pretix/control/templates/pretixcontrol/item/index.html:58
@@ -22354,8 +22354,8 @@ msgstr ""
"Si seleccionas una duración expresada en días, meses o años, la validez "
"siempre finalizará al finalizar un día completo (medianoche), más el número "
"de minutos y horas seleccionados anteriormente. La fecha de inicio está "
"incluida en el cálculo, por lo que si introduces \"1 día\", la entrada será "
"válida hasta el final del día de inicio."
"incluida en el cálculo, por lo que si introduces \"1 día\", el billete será "
"válido hasta el final del día de inicio."
#: pretix/control/templates/pretixcontrol/item/index.html:254
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:619
@@ -22727,7 +22727,7 @@ msgstr "%% de respuestas"
#: pretix/control/templates/pretixcontrol/items/question.html:93
#, python-format
msgid "%% of tickets"
msgstr "%% de entradas"
msgstr "%% de billetes"
#: pretix/control/templates/pretixcontrol/items/question.html:112
#: pretix/control/templates/pretixcontrol/order/transactions.html:85
@@ -23339,9 +23339,9 @@ msgid ""
"ticket here will not affect the membership. Memberships can be managed in "
"the customer account."
msgstr ""
"La venta de esta plaza creó una suscripción. Cambiar la validez de la "
"entrada aquí no afectará a la suscripción. Las suscripciones se pueden "
"gestionar en la cuenta de cliente."
"La venta de esta plaza creó una suscripción. Cambiar la validez del billete "
"aquí no afectará a la suscripción. Las suscripcionies se pueden gestionar en "
"la cuenta de cliente."
#: pretix/control/templates/pretixcontrol/order/change.html:296
msgid ""
@@ -25610,8 +25610,8 @@ msgstr ""
msgid ""
"This can be used to enable products like year passes, tickets of ten, etc."
msgstr ""
"Esto se puede utilizar para habilitar productos como abonos al año, bonos de "
"diez, etc."
"Esto se puede utilizar para habilitar productos como abonos de año, billetes "
"de diez, etc."
#: pretix/control/templates/pretixcontrol/organizers/plugin_events.html:6
#: pretix/control/templates/pretixcontrol/organizers/plugin_events.html:12
@@ -30919,7 +30919,7 @@ msgstr "Incluir código QR secreto"
#: pretix/plugins/checkinlists/exporters.py:101
msgid "Only tickets requiring special attention"
msgstr "Sólo las entradas que requieren una atención especial"
msgstr "Sólo los billetes que requieren una atención especial"
#: pretix/plugins/checkinlists/exporters.py:134
msgid "Include questions"
@@ -32147,8 +32147,7 @@ msgstr "El pedido recibió un correo electrónico masivo."
#: pretix/plugins/sendmail/signals.py:129
msgid "A ticket holder of this order received a mass email."
msgstr ""
"El titular de una entrada de este pedido recibió un correo electrónico "
"masivo."
"El titular de un billete de este pedido recibió un correo electrónico masivo."
#: pretix/plugins/sendmail/signals.py:134
msgid "The person on the waiting list received a mass email."
@@ -32371,8 +32370,8 @@ msgid ""
"Send an email to every customer, or to every person a ticket has been "
"purchased for, or a combination of both."
msgstr ""
"Enviar un correo electrónico a cada cliente, o a cada persona para las "
"cuales haya comprado una entrada, o una combinación de ambos."
"Envíe un correo electrónico a cada cliente, o a cada persona para la que se "
"haya comprado un billete, o una combinación de ambos."
#: pretix/plugins/sendmail/views.py:417
#, python-format
@@ -32657,7 +32656,7 @@ msgid ""
"Stripe Dashboard."
msgstr ""
"pretix intentará verificar si el navegador web del cliente admite métodos de "
"pago que emplea carteras como Apple Pay o Google Pay y los mostrará de "
"pago basados en billetera como Apple Pay o Google Pay y los mostrará de "
"manera destacada junto con el método de pago con tarjeta de crédito. Esta "
"detección no tiene en cuenta si Google Pay/Apple Pay se ha desactivado en el "
"Panel de Stripe."
@@ -33508,7 +33507,7 @@ msgstr "Entrada alternativa"
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html:8
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html:15
msgid "Ticket layout"
msgstr "Diseño de entradas"
msgstr "Diseño de billetes"
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/delete.html:9
#, python-format
@@ -36750,8 +36749,8 @@ msgid ""
"No ticket types are available for the waiting list, have a look at the "
"ticket shop instead."
msgstr ""
"No hay entradas disponibles en la lista de espera, revise la taquilla "
"virtual podría encontrar entradas disponibles."
"No hay billetes disponibles en la lista de espera, revise la taquilla "
"virtual podría encontrar billetes disponibles."
#: pretix/presale/views/waiting.py:137 pretix/presale/views/waiting.py:161
msgid "Waiting lists are disabled for this event."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-11-08 13:00+0000\n"
"PO-Revision-Date: 2025-11-04 10:25+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"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.14.2\n"
"X-Generator: Weblate 5.14\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -471,7 +471,7 @@ msgstr "注文情報が変更されました"
#: pretix/api/webhooks.py:294 pretix/base/notifications.py:275
msgid "Order contact address changed"
msgstr "注文の連絡先アドレスが変更されました"
msgstr "注文のEメールアドレスが変更されました"
#: pretix/api/webhooks.py:298 pretix/base/notifications.py:281
#: pretix/control/templates/pretixcontrol/event/mail.html:102
@@ -3527,7 +3527,7 @@ msgstr "納税者番号"
#: pretix/base/invoicing/national.py:53
msgctxt "italian_invoice"
msgid "Address for certified electronic mail"
msgstr "認証済みメールアドレス"
msgstr "認証付き電子メールアドレス"
#: pretix/base/invoicing/national.py:57
msgctxt "italian_invoice"
@@ -4279,7 +4279,7 @@ msgstr "電源を切ると、通知を受け取れません。"
#: pretix/control/templates/pretixcontrol/users/form.html:6
#: pretix/control/views/organizer.py:170 tests/base/test_mail.py:149
msgid "User"
msgstr "ユーザ"
msgstr "ユーザ"
#: pretix/base/models/auth.py:284 pretix/control/navigation.py:411
#: pretix/control/templates/pretixcontrol/users/index.html:5
@@ -4439,7 +4439,7 @@ msgstr "サーバーエラー"
#: pretix/base/models/checkin.py:365
msgid "Ticket blocked"
msgstr "チケットがブロックされました"
msgstr "チケットのチェックインが完了しました"
#: pretix/base/models/checkin.py:366
msgid "Order not approved"
@@ -5019,8 +5019,9 @@ msgstr ""
#: pretix/base/models/event.py:1797
msgid "A property can either be required or have a default value, not both."
msgstr "プロパティは必須または初期値のいずれかを持つことができますが、両方を持つこと"
"はできません。"
msgstr ""
"プロパティは、必須であるかデフォルト値を持つかのどちらかであり、両方ではあり"
"ません。"
#: pretix/base/models/event.py:1877 pretix/base/models/organizer.py:582
msgid "Link text"
@@ -5418,9 +5419,9 @@ msgid ""
"paid and completed. You can use this e.g. for discounted tickets that are "
"only available to specific groups."
msgstr ""
"この製品が注文に含まれる場合、注文は「承認」状態になり、支払いと完了の前にあ"
"なたによる確認が必要になります。えば、特定のグループのみが利用できる割引チ"
"ケットなどに使用できます。"
"この製品が注文の一部である場合、注文は「承認」状態になり、支払いと完了が行わ"
"れる前にあなたによって確認される必要があります。たとえば、特定のグループの"
"み利用可能な割引チケットなどに使用できます。"
#: pretix/base/models/items.py:624
msgid ""
@@ -5479,9 +5480,9 @@ msgid ""
"tickets to indicate to the person at check-in that the student ID card still "
"needs to be checked."
msgstr ""
"これを設定すると、チェックインアプリこのチケット特別な注意必要とするこ"
"とを示す警告表示されます。えば、学生チケットチェックイン担当者に学生証"
"の確認がまだ必要であることを示すために使用できます。"
"これを設定すると、チェックインアプリは、このチケット特別な注意必要である"
"とを示す可視警告表示ます。たとえば、学生チケットの場合、チェックイン"
"に学生証の確認がまだ必要であることを示すために使用できます。"
#: pretix/base/models/items.py:665 pretix/base/models/items.py:1251
msgid ""
@@ -5496,8 +5497,10 @@ msgid ""
"If set, this will be displayed next to the current price to show that the "
"current price is a discounted one. This is just a cosmetic setting and will "
"not actually impact pricing."
msgstr "設定した場合、現在の価格が割引価格であることを示すために、現在の価格の横に表"
"示されます。これは表示上の設定であり、実際の価格には影響しません。"
msgstr ""
"設定された場合、現在の価格の横に表示され、現在の価格が割引価格であることを示"
"します。これは見た目の設定であり、実際に価格に影響を与えるものではありませ"
"ん。"
#: pretix/base/models/items.py:681
msgid "Only sell tickets for this product on the selected sales channels."
@@ -5809,11 +5812,11 @@ msgstr "そのアイテムにはすでにこのカテゴリのアドオンがあ
#: pretix/base/models/items.py:1506
msgid "The minimum count needs to be equal to or greater than zero."
msgstr "最小数は0以上である必要があります。"
msgstr "最小カウントはゼロ以上である必要があります。"
#: pretix/base/models/items.py:1511
msgid "The maximum count needs to be equal to or greater than zero."
msgstr "最大数は0以上である必要があります。"
msgstr "最大カウントはゼロ以上である必要があります。"
#: pretix/base/models/items.py:1516
msgid "The maximum count needs to be greater than the minimum count."
@@ -5858,7 +5861,7 @@ msgstr "選択されたバリエーションはこのアイテムには属して
#: pretix/base/models/items.py:1593
msgid "The count needs to be equal to or greater than zero."
msgstr "数量は0以上である必要があります。"
msgstr "カウントはゼロ以上である必要があります。"
#: pretix/base/models/items.py:1648
msgid "Number"
@@ -5977,7 +5980,7 @@ msgstr "最大長"
#: pretix/base/models/items.py:1758
msgid "Validate file to be a portrait"
msgstr "ファイルが縦向きであることを検証"
msgstr "ファイルがポートレートであることを確認してください"
#: pretix/base/models/items.py:1759
msgid ""
@@ -6130,11 +6133,12 @@ msgid ""
"are ignored if they are set to \"Allow re-entering after an exit scan\" to "
"prevent accidental overbooking."
msgstr ""
"このオプションを使用すると、イベントの退出時にスキャンされた時点で定員枠が解"
"放されます。これは、エントリーと退の両方でスキャンされ、かつ退出のスキャン"
"がより新しい場合にのみ実行されます。どちらのスキャンがどのチェックインリスト"
"で行われたかは関係ありませんが、誤った過剰予約を防ぐため、「退スキャン後の"
"再入場を許可」が設定されているチェックインリストは無視されます。"
"このオプションを使用すると、イベントの退場口で人々がスキャンされるとすぐに"
"クォータが解放されます。これは、入場と退の両方でスキャンされ、退場の方が最"
"新のスキャンである場合にのみ発生します。どちらのスキャンがどのチェックインリ"
"ストで行われたかは関係ありませんが、「退スキャン後の再入場を許可する」に設"
"定されているチェックインリストは、偶発的なオーバーブッキングを防ぐため無視さ"
"れます。"
#: pretix/base/models/items.py:2122 pretix/control/navigation.py:156
#: pretix/control/templates/pretixcontrol/items/quotas.html:4
@@ -6279,10 +6283,10 @@ msgid ""
"custom message, so you need to brief your check-in staff how to handle these "
"cases."
msgstr ""
"これを設定すると、チェックインアプリこの注文のチケット特別な注意必要"
"ることを示す警告表示されます。詳細やカスタムメッセージは表示されないため"
"、これらのケースへの対応方法についてチェックインスタッフに事前説明する必要が"
"あります。"
"これを設定すると、チェックインアプリは、この注文のチケット特別な注意必要"
"であることを示す可視警告表示ます。これには詳細やカスタムメッセージは表示"
"されませんので、チェックインスタッフにこれらのケースの処理方法を簡潔に説明す"
"る必要があります。"
#: pretix/base/models/orders.py:291
msgid ""
@@ -7459,9 +7463,9 @@ msgid ""
"the given value. The order total for this purpose may be computed without "
"taking the fees imposed by this payment method into account."
msgstr ""
"この決済方法は、注文合計金額が指定された金額以下の場合にのみ利用できます。こ"
"の目的での注文合計金額は、この決済方法によって課れる手数料を考慮せずに計算"
"される場合があります。"
"この支払いは、注文合計が指定された値以下である場合にのみ利用可能となりま"
"す。この目的のための注文合計は、この支払い方法によって課せられる手数料を考"
"慮に入れずに計算される場合があります。"
#: pretix/base/payment.py:428 pretix/base/payment.py:437
msgid "Additional fee"
@@ -8900,8 +8904,9 @@ msgstr ""
msgid ""
"You are trying to use a membership of type \"{type}\" more than {number} "
"times, which is the maximum amount."
msgstr "タイプ「{type}」のメンバーシップを最大回数の{number}回を超えて使用しようとし"
"ています。"
msgstr ""
"指定されたタイプ “{type}” のメンバーシップを最大回数である{number}回以上使用"
"しようとしています。"
#: pretix/base/services/memberships.py:227
#, python-brace-format
@@ -9527,7 +9532,9 @@ msgstr "製品リストに税込価格ではなく税抜価格を表示する"
msgid ""
"Independent of your choice, the cart will show gross prices as this is the "
"price that needs to be paid."
msgstr "選択に関わらず、カートには支払う必要がある金額として税込価格が表示されます。"
msgstr ""
"あなたの選択に関わらず、カートには支払う必要がある価格である総価格を表示しま"
"す。"
#: pretix/base/settings.py:346
msgid "Hide prices on attendee ticket page"
@@ -10943,7 +10950,7 @@ msgstr "払い戻し方法"
#: pretix/base/settings.py:2106 pretix/base/settings.py:2119
msgid "Terms of cancellation"
msgstr "キャンセル規定"
msgstr "取り消し"
#: pretix/base/settings.py:2109
msgid ""
@@ -13439,7 +13446,7 @@ msgstr "使用言語"
#: pretix/control/forms/event.py:93
msgid "Choose all languages that your event should be available in."
msgstr "イベント利用可能とするすべての言語を選択してください。"
msgstr "あなたのイベント利用可能であるべき言語をすべて選択してください。"
#: pretix/control/forms/event.py:96
msgid "This is an event series"
@@ -13766,7 +13773,7 @@ msgstr "HTMLメールレンダラー"
#: pretix/control/forms/event.py:1087 pretix/control/forms/event.py:1114
#: pretix/control/forms/event.py:1141 pretix/control/forms/event.py:1291
msgid "Subject sent to order contact address"
msgstr "注文の連絡先アドレスに送信される件名"
msgstr "注文のEメールアドレスが変更されました"
#: pretix/control/forms/event.py:1092 pretix/control/forms/event.py:1119
#: pretix/control/forms/event.py:1146 pretix/control/forms/event.py:1296
@@ -14696,12 +14703,12 @@ msgid ""
"any IP addresses and we will not know who you are or where to find your "
"instance. You can disable this behavior here at any time."
msgstr ""
"アップデート確認時に、pretixは匿名の一意なインストールID、pretixの現在のバー"
"ジョン、インストール済みのプラグイン、およびインストール内のアクティブおよび"
"非アクティブなイベントの数をpretix開発者が運営するサーバーに送信します。匿名"
"データのみを保存し、IPアドレスは一切保存しません。また、あなたが誰であるか、"
"またあなたのインスタンスがどこにあるかを知ることはありません。この動作はい"
"つでもここで無効化できます。"
"アップデートチェック中、pretixは匿名でユニークなインストールID、現在のpretix"
"のバージョン、インストールされているプラグイン、およびインストールされている"
"イベントのアクティブおよび非アクティブな数をpretix開発者が運営するサーバーに"
"報告します。私たちは匿名のデータのみを保存し、IPアドレスは一切保存せず、あな"
"たが誰であるか、またあなたのインスタンスを見つける方法を知ることはありませ"
"ん。いつでもこの動作を無効にすることができます。"
#: pretix/control/forms/global_settings.py:131
msgid "Email notifications"
@@ -15456,7 +15463,7 @@ msgstr "新しい価格(総額)"
#: pretix/control/forms/orders.py:484
msgid "Ticket is blocked"
msgstr "チケットはブロックされています"
msgstr "チケットのチェックインが完了しました"
#: pretix/control/forms/orders.py:489
msgid "Validity start"
@@ -15891,9 +15898,9 @@ msgid ""
"verified to really belong the the user. If this can't be guaranteed, "
"security issues might arise."
msgstr ""
"SSOプロバイダーから受信したすべてのメールアドレスは、実際にユーザーに属してい"
"ることが確認済みであるとなします。これが保証できない場合、セキュリティ上の"
"問題が発生する可能性があります。"
"SSOプロバイダーから受信したすべてのメールアドレスは、実際にそのユーザーのもの"
"であることが確認済みであるとなします。これが保証できない場合、セキュリティ"
"問題が発生する可能性があります。"
#: pretix/control/forms/organizer.py:1080
msgctxt "sso_oidc"
@@ -16039,7 +16046,7 @@ msgstr "選択肢にはさまざまな値が含まれています"
#: pretix/control/forms/subevents.py:288 pretix/control/forms/subevents.py:317
msgid "The end of availability should be after the start of availability."
msgstr "利用可能終了日時は利用可能開始日時より後である必要があります。"
msgstr "利用可能性の終了は利用可能性の開始の後であるべきです。"
#: pretix/control/forms/subevents.py:350
msgid "Available_until"
@@ -16171,8 +16178,8 @@ msgstr ""
#: pretix/control/forms/vouchers.py:382
#, python-brace-format
msgid "CSV input needs to contain a field with the header \"{header}\"."
msgstr "CSV入力には、ヘッダー「{header}」を持つフィールドが含まれている必要があります"
"。"
msgstr ""
"CSVの入力には、ヘッダーが\"{header}\"であるフィールドを含める必要があります。"
#: pretix/control/forms/vouchers.py:385
#, python-brace-format
@@ -16199,8 +16206,9 @@ msgstr "そのコードを使用したバウチャーはすでに存在してい
msgid ""
"The voucher code {code} is too short. Make sure all voucher codes are at "
"least {min_length} characters long."
msgstr "バウチャーコード {code} が短すぎます。すべてのバウチャーコードは少なくとも "
"{min_length} 文字以上にしてください。"
msgstr ""
"バウチャーコード {code} は短すぎます。すべてのバウチャーコードは少なくとも "
"{min_length} 文字以上であることを確認してください。"
#: pretix/control/forms/vouchers.py:432
#, python-brace-format
@@ -16370,8 +16378,8 @@ msgid ""
"Scan of revoked code \"{barcode}…\" at {datetime} for list \"{list}\", type "
"\"{type}\", was uploaded."
msgstr ""
"取り消されたコード「{barcode}…」のスキャンが{datetime}にリスト{list}」、タ"
"イプ「{type}」でアップロードされました。"
"{datetime} にスキャンされた、リスト\"{list}\"、種別 \"{type}\"の取り消し済み"
"のコード \"{barcode}…\"がアップロードされました。"
#: pretix/control/logdisplay.py:318
#, python-brace-format
@@ -16405,29 +16413,34 @@ msgstr ""
msgid ""
"Annulled scan of position #{posid} at {datetime} for list \"{list}\", type "
"\"{type}\"."
msgstr "座席 #{posid} のスキャンが{datetime}にリスト「{list}」、タイプ「{type}」で無"
"効化されました。"
msgstr ""
"リスト\"{list}\"、タイプ\"{type}\"において、{datetime}に実行されたポジション"
"#{posid}のスキャンを取り消しました。"
#: pretix/control/logdisplay.py:326
#, python-brace-format
msgid ""
"Annulled scan of position #{posid} for list \"{list}\", type \"{type}\"."
msgstr "座席 #{posid} のスキャンがリスト「{list}」、タイプ「{type}」で無効化されまし"
"た。"
msgstr ""
"リスト\"{list}\"、タイプ\"{type}\"のポジション#{posid}のスキャンを取り消しま"
"した。"
#: pretix/control/logdisplay.py:329
#, python-brace-format
msgid ""
"Ignored annulment of position #{posid} at {datetime} for list \"{list}\", "
"type \"{type}\"."
msgstr "座席 #{posid} の無効化が{datetime}にリスト「{list}」、タイプ「{type}」で無視"
"されました。"
msgstr ""
"リスト\"{list}\"、タイプ\"{type}\"において、{datetime}に実行されたポジション"
"#{posid}の取り消しを無視しました。"
#: pretix/control/logdisplay.py:330
#, python-brace-format
msgid ""
"Ignored annulment of position #{posid} for list \"{list}\", type \"{type}\"."
msgstr "座席 #{posid} の無効化がリスト「{list}」、タイプ「{type}」で無視されました。"
msgstr ""
"リスト\"{list}\"、タイプ\"{type}\"のポジション#{posid}の取り消しを無視しまし"
"た。"
#: pretix/control/logdisplay.py:332 pretix/control/logdisplay.py:333
#, python-brace-format
@@ -16622,8 +16635,9 @@ msgstr "メールアドレスは\"{old_email}\"から\"{new_email}\"に変更さ
msgid ""
"The email address has been confirmed to be working (the user clicked on a "
"link in the email for the first time)."
msgstr "メールアドレスが正常に動作することが確認できました(ユーザーが初めてメール内の"
"リンクをクリックしました)。"
msgstr ""
"メールアドレスが有効であることが確認されました(ユーザーが初回のメール内のリ"
"ンクをクリックしました)。"
#: pretix/control/logdisplay.py:520
#, python-brace-format
@@ -16740,8 +16754,8 @@ msgstr "参加者にカスタムメールが送信されました。"
msgid ""
"An email has been sent with a reminder that the ticket is available for "
"download."
msgstr "チケットがダウンロード可能であることを通知するリマインダーメールが送信されま"
"した。"
msgstr ""
"チケットのダウンロードが可能であることを通知するメールが送信されました。"
#: pretix/control/logdisplay.py:550
msgid ""
@@ -16796,7 +16810,8 @@ msgstr ""
msgid ""
"An email has been sent to notify the user that the order has been received "
"and requires approval."
msgstr "注文を受け付け、承認が必要であることをユーザーに通知するメールが送信されまし"
msgstr ""
"注文が受信され、承認が必要であることをユーザーに通知するメールが送信されまし"
"た。"
#: pretix/control/logdisplay.py:571
@@ -16948,7 +16963,7 @@ msgstr "エクスポートのスケジュールが変更されました。"
#: pretix/control/logdisplay.py:690 pretix/control/logdisplay.py:737
msgid "A scheduled export has been deleted."
msgstr "スケジュールされたエクスポートが削除されました。"
msgstr "エクスポートのスケジュールが作事されました。"
#: pretix/control/logdisplay.py:691 pretix/control/logdisplay.py:738
msgid "A scheduled export has been executed."
@@ -18398,7 +18413,7 @@ msgstr "現金"
#: pretix/control/templates/pretixcontrol/checkin/bulk_revert_confirm.html:4
#: pretix/control/templates/pretixcontrol/checkin/bulk_revert_confirm.html:6
msgid "Delete check-ins"
msgstr "チェックインを削除"
msgstr "チケットのチェックインが完了しました"
#: pretix/control/templates/pretixcontrol/checkin/bulk_revert_confirm.html:15
#, python-format
@@ -18832,7 +18847,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/checkin/list_edit.html:113
msgid "Please double-check if this was intentional."
msgstr "こが意図的なものであるか再度ご確認ください。"
msgstr "このことが意図的であるかどうかをもう一度確認してください。"
#: pretix/control/templates/pretixcontrol/checkin/lists.html:10
msgid ""
@@ -19138,22 +19153,23 @@ msgid ""
"\n"
"Your %(instance)s team\n"
msgstr ""
"こんにちは、\n"
"前略\n"
"\n"
"%(instance)s で %(address)s を送信元アドレスとして使用するリクエストがありま"
"した。\n"
"これにより、このメールアドレス発信元とするメールを送信できるようになります"
"。\n"
"ご本人によるリクエストの場合は、以下の確認コードを入力してください:\n"
"誰かにより、%(instance)sの送信元アドレスとして%(address)sを使用することが要求"
"されました。\n"
"これにより、その人は当該メールアドレス発信者であると表示された電子メールを"
"送信できるようになります。\n"
"ご本人からのリクエストで間違いがなければ、次の確認コードを入力してくださ"
"い:\n"
"\n"
"%(code)s\n"
"\n"
"このリクエストに心当たりがない場合は、このメールを無視していただいて問題あり"
"ません。\n"
"もしこれがあなたによってリクエストされたものでない場合は、このメールを無視し"
"てください。\n"
"\n"
"よろしくお願いいたします、\n"
"敬具\n"
"\n"
"%(instance)s チーム\n"
"担当 %(instance)sチーム\n"
#: pretix/control/templates/pretixcontrol/email/forgot.txt:1
#, python-format
@@ -19290,7 +19306,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/email_setup_simple.html:8
#: pretix/control/templates/pretixcontrol/email_setup_smtp.html:8
msgid "Email sending"
msgstr "メール送信"
msgstr "電子メール送信"
#: pretix/control/templates/pretixcontrol/email_setup.html:21
msgid "Use system default"
@@ -19300,8 +19316,9 @@ msgstr "システムのデフォルトを使用"
msgid ""
"Emails will be sent through the system's default server. They will show the "
"following sender information:"
msgstr "メールはシステムのデフォルトサーバーから送信されます。以下の送信者情報が表示"
"されます:"
msgstr ""
"電子メールはシステムのデフォルトサーバーを通じて送信されます。送信者情報は以"
"下のように表示されます:"
#: pretix/control/templates/pretixcontrol/email_setup.html:35
msgctxt "mail_header"
@@ -19382,8 +19399,8 @@ msgid ""
"We've sent an email to %(recp)s with a confirmation code to verify that this "
"email address is owned by you. Please enter the verification code below:"
msgstr ""
"%(recp)s に確認コードを記載したメールを送信しました。このメールアドレスがご本"
"のものであることを確認するため、以下に確認コードを入力してください:"
"%(recp)sに確認コードを含む電子メールを送信しました。このメールアドレスが"
"なたのものであることを確認するため、以下に確認コードを入力してください:"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html:63
msgid "Verification code"
@@ -20110,7 +20127,7 @@ msgstr "空席待ちリスト通知"
#: pretix/control/templates/pretixcontrol/event/mail.html:117
msgid "Order custom mail"
msgstr "注文のカスタムメール"
msgstr "注文のカスタム電子メール"
#: pretix/control/templates/pretixcontrol/event/mail.html:123
msgid "Reminder to download tickets"
@@ -20790,7 +20807,7 @@ msgstr "条件イベント開催地"
#: pretix/control/templates/pretixcontrol/event/tax_edit.html:61
msgid "Calculation"
msgstr "計算"
msgstr "取り消し"
#: pretix/control/templates/pretixcontrol/event/tax_edit.html:64
msgid "Reason"
@@ -20944,9 +20961,10 @@ msgid ""
"less than 10 characters that can be easily remembered, but you can also "
"choose to use a random value."
msgstr ""
"ユーザーがチケットを購入できるアドレスです。短く、小文字、数字、ドット、ダッ"
"シュのみを含み、イベント間で一意である必要があります。簡単に覚えられる10文字"
"未満の略称や日付の使用を推奨しますが、ランダムな値を選択することもできます。"
"こちらがチケットを購入できるアドレスです。短く、小文字、数字、ドット、ダッ"
"シュのみを含み、イベント間で一意である必要があります。簡単に覚えられるような"
"略語や10文字未満の日付をお勧めしますが、ランダムな値を使用することも選択でき"
"ます。"
#: pretix/control/templates/pretixcontrol/events/create_basics.html:29
msgid ""
@@ -21145,9 +21163,9 @@ msgid ""
"page does not guarantee you are within the license. Only the original "
"license text is legally binding."
msgstr ""
"このページの文章と出力は法的拘束力を持た、このページ記入しても、ライセン"
"スを遵守していること保証されるわけではありません。法的拘束力を持つのは、ラ"
"イセンスの原文のみです。"
"このページのテキストや出力は法的拘束力を持ちません。また、このページ記入"
"ライセンスの範囲内であること保証するものではありません。法的拘束力を持つ"
"のは元のライセンステキストのみです。"
#: pretix/control/templates/pretixcontrol/global_license.html:14
msgid ""
@@ -21517,8 +21535,9 @@ msgid ""
"This product exists in multiple variations which are different in either "
"their name, price, quota, or description. All other settings need to be the "
"same."
msgstr "この製品は、名前、価格、定員枠、または説明が異なる複数のバリエーションで存在"
"します。その他のすべての設定は同じである必要があります。"
msgstr ""
"この製品は複数のバリエーションで存在し、それぞれが名前、価格、クォータ、また"
"は説明が異なります。その他の設定は同じである必要があります。"
#: pretix/control/templates/pretixcontrol/item/create.html:132
msgid ""
@@ -21776,9 +21795,9 @@ msgid ""
"feature and are not suitable for strictly ensuring that products are only "
"available in certain combinations."
msgstr ""
"クロスセル・カテゴリはマーケティング機能として意図されており、製品が特定の組"
"み合わせでのみ利用可能であることを厳密に保証するためには適していないことにご"
"注意ください。"
"クロスセリングカテゴリはマーケティング機能として意図されており、厳密に製品"
"が特定の組み合わせでのみ利用可能であることを確実にするためには適していませ"
"。"
#: pretix/control/templates/pretixcontrol/items/category.html:39
msgid "Category history"
@@ -21878,10 +21897,10 @@ msgid ""
"purchases (\"buy a package of 10 you can turn into individual tickets "
"later\"), you can use customer accounts and memberships instead."
msgstr ""
"自動割引は、有効化されている限りすべての顧客利用できます。特定の顧客のみに"
"特別価格を提供したい場合は、代わりにバウチャーを使用できます。複数の購入にわ"
"たる割引(「後で個チケットに変換できる10枚セットを購入」など)を提供したい場"
"合は、代わりに顧客アカウントとメンバーシップを使用できます。"
"自動割引は、顧客がアクティブである限りすべての顧客利用可能です。特定の顧"
"客にだけ特別価格を提供したい場合は、代わりにバウチャーを使用できます。複数の"
"購入に割引を提供したい場合「10個のパッケージを購入すると後で個々のチケット"
"に変換できます」)、代わりに顧客アカウントや会員資格を使用できます。"
#: pretix/control/templates/pretixcontrol/items/discounts.html:23
msgid ""
@@ -22395,8 +22414,9 @@ msgstr "アクセスを取り消す"
msgid ""
"Are you sure you want to revoke access to your account for the application "
"<strong>%(application)s</strong>?"
msgstr "アプリケーション <strong>%(application)s</strong> のアカウントへのアクセスを"
"取り消してもよろしいですか?"
msgstr ""
"アプリケーション<strong>%(application)s</strong>へのアカウントアクセスを取り"
"消してもよろしいですか?"
#: pretix/control/templates/pretixcontrol/oauth/auth_revoke.html:15
#: pretix/control/templates/pretixcontrol/organizers/device_revoke.html:24
@@ -22623,7 +22643,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/change.html:220
msgid "Ticket block"
msgstr "チケットブロック"
msgstr "チケットのチェックインが完了しました"
#: pretix/control/templates/pretixcontrol/order/change.html:226
msgid "Blocked due to external constraints"
@@ -22896,8 +22916,9 @@ msgstr "連絡先メール"
msgid ""
"We know that this email address works because the user clicked a link we "
"sent them."
msgstr "ユーザーが送信されたリンクをクリックしたため、このメールアドレスが正常に動作"
"することが確認できています。"
msgstr ""
"このメールアドレスは、ユーザーがこちらから送信したリンクをクリックしたため、"
"有効であることが確認されています。"
#: pretix/control/templates/pretixcontrol/order/index.html:278
msgid ""
@@ -23231,9 +23252,9 @@ msgid ""
"products in the order are still available. If the order is pending payment, "
"the expiry date will be reset."
msgstr ""
"注文を再有効化する、キャンセル取り消され、注文は保留中または支払い済みの"
"状態に戻ます。これは注文内のすべての製品がまだ利用可能な場合にのみ可能で"
"す。注文が支払い保留中の場合、有効期限がリセットされます。"
"注文を再有効化することで、キャンセル取り消し、保留中または支払い済みの注文"
"に戻ます。これは注文内のすべての製品がまだ利用可能な場合にのみ可能です。注"
"文が支払い保留中の場合、有効期限がリセットされます。"
#: pretix/control/templates/pretixcontrol/order/reactivate.html:34
msgid "Reactivate"
@@ -23509,8 +23530,9 @@ msgstr ""
msgid ""
"All actions performed on this page are irreversible. If in doubt, please "
"contact support before using it."
msgstr "このページで実行されるすべての操作は元に戻すことができません。不明な点がある"
"場合は、使用する前にサポートにお問い合わせください。"
msgstr ""
"このページで行われるすべてのアクションは取り消しできません。疑問がある場合"
"は、使用する前にサポートに連絡してください。"
#: pretix/control/templates/pretixcontrol/orders/cancel.html:29
msgctxt "subevents"
@@ -23845,7 +23867,8 @@ msgstr ""
msgid ""
"All recipients of the export will be able to see who the owner of the report "
"is."
msgstr "エクスポートの受信者全員が、レポートの所有者が誰であるかを確認できます。"
msgstr ""
"エクスポートの受信者は、レポートの所有者が誰であるかを見ることができます。"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:13
msgctxt "order state"
@@ -23899,8 +23922,9 @@ msgstr "新しいファイルをアップロード"
msgid ""
"The uploaded file should be a CSV file with a header row. You will be able "
"to assign the meanings of the different columns in the next step."
msgstr "アップロードするファイルは、ヘッダー行を含むCSVファイルである必要があります。"
"次のステップで各列の意味を割り当てることができます。"
msgstr ""
"アップロードされたファイルはヘッダー行を持つCSVファイルである必要があります。"
"次のステップで異なる列の意味を割り当てることができます。"
#: pretix/control/templates/pretixcontrol/orders/import_start.html:22
#: pretix/control/templates/pretixcontrol/vouchers/import_start.html:22
@@ -24129,7 +24153,7 @@ msgstr "クライアントID"
#: pretix/control/templates/pretixcontrol/organizers/channel_delete.html:5
msgid "Delete sales channel:"
msgstr "販売チャネルを削除:"
msgstr "不明な販路。"
#: pretix/control/templates/pretixcontrol/organizers/channel_delete.html:10
msgid "Are you sure you want to delete this sales channel?"
@@ -24139,12 +24163,13 @@ msgstr "この販売チャネルを削除してもよろしいですか?"
msgid ""
"This sales channel cannot be deleted since it has already been used to sell "
"orders or because it is a core element of the system."
msgstr "この販売チャネルは、すでに注文の販売に使用されているか、システムの中核要素で"
"あるため、削除できません。"
msgstr ""
"この販売チャンネルは、すでに注文の販売に使用されているか、システムの中核要素"
"であるため、削除することはできません。"
#: pretix/control/templates/pretixcontrol/organizers/channel_edit.html:6
msgid "Sales channel:"
msgstr "販売チャネル:"
msgstr "不明な販路。"
#: pretix/control/templates/pretixcontrol/organizers/channels.html:8
msgid ""
@@ -24974,7 +24999,7 @@ msgstr "プロパティ:"
#: pretix/control/templates/pretixcontrol/organizers/property_edit.html:26
msgid "Validation"
msgstr "検証"
msgstr "取り消し"
#: pretix/control/templates/pretixcontrol/organizers/property_edit.html:31
msgid "Allowed values"
@@ -25759,9 +25784,10 @@ msgid ""
"the affected data in your legislation, e.g. for reasons of taxation. In many "
"countries, you need to keep some data in the live system in case of an audit."
msgstr ""
"税務上の理由などで、該当するデータを削除することが法律上許可されているかどう"
"かを確認することは、あなた自身の責任です。多くの国では、監査の際に備えて、稼"
"働中のシステムに一部のデータを保持する必要があります。"
"あなたが影響を受けるデータを削除してもよいかどうかを確認するのはあなた自身の"
"責任です。たとえば、課税の理由である場合など、あなたの立法で削除が許可されて"
"いるかどうかを確認する必要があります。多くの国では、監査が発生した場合に備え"
"て、稼働中のシステムに一部のデータを保持する必要があります。"
#: pretix/control/templates/pretixcontrol/shredder/index.html:32
msgid ""
@@ -26382,15 +26408,17 @@ msgstr "通知を無効にします"
msgid ""
"We just want to make sure it's really you. Please re-authenticate with "
"'%(login_provider)s'."
msgstr "ご本人であることを確認させてください。「%(login_provider)s」で再認証してくだ"
"さい。"
msgstr ""
"本当にあなたが本人であることを確認したいだけです。 '%(login_provider)s' で再"
"認証してください。"
#: pretix/control/templates/pretixcontrol/user/reauth.html:14
msgid ""
"We just want to make sure it's really you. Please re-enter your password to "
"continue."
msgstr "ご本人であることを確認させてください。続行するにはパスワードを再入力してくだ"
"さい。"
msgstr ""
"私たちはあなたが本当にあなたであることを確認したいだけです。続行するには、パ"
"スワードを再入力してください。"
#: pretix/control/templates/pretixcontrol/user/reauth.html:26
msgid "Alternatively, you can use your WebAuthn device."
@@ -27007,8 +27035,9 @@ msgid ""
"email containing further instructions. Please note that we will send at most "
"one email every 24 hours."
msgstr ""
"アドレスが有効なアカウントに登録されている場合、詳細な手順を記載したメールを"
"送信しました。24時間ごとに最大1通のメールのみを送信することにご注意ください。"
"このアドレスが有効なアカウントに登録されている場合、詳しい手順を記載したメー"
"ルをお送りしました。24時間以内に送信するメールは最大1通であることにご注意くだ"
"さい。"
#: pretix/control/views/auth.py:360
msgid ""
@@ -28279,8 +28308,9 @@ msgstr "選択されたチームは削除できません。"
msgid ""
"The team could not be deleted because the team or one of its API tokens is "
"part of historical audit logs."
msgstr "チームまたはそのAPIトークンのいずれかが監査ログの履歴に含まれているため、チー"
"ムを削除できませんでした。"
msgstr ""
"チームは削除できません。チームまたはそのAPIトークンのいずれかが歴史的な監査ロ"
"グの一部であるためです。"
#: pretix/control/views/organizer.py:1002
msgid ""
@@ -28895,7 +28925,7 @@ msgstr "チェックイン"
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/add.html:13
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/edit.html:13
msgid "Auto check-in"
msgstr "自動チェックイン"
msgstr "チケットのチェックインが完了しました"
#: pretix/plugins/autocheckin/forms.py:60
#: pretix/plugins/autocheckin/models.py:82
@@ -28965,7 +28995,7 @@ msgstr "イベント開催地"
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/delete.html:4
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/delete.html:6
msgid "Delete auto check-in rule"
msgstr "自動チェックインルールを削除"
msgstr "チケットのチェックインが完了しました"
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/delete.html:9
msgid "Are you sure you want to delete the auto check-in rule?"
@@ -28979,7 +29009,7 @@ msgstr "チケットのチェックインが取り消されました"
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/index.html:5
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/index.html:7
msgid "Auto check-in rules"
msgstr "自動チェックインルール"
msgstr "チケットのチェックインが完了しました"
#: pretix/plugins/autocheckin/templates/pretixplugins/autocheckin/index.html:11
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:96
@@ -29082,9 +29112,10 @@ msgid ""
"Please note that your individual badge layouts must already be in the "
"correct size."
msgstr ""
"このオプションを使用すると、1ページに複数のバッジを配置できます。えば、通"
"のオフィスプリンターでシール用紙に印刷する場合などに使用します。個々のバッジ"
"レイアウトすでに正しいサイズになっている必要があることにご注意ください。"
"このオプションを使用すると、1ページに複数のバッジを配置できます。たとえば、通"
"のオフィスプリンターでシールシートに印刷したい場合などに便利です。個々の"
"バッジのレイアウトすでに正しいサイズである必要がありますので、ご注意くださ"
"い。"
#: pretix/plugins/badges/exporters.py:465
msgid "Start event date"
@@ -29133,8 +29164,9 @@ msgid ""
"invalid values in your databases, such as answers to number questions which "
"are not a number."
msgstr ""
"データを要求どおりに変換できませんでした。これは、数値の質問に対する回答が数"
"値でないなど、データベース内の無効な値が原因である可能性があります。"
"申し訳ありませんが、リクエストされたデータを変換できませんでした。これは、"
"データベース内の無効な値(たとえば、数値の質問に対する数値でない回答など)が"
"原因である可能性があります。"
#: pretix/plugins/badges/forms.py:33
msgid "Template"
@@ -29994,8 +30026,9 @@ msgstr ""
msgid ""
"I'm sorry, but we detected this file as empty. Please contact support for "
"help."
msgstr "申し訳ございませんが、このファイルが空であることが検出されました。サポートに"
"ご連絡ください。"
msgstr ""
"申し訳ありませんが、このファイルは空であると検出されました。サポートに連絡し"
"てサポートを受けてください。"
#: pretix/plugins/banktransfer/views.py:479
msgid "Invalid input data."
@@ -30440,8 +30473,9 @@ msgid ""
"pay in multiple installments or within 30 days. You, as the merchant, are "
"getting your money right away."
msgstr ""
"顧客に対して、(一定の上限まで)今すぐ購入し、複数回の分割払いまたは30日以内に"
"支払う選択肢を提供します。販売者であるあなたは、すぐに代金を受け取ります。"
"顧客には、一定の限度額まで今すぐ購入し、複数回の分割払いまたは30日以内に支払"
"うことができる選択肢を提供します。販売者であるあなたは、すぐに代金を受け取"
"ことができます。"
#: pretix/plugins/paypal2/payment.py:217
msgid "-- Automatic --"
@@ -30799,9 +30833,10 @@ msgid ""
"might therefore be inaccurate with regards to orders that were changed in "
"the time frame."
msgstr ""
"レポートの期間には、このレポート作成に必要なすべてのデータをまだ保存してい"
"ない古いソフトウェアバージョンで生成されたデータが含まれています。そのため、"
"期間内に変更された注文に関してレポートが不正確になる可能性があります。"
"レポートの時間枠には、このレポート作成するために必要なすべてのデータをまだ"
"保存していなかった古いソフトウェアバージョンで生成されたデータが含まれていま"
"す。したがって、このレポートは時間枠内で変更された注文に関して不正確である可"
"能性があります。"
#: pretix/plugins/reports/accountingreport.py:645
#: pretix/plugins/reports/accountingreport.py:695
@@ -31680,8 +31715,9 @@ msgstr "テスト中"
msgid ""
"If your event is in test mode, we will always use Stripe's test API, "
"regardless of this setting."
msgstr "イベントがテストモードの場合、この設定に関わらず、常にStripeのテストAPIを使用"
"します。"
msgstr ""
"もしイベントがテストモードである場合、この設定に関係なく常にStripeのテストAPI"
"を使用します。"
#: pretix/plugins/stripe/payment.py:277
msgid "Publishable key"
@@ -32337,15 +32373,15 @@ msgid ""
"statement that you can obtain from your bank. You agree to receive "
"notifications for future debits up to 2 days before they occur."
msgstr ""
"支払い情報を提供しこの支払いを確認することで、(A) %(sepa_creditor_name)s お"
"よび当社の決済サービスプロバイダーであるStripe および/または その現地サービス"
"プロバイダーであるPPROが、あなたの銀行に対してあなたの口座から引き落とすよう"
"指示を送信すること、および (B) あなたの銀行がその指示に従ってあなたの口座から"
"引き落とすことを承認します。あなたの権利の一として、銀行との契約の規約に基"
"づき、銀行から払い戻しを受ける権利があります。払い戻しは、口座から引き落と"
"れた日から8週間以内に請求する必要があります。あなたの権利については、銀行か"
"入手できる明細書説明されています。今後の引き落としについて、実行の最大2日"
"までに通知を受け取ることに同意します。"
"支払い情報を提供しこの支払いを確認することで、あなたは以下を承認します:"
"(A) %(sepa_creditor_name)s および当社の決済サービスプロバイダーであるStripe"
"またはその現地サービスプロバイダーであるPPROが、あなたの口座から引き落としを"
"行うよう銀行に指示を送信すること、(B) あなたの銀行がその指示に従ってあなたの"
"口座から引き落としを行うこと。あなたの権利の一として、銀行との契約の条件に"
"づき、銀行から払い戻しを受ける権利があります。払い戻しは、口座から引き落と"
"れた日から8週間以内に請求する必要があります。あなたの権利については、銀行か"
"入手できる明細書説明されています。今後の引き落としについて、実行の最大2日"
"までに通知を受け取ることに同意します。"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:7
msgid "Charge ID"
@@ -35720,7 +35756,8 @@ msgstr "このイベントでは空席待ちリストが無効になっていま
msgid ""
"You cannot add yourself to the waiting list as this product is currently "
"available."
msgstr "この製品は現在購入可能であるため、空席待ちリストに追加できません。"
msgstr ""
"この製品は現在利用可能であるため、空席待ちリストにご自身を追加できません。"
#: pretix/presale/views/waiting.py:180
#, python-brace-format

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"PO-Revision-Date: 2025-11-08 13:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/"
"pretix-js/ja/>\n"
"PO-Revision-Date: 2025-10-27 12:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
"Language: ja\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.14.2\n"
"X-Generator: Weblate 5.14\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -299,7 +299,7 @@ msgstr "チケットコードのブロック/変更"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Ticket blocked"
msgstr "チケットブロックされました"
msgstr "チケットブロック"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-11-07 23:00+0000\n"
"PO-Revision-Date: 2025-10-10 17:00+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
"sv/>\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.14.2\n"
"X-Generator: Weblate 5.13.3\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -8899,10 +8899,10 @@ msgstr ""
"exporter."
#: pretix/base/services/invoices.py:115
#, python-brace-format
#, fuzzy, python-brace-format
msgctxt "invoice"
msgid "Please complete your payment before {expire_date}."
msgstr "{expire_date}."
msgstr "."
#: pretix/base/services/invoices.py:127
#, python-brace-format
@@ -35400,7 +35400,9 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html:43
msgid "Please note that we still await your payment to complete the process."
msgstr "."
msgstr ""
"Observera att vi fortfarande väntar på din betalning för att slutföra "
"processen."
#: pretix/presale/templates/pretixpresale/event/order.html:55
msgid ""

View File

@@ -26,6 +26,7 @@ from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from pretix.base.logentrytype_registry import LogEntryType, log_entry_types
from pretix.base.models import Checkin, OrderPayment
from pretix.base.signals import (
checkin_created, event_copy_data, item_copy_data, logentry_display,
@@ -64,19 +65,13 @@ def nav_event_receiver(sender, request, **kwargs):
]
@receiver(signal=logentry_display)
def logentry_display_receiver(sender, logentry, **kwargs):
plains = {
"pretix.plugins.autocheckin.rule.added": _("An auto check-in rule was created"),
"pretix.plugins.autocheckin.rule.changed": _(
"An auto check-in rule was updated"
),
"pretix.plugins.autocheckin.rule.deleted": _(
"An auto check-in rule was deleted"
),
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@log_entry_types.new_from_dict({
"pretix.plugins.autocheckin.rule.added": _("An auto check-in rule was created"),
"pretix.plugins.autocheckin.rule.changed": _("An auto check-in rule was updated"),
"pretix.plugins.autocheckin.rule.deleted": _("An auto check-in rule was deleted"),
})
class AutocheckinLogEntryType(LogEntryType):
pass
@receiver(item_copy_data, dispatch_uid="autocheckin_item_copy")

View File

@@ -31,6 +31,7 @@ from django.utils.translation import gettext_lazy as _
from paypalhttp import HttpResponse
from pretix.base.forms import SecretKeySettingsField
from pretix.base.logentrytype_registry import LogEntryType, log_entry_types
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
@@ -80,37 +81,36 @@ def html_head_presale(sender, request=None, **kwargs):
return ""
@receiver(signal=logentry_display, dispatch_uid="stripe_logentry_display")
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
if logentry.action_type != 'pretix.plugins.stripe.event':
return
@log_entry_types.new()
class StripeEvent(LogEntryType):
action_type = 'pretix.plugins.stripe.event'
data = json.loads(logentry.data)
event_type = data.get('type')
text = None
plains = {
'charge.succeeded': _('Charge succeeded.'),
'charge.refunded': _('Charge refunded.'),
'charge.updated': _('Charge updated.'),
'charge.pending': _('Charge pending'),
'source.chargeable': _('Payment authorized.'),
'source.canceled': _('Payment authorization canceled.'),
'source.failed': _('Payment authorization failed.')
}
def display(self, logentry, data):
event_type = data.get('type')
text = None
plains = {
'charge.succeeded': _('Charge succeeded.'),
'charge.refunded': _('Charge refunded.'),
'charge.updated': _('Charge updated.'),
'charge.pending': _('Charge pending'),
'source.chargeable': _('Payment authorized.'),
'source.canceled': _('Payment authorization canceled.'),
'source.failed': _('Payment authorization failed.')
}
if event_type in plains:
text = plains[event_type]
elif event_type == 'charge.failed':
text = _('Charge failed. Reason: {}').format(data['data']['object']['failure_message'])
elif event_type == 'charge.dispute.created':
text = _('Dispute created. Reason: {}').format(data['data']['object']['reason'])
elif event_type == 'charge.dispute.updated':
text = _('Dispute updated. Reason: {}').format(data['data']['object']['reason'])
elif event_type == 'charge.dispute.closed':
text = _('Dispute closed. Status: {}').format(data['data']['object']['status'])
if event_type in plains:
text = plains[event_type]
elif event_type == 'charge.failed':
text = _('Charge failed. Reason: {}').format(data['data']['object']['failure_message'])
elif event_type == 'charge.dispute.created':
text = _('Dispute created. Reason: {}').format(data['data']['object']['reason'])
elif event_type == 'charge.dispute.updated':
text = _('Dispute updated. Reason: {}').format(data['data']['object']['reason'])
elif event_type == 'charge.dispute.closed':
text = _('Dispute closed. Status: {}').format(data['data']['object']['status'])
if text:
return _('Stripe reported an event: {}').format(text)
if text:
return _('Stripe reported an event: {}').format(text)
settings_hierarkey.add_default('payment_stripe_method_card', True, bool)

View File

@@ -20,12 +20,10 @@
# <https://www.gnu.org/licenses/>.
#
import datetime
from collections import namedtuple
from urllib.parse import urlparse
import vobject
from django.conf import settings
from django.db.models import prefetch_related_objects
from django.utils.formats import date_format
from django.utils.translation import gettext as _
@@ -124,109 +122,61 @@ def get_private_icals(event, positions):
creation_time = datetime.datetime.now(datetime.timezone.utc)
calobjects = []
calentries = set() # using set for automatic deduplication of CalEntries
CalEntry = namedtuple('CalEntry', ['summary', 'description', 'location', 'dtstart', 'dtend', 'uid'])
# collecting the positions' calendar entries, preferring the most exact date and time available (positions > subevent > event)
prefetch_related_objects(positions, 'item__program_times')
for p in positions:
ev = p.subevent or event
program_times = p.item.program_times.all()
if program_times:
# if program times have been configured, they are preferred for the position's calendar entries
evs = set(p.subevent or event for p in positions)
for ev in evs:
if isinstance(ev, Event):
url = build_absolute_uri(event, 'presale:event.index')
for index, pt in enumerate(program_times):
summary = _('{event} - {item}').format(event=ev, item=p.item.name)
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
descr.append(str(_('Start: {datetime}')).format(
datetime=date_format(pt.start.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
descr.append(str(_('End: {datetime}')).format(
datetime=date_format(pt.end.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
location = None
dtstart = pt.start.astimezone(tz)
dtend = pt.end.astimezone(tz)
uid = 'pretix-{}-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
p.item.id,
index,
urlparse(url).netloc
)
calentries.add(CalEntry(summary, description, location, dtstart, dtend, uid))
else:
# without program times, the subevent or event times are used for calendar entries, preferring subevents
if p.subevent:
url = build_absolute_uri(event, 'presale:event.index', {
'subevent': p.subevent.pk
})
else:
url = build_absolute_uri(event, 'presale:event.index')
url = build_absolute_uri(event, 'presale:event.index', {
'subevent': ev.pk
})
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
if ev.date_admission:
descr.append(str(_('Admission: {datetime}')).format(
datetime=date_format(ev.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
if ev.date_admission:
descr.append(str(_('Admission: {datetime}')).format(
datetime=date_format(ev.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
summary = str(ev.name)
if ev.location:
location = ", ".join(l.strip() for l in str(ev.location).splitlines() if l.strip())
else:
location = None
if event.settings.show_times:
dtstart = ev.date_from.astimezone(tz)
else:
dtstart = ev.date_from.astimezone(tz).date()
if event.settings.show_date_to and ev.date_to:
if event.settings.show_times:
dtend = ev.date_to.astimezone(tz)
else:
# with full-day events date_to in pretix is included (e.g. last day)
# whereas dtend in vcalendar is non-inclusive => add one day for export
dtend = ev.date_to.astimezone(tz).date() + datetime.timedelta(days=1)
else:
dtend = None
uid = 'pretix-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
ev.pk if p.subevent else '0',
urlparse(url).netloc
)
calentries.add(CalEntry(summary, description, location, dtstart, dtend, uid))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
for calentry in calentries:
cal = vobject.iCalendar()
cal.add('prodid').value = '-//pretix//{}//'.format(settings.PRETIX_INSTANCE_NAME.replace(" ", "_"))
vevent = cal.add('vevent')
vevent.add('summary').value = calentry.summary
vevent.add('description').value = calentry.description
vevent.add('summary').value = str(ev.name)
vevent.add('description').value = description
vevent.add('dtstamp').value = creation_time
if calentry.location:
vevent.add('location').value = calentry.location
vevent.add('uid').value = calentry.uid
vevent.add('dtstart').value = calentry.dtstart
if calentry.dtend:
vevent.add('dtend').value = calentry.dtend
if ev.location:
vevent.add('location').value = ", ".join(l.strip() for l in str(ev.location).splitlines() if l.strip())
vevent.add('uid').value = 'pretix-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
ev.pk if not isinstance(ev, Event) else '0',
urlparse(url).netloc
)
if event.settings.show_times:
vevent.add('dtstart').value = ev.date_from.astimezone(tz)
else:
vevent.add('dtstart').value = ev.date_from.astimezone(tz).date()
if event.settings.show_date_to and ev.date_to:
if event.settings.show_times:
vevent.add('dtend').value = ev.date_to.astimezone(tz)
else:
# with full-day events date_to in pretix is included (e.g. last day)
# whereas dtend in vcalendar is non-inclusive => add one day for export
vevent.add('dtend').value = ev.date_to.astimezone(tz).date() + datetime.timedelta(days=1)
calobjects.append(cal)
return calobjects

View File

@@ -3,8 +3,6 @@
{% load eventurl %}
{% load money %}
{% load thumb %}
{% load icon %}
{% load getitem %}
{% load eventsignal %}
{% load rich_text %}
{% for tup in items_by_category %}{% with category=tup.0 items=tup.1 form_prefix=tup.2 %}
@@ -45,9 +43,7 @@
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">
{{ item.name }}
</h{{ headline_level|default:3|add:1 }}>
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h{{ headline_level|default:3|add:1 }}>
{% if item.description %}
<div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
@@ -123,19 +119,7 @@
data-price="{% if event.settings.display_net_prices %}{{ var.display_price.net|unlocalize }}{% else %}{{ var.display_price.gross|unlocalize }}{% endif %}"
{% endif %}>
<div class="col-md-8 col-sm-6 col-xs-12">
<h{{ headline_level|default:3|add:2 }} class="h5" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend">
{{ var }}
{% if cart.itemvarsums|getitem:var %}
<span class="textbubble-success">
{% icon "shopping-cart" %}
{% blocktrans trimmed count amount=cart.itemvarsums|getitem:var %}
{{ amount }}× in your cart
{% plural %}
{{ amount }}× in your cart
{% endblocktrans %}
</span>
{% endif %}
</h{{ headline_level|default:3|add:2 }}>
<h{{ headline_level|default:3|add:2 }} class="h5" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h{{ headline_level|default:3|add:2 }}>
{% if var.description %}
<div id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
{{ var.description|localize|rich_text }}
@@ -280,19 +264,7 @@
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">
{{ item.name }}
{% if cart.itemvarsums|getitem:item %}
<span class="textbubble-success">
{% icon "shopping-cart" %}
{% blocktrans trimmed count amount=cart.itemvarsums|getitem:item %}
{{ amount }}× in your cart
{% plural %}
{{ amount }}× in your cart
{% endblocktrans %}
</span>
{% endif %}
</h{{ headline_level|default:3|add:1 }}>
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h{{ headline_level|default:3|add:1 }}>
{% if item.description %}
<div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}

View File

@@ -33,7 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import copy
import warnings
from collections import Counter, defaultdict
from collections import defaultdict
from datetime import datetime, timedelta
from decimal import Decimal
from functools import wraps
@@ -242,10 +242,6 @@ class CartMixin:
minutes_left = None
seconds_left = None
itemvarsums = Counter()
for p in cartpos:
itemvarsums[p.variation or p.item] += 1
return {
'positions': positions,
'invoice_address': self.invoice_address,
@@ -262,7 +258,6 @@ class CartMixin:
'max_expiry_extend': max_expiry_extend,
'is_ordered': bool(order),
'itemcount': sum(c.count for c in positions if not c.addon_to),
'itemvarsums': itemvarsums,
'current_selected_payments': [
p for p in self.current_selected_payments(positions, fees, self.invoice_address)
if p.get('multi_use_supported')

View File

@@ -263,11 +263,3 @@ svg.svg-icon {
@include table-row-variant('warning', var(--pretix-brand-warning-lighten-40), var(--pretix-brand-warning-lighten-35));
@include table-row-variant('danger', var(--pretix-brand-danger-lighten-30), var(--pretix-brand-danger-lighten-25));
.confirmation-code-input {
font-size: 200%;
font-family: monospace;
font-stretch: expanded;
text-align: center;
height: 50px;
margin: 10px 0;
}

View File

@@ -938,25 +938,3 @@ details {
}
}
}
@media (min-width: $screen-lg-min) {
.centered-form {
margin: 80px auto;
max-width: 800px;
border: 1px solid #ddd;
padding: 20px 40px 0;
border-radius: 4px;
box-shadow: 2px 2px 2px #eee;
}
.form.centered-form .submit-group {
margin: 25px -40px 0 !important;
padding-right: 40px;
padding-left: 40px;
}
.centered-form p {
margin: 20px 0;
}
}

View File

@@ -574,7 +574,6 @@ h2 .label {
.textbubble-success, .textbubble-success-warning, .textbubble-info, .textbubble-warning, .textbubble-danger {
display: inline-block;
padding: 0 .4em;
border-radius: $border-radius-base;
font-weight: bold;

View File

@@ -46,8 +46,7 @@ from tests.const import SAMPLE_PNG
from pretix.base.models import (
CartPosition, InvoiceAddress, Item, ItemAddOn, ItemBundle, ItemCategory,
ItemProgramTime, ItemVariation, Order, OrderPosition, Question,
QuestionOption, Quota,
ItemVariation, Order, OrderPosition, Question, QuestionOption, Quota,
)
from pretix.base.models.orders import OrderFee
@@ -332,7 +331,6 @@ TEST_ITEM_RES = {
"variations": [],
"addons": [],
"bundles": [],
"program_times": [],
"show_quota_left": None,
"original_price": None,
"free_price_suggestion": None,
@@ -511,24 +509,6 @@ def test_item_detail_bundles(token_client, organizer, event, team, item, categor
assert res == resp.data
@pytest.mark.django_db
def test_item_detail_program_times(token_client, organizer, event, team, item, category):
with scopes_disabled():
item.program_times.create(item=item, start=datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc))
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
res["program_times"] = [{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
}]
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_item_create(token_client, organizer, event, item, category, taxrule, membership_type):
resp = token_client.post(
@@ -1092,57 +1072,6 @@ def test_item_create_with_bundle(token_client, organizer, event, item, category,
assert resp.content.decode() == '{"bundles":["The chosen variation does not belong to this item."]}'
@pytest.mark.django_db
def test_item_create_with_product_time(token_client, organizer, event, item, category, taxrule):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug),
{
"category": category.pk,
"name": {
"en": "Ticket"
},
"active": True,
"description": None,
"default_price": "23.00",
"free_price": False,
"tax_rate": "19.00",
"tax_rule": taxrule.pk,
"admission": True,
"issue_giftcard": False,
"position": 0,
"picture": None,
"available_from": None,
"available_until": None,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
"min_per_order": None,
"max_per_order": None,
"checkin_attention": False,
"checkin_text": None,
"has_variations": False,
"program_times": [
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
},
{
"start": "2017-12-29T00:00:00Z",
"end": "2017-12-30T00:00:00Z",
}
]
},
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
new_item = Item.objects.get(pk=resp.data['id'])
assert new_item.program_times.first().start == datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc)
assert new_item.program_times.first().end == datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc)
assert new_item.program_times.last().start == datetime(2017, 12, 29, 0, 0, 0, tzinfo=timezone.utc)
assert new_item.program_times.last().end == datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc)
@pytest.mark.django_db(transaction=True)
def test_item_update(token_client, organizer, event, item, category, item2, category2, taxrule2):
resp = token_client.patch(
@@ -1218,8 +1147,8 @@ def test_item_update(token_client, organizer, event, item, category, item2, cate
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug, item.pk),
@@ -1236,8 +1165,8 @@ def test_item_update(token_client, organizer, event, item, category, item2, cate
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
item.personalized = True
item.admission = True
@@ -1393,8 +1322,8 @@ def test_item_update_with_variation(token_client, organizer, event, item):
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
@pytest.mark.django_db
@@ -1416,8 +1345,8 @@ def test_item_update_with_addon(token_client, organizer, event, item, category):
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
@pytest.mark.django_db
@@ -1952,123 +1881,6 @@ def test_addons_delete(token_client, organizer, event, item, addon):
assert not item.addons.filter(pk=addon.id).exists()
@pytest.fixture
def program_time(item, category):
return item.program_times.create(start=datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc))
@pytest.fixture
def program_time2(item, category):
return item.program_times.create(start=datetime(2017, 12, 29, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc))
TEST_PROGRAM_TIMES_RES = {
0: {
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
},
1: {
"start": "2017-12-29T00:00:00Z",
"end": "2017-12-30T00:00:00Z",
}
}
@pytest.mark.django_db
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2):
res = dict(TEST_PROGRAM_TIMES_RES)
res[0]["id"] = program_time.pk
res[1]["id"] = program_time2.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res[0]['start'] == resp.data['results'][0]['start']
assert res[0]['end'] == resp.data['results'][0]['end']
assert res[0]['id'] == resp.data['results'][0]['id']
assert res[1]['start'] == resp.data['results'][1]['start']
assert res[1]['end'] == resp.data['results'][1]['end']
assert res[1]['id'] == resp.data['results'][1]['id']
@pytest.mark.django_db
def test_program_times_detail(token_client, organizer, event, item, program_time):
res = dict(TEST_PROGRAM_TIMES_RES)
res[0]["id"] = program_time.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug,
item.pk, program_time.pk))
assert resp.status_code == 200
assert res[0] == resp.data
@pytest.mark.django_db
def test_program_times_create(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z"
},
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc) == program_time.start
assert datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc) == program_time.end
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-28T00:00:00Z",
"end": "2017-12-27T00:00:00Z"
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
@pytest.mark.django_db
def test_program_times_update(token_client, organizer, event, item, program_time):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug, item.pk,
program_time.pk),
{
"start": "2017-12-26T00:00:00Z"
},
format='json'
)
assert resp.status_code == 200
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert datetime(2017, 12, 26, 0, 0, 0, tzinfo=timezone.utc) == program_time.start
assert datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc) == program_time.end
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug, item.pk,
program_time.pk),
{
"start": "2017-12-30T00:00:00Z"
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
@pytest.mark.django_db
def test_program_times_delete(token_client, organizer, event, item, program_time):
resp = token_client.delete(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug,
item.pk, program_time.pk))
assert resp.status_code == 204
with scopes_disabled():
assert not item.program_times.filter(pk=program_time.id).exists()
@pytest.fixture
def quota(event, item):
q = event.quotas.create(name="Budget Quota", size=200)

View File

@@ -1996,7 +1996,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['positions'][0].get('pdf_data')
# order list
with django_assert_max_num_queries(33):
with django_assert_max_num_queries(32):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
))

View File

@@ -39,9 +39,7 @@ import pytest
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
Event, ItemProgramTime, Organizer, Question, SeatingPlan,
)
from pretix.base.models import Event, Organizer, Question, SeatingPlan
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
@@ -80,10 +78,6 @@ def test_full_clone_same_organizer():
# todo: test that item pictures are copied, not linked
ItemMetaValue.objects.create(item=item1, property=item_meta, value="Foo")
assert item1.meta_data
ItemProgramTime.objects.create(item=item1,
start=datetime.datetime(2017, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc))
assert item1.program_times
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15,
hidden_if_item_available=item1)
item2v = item2.variations.create(value="red", default_price=15, all_sales_channels=False)
@@ -167,8 +161,6 @@ def test_full_clone_same_organizer():
assert copied_item1.category == copied_event.categories.get(name='Tickets')
assert copied_item1.limit_sales_channels.get() == sc
assert copied_item1.meta_data == item1.meta_data
assert copied_item1.program_times.first().start == item1.program_times.first().start
assert copied_item1.program_times.first().end == item1.program_times.first().end
assert copied_item2.variations.get().meta_data == item2v.meta_data
assert copied_item1.hidden_if_available == copied_q2
assert copied_item1.grant_membership_type == membership_type

View File

@@ -1134,7 +1134,7 @@ class PasswordChangeRequiredTest(TestCase):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
def test_redirect_to_password_change(self):
def test_redirect_to_settings(self):
self.user.needs_password_change = True
self.user.save()
self.client.login(email='dummy@dummy.dummy', password='dummy')
@@ -1143,9 +1143,9 @@ class PasswordChangeRequiredTest(TestCase):
self.assertEqual(response.status_code, 302)
assert self.user.needs_password_change is True
self.assertIn('/control/settings/password/change?next=/control/events/', response['Location'])
self.assertIn('/control/settings?next=/control/events/', response['Location'])
def test_redirect_to_2fa_to_password_change(self):
def test_redirect_to_2fa_to_settings(self):
self.user.require_2fa = True
self.user.needs_password_change = True
self.user.save()
@@ -1168,4 +1168,4 @@ class PasswordChangeRequiredTest(TestCase):
response = self.client.get('/control/events/')
self.assertEqual(response.status_code, 302)
self.assertIn('/control/settings/password/change?next=/control/events/', response['Location'])
self.assertIn('/control/settings?next=/control/events/', response['Location'])

View File

@@ -681,10 +681,6 @@ class ItemsTest(ItemFormTest):
self.var2.save()
prop = self.event1.item_meta_properties.create(name="Foo")
self.item2.meta_values.create(property=prop, value="Bar")
self.item2.program_times.create(start=datetime.datetime(2017, 12, 27, 0, 0, 0,
tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0,
tzinfo=datetime.timezone.utc))
doc = self.get_doc('/control/event/%s/%s/items/add?copy_from=%d' % (self.orga1.slug, self.event1.slug, self.item2.pk))
data = extract_form_fields(doc.select("form")[0])
@@ -713,8 +709,6 @@ class ItemsTest(ItemFormTest):
assert i_new.meta_data == i_old.meta_data == {"Foo": "Bar"}
assert set(i_new.questions.all()) == set(i_old.questions.all())
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])
assert i_old.program_times.first().start == i_new.program_times.first().start
assert i_old.program_times.first().end == i_new.program_times.first().end
def test_add_to_existing_quota(self):
with scopes_disabled():

View File

@@ -19,11 +19,22 @@
# 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 re
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Jason Estibeiro
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# 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 time
import pytest
from django.core import mail as djmail
from django.utils.timezone import now
from django_otp.oath import TOTP
from django_otp.plugins.otp_static.models import StaticDevice
@@ -45,7 +56,7 @@ class UserSettingsTest(SoupTest):
self.user = User.objects.create_user('dummy@dummy.dummy', 'barfoofoo')
self.client.login(email='dummy@dummy.dummy', password='barfoofoo')
doc = self.get_doc('/control/settings')
self.form_data = extract_form_fields(doc.select('form[data-testid="usersettingsform"]')[0])
self.form_data = extract_form_fields(doc.select('.container-fluid form')[0])
def save(self, data):
form_data = self.form_data.copy()
@@ -60,107 +71,33 @@ class UserSettingsTest(SoupTest):
self.user = User.objects.get(pk=self.user.pk)
assert self.user.fullname == 'Peter Miller'
def test_set_locale_and_timezone(self):
def test_change_email_require_password(self):
doc = self.save({
'locale': 'fr',
'timezone': 'Europe/Paris',
'email': 'foo@example.com',
})
assert doc.select(".alert-success")
assert doc.select(".alert-danger")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.locale == 'fr'
assert self.user.timezone == 'Europe/Paris'
class UserEmailChangeTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'barfoofoo')
self.client.login(email='dummy@dummy.dummy', password='barfoofoo')
session = self.client.session
session['pretix_auth_login_time'] = int(time.time())
session.save()
doc = self.get_doc('/control/settings/email/change')
self.form_data = extract_form_fields(doc.select('.container-fluid form')[0])
def test_require_reauth(self):
session = self.client.session
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 2
session.save()
response = self.client.get('/control/settings/email/change')
self.assertIn('/control/reauth', response['Location'])
self.assertEqual(response.status_code, 302)
response = self.client.post('/control/reauth/?next=/control/settings/email/change', {
'password': 'barfoofoo'
})
self.assertIn('/control/settings/email/change', response['Location'])
self.assertEqual(response.status_code, 302)
def submit_step_1(self, data):
form_data = self.form_data.copy()
form_data.update(data)
return self.post_doc('/control/settings/email/change', form_data)
def submit_step_2(self, data):
form_data = self.form_data.copy()
form_data.update(data)
return self.post_doc('/control/settings/email/confirm?reason=email_change', form_data)
assert self.user.email == 'dummy@dummy.dummy'
def test_change_email_success(self):
djmail.outbox = []
doc = self.submit_step_1({
'new_email': 'foo@example.com',
})
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['foo@example.com']
code = re.search("[0-9]{7}", djmail.outbox[0].body).group(0)
doc = self.submit_step_2({
'code': code,
doc = self.save({
'email': 'foo@example.com',
'old_pw': 'barfoofoo'
})
assert doc.select(".alert-success")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'foo@example.com'
def test_change_email_wrong_code(self):
djmail.outbox = []
doc = self.submit_step_1({
'new_email': 'foo@example.com',
})
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['foo@example.com']
code = re.search("[0-9]{7}", djmail.outbox[0].body).group(0)
wrong_code = '0000000' if code == '1234567' else '1234567'
doc = self.submit_step_2({
'code': wrong_code,
})
assert doc.select(".alert-danger")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'dummy@dummy.dummy'
def test_change_email_no_duplicates(self):
User.objects.create_user('foo@example.com', 'foo')
doc = self.submit_step_1({
'new_email': 'foo@example.com',
doc = self.save({
'email': 'foo@example.com',
'old_pw': 'barfoofoo'
})
assert doc.select(".alert-danger")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'dummy@dummy.dummy'
class UserPasswordChangeTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'barfoofoo')
self.client.login(email='dummy@dummy.dummy', password='barfoofoo')
doc = self.get_doc('/control/settings/password/change')
self.form_data = extract_form_fields(doc.select('.container-fluid form')[0])
def save(self, data):
form_data = self.form_data.copy()
form_data.update(data)
return self.post_doc('/control/settings/password/change', form_data)
def test_change_password_require_password(self):
doc = self.save({
'new_pw': 'foo',
@@ -256,6 +193,18 @@ class UserPasswordChangeTest(SoupTest):
})
assert doc.select(".alert-danger")
def test_needs_password_change(self):
self.user.needs_password_change = True
self.user.save()
doc = self.save({
'email': 'foo@example.com',
'old_pw': 'barfoofoo'
})
assert doc.select(".alert-success")
assert doc.select(".alert-warning")
self.user.refresh_from_db()
assert self.user.needs_password_change is True
def test_needs_password_change_changed(self):
self.user.needs_password_change = True
self.user.save()