mirror of
https://github.com/pretix/pretix.git
synced 2026-01-18 23:42:27 +00:00
Compare commits
6 Commits
typeahead-
...
logentryty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8e5c7867b | ||
|
|
7010752bb0 | ||
|
|
7d8bcd4b10 | ||
|
|
2121566ae5 | ||
|
|
9f6216e6f1 | ||
|
|
c824663946 |
@@ -19,7 +19,6 @@ at :ref:`plugin-docs`.
|
||||
item_bundles
|
||||
item_add-ons
|
||||
item_meta_properties
|
||||
item_program_times
|
||||
questions
|
||||
question_options
|
||||
quotas
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.*",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -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'}),
|
||||
)
|
||||
|
||||
@@ -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)')),
|
||||
))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."))
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ class UserEditForm(forms.ModelForm):
|
||||
'email',
|
||||
'require_2fa',
|
||||
'is_active',
|
||||
'is_verified',
|
||||
'is_staff',
|
||||
'needs_password_change',
|
||||
'last_login'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user