Compare commits

..
Author SHA1 Message Date
Raphael Michel 3cdb992134 Bump dnspython to 2.2.* 2022-12-20 11:55:52 +01:00
254 changed files with 92346 additions and 132117 deletions
-1
View File
@@ -81,6 +81,5 @@ jobs:
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
with: with:
file: src/coverage.xml file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.10' if: matrix.database == 'postgres' && matrix.python-version == '3.10'
+1 -1
View File
@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_. offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 11.6** but it should work very similar on other We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
modern distributions, especially on all systemd-based ones. modern distributions, especially on all systemd-based ones.
Requirements Requirements
-3
View File
@@ -192,9 +192,6 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"`` File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
specifiers in requests specifiers in requests
(see below). (see below).
Date range *either* two dates separated ``2022-03-18/2022-03-23``, ``2022-03-18/``,
by ``/`` *or* the name of a ``/2022-03-23``, ``week_this``, ``week_next``,
defined range. ``month_this``
===================== ============================ =================================== ===================== ============================ ===================================
Query parameters Query parameters
-2
View File
@@ -98,8 +98,6 @@ Endpoints
:query string ends_after: Exclude all check-in lists attached to a sub-event that is already in the past at the given time. :query string ends_after: Exclude all check-in lists attached to a sub-event that is already in the past at the given time.
:query string expand: Expand a field into a full object. Currently only ``subevent`` is supported. Can be passed multiple times. :query string expand: Expand a field into a full object. Currently only ``subevent`` is supported. Can be passed multiple times.
:query string exclude: Exclude a field from the output, e.g. ``checkin_count``. Can be used as a performance optimization. Can be passed multiple times. :query string exclude: Exclude a field from the output, e.g. ``checkin_count``. Can be used as a performance optimization. Can be passed multiple times.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``name``, and ``subevent__date_from``,
Default: ``subevent__date_from,name``
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch :param event: The ``slug`` field of the event to fetch
:statuscode 200: no error :statuscode 200: no error
+1 -12
View File
@@ -35,12 +35,6 @@ tax_rule integer The internal ID
admission boolean ``true`` for items that grant admission to the event admission boolean ``true`` for items that grant admission to the event
(such as primary tickets) and ``false`` for others (such as primary tickets) and ``false`` for others
(such as add-ons or merchandise). (such as add-ons or merchandise).
personalized boolean ``true`` for items that require personalization according
to event settings. Only affects system-level fields, not
custom questions. Currently only allowed for products with
``admission`` set to ``true``. For backwards compatibility,
when creating new items and this field is not given, it defaults
to the same value as ``admission``.
position integer An integer, used for sorting position integer An integer, used for sorting
picture file A product picture to be displayed in the shop picture file A product picture to be displayed in the shop
(can be ``null``). (can be ``null``).
@@ -164,7 +158,7 @@ meta_data object Values set for
.. versionchanged:: 4.16 .. versionchanged:: 4.16
The ``variations[x].meta_data`` attribute has been added. The ``personalized`` attribute has been added. The ``variations[x].meta_data`` attribute has been added.
Notes Notes
----- -----
@@ -219,7 +213,6 @@ Endpoints
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"personalized": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {}, "meta_data": {},
"position": 0, "position": 0,
@@ -336,7 +329,6 @@ Endpoints
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"personalized": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {}, "meta_data": {},
"position": 0, "position": 0,
@@ -434,7 +426,6 @@ Endpoints
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"personalized": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {}, "meta_data": {},
"position": 0, "position": 0,
@@ -519,7 +510,6 @@ Endpoints
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"personalized": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {}, "meta_data": {},
"position": 0, "position": 0,
@@ -636,7 +626,6 @@ Endpoints
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"personalized": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {}, "meta_data": {},
"position": 0, "position": 0,
-4
View File
@@ -76,10 +76,6 @@ The exporter class
This is an abstract attribute, you **must** override this! This is an abstract attribute, you **must** override this!
.. autoattribute:: description
.. autoattribute:: category
.. autoattribute:: export_form_fields .. autoattribute:: export_form_fields
.. automethod:: render .. automethod:: render
+4 -127
View File
@@ -17,13 +17,9 @@ Field Type Description
id integer Internal layout ID id integer Internal layout ID
name string Internal layout description name string Internal layout description
default boolean ``true`` if this is the default layout default boolean ``true`` if this is the default layout
layout list Dynamic layout specification. Each list element layout object Layout specification for libpretixprint
corresponds to one dynamic element of the layout.
The current version of the schema in use can be found
`here`_.
Submitting invalid content can lead to application errors.
background URL Background PDF file background URL Background PDF file
item_assignments list of objects Products this layout is assigned to (currently read-only) item_assignments list of objects Products this layout is assigned to
├ sales_channel string Sales channel (defaults to ``web``). ├ sales_channel string Sales channel (defaults to ``web``).
└ item integer Item ID └ item integer Item ID
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -62,7 +58,7 @@ Endpoints
"name": "Default layout", "name": "Default layout",
"default": true, "default": true,
"layout": {…}, "layout": {…},
"background": null, "background": {},
"item_assignments": [] "item_assignments": []
} }
] ]
@@ -100,7 +96,7 @@ Endpoints
"name": "Default layout", "name": "Default layout",
"default": true, "default": true,
"layout": {…}, "layout": {…},
"background": null, "background": {},
"item_assignments": [] "item_assignments": []
} }
@@ -151,122 +147,3 @@ Endpoints
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it. :statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
Creates a new ticket layout
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "Default layout",
"default": true,
"layout": […],
"background": null,
"item_assignments": []
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": […],
"background": null,
"item_assignments": []
}
:param organizer: The ``slug`` field of the organizer of the event to create a layout for
:param event: The ``slug`` field of the event to create a layout for
:statuscode 201: no error
:statuscode 400: The layout 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)/ticketlayouts/(id)/
Update a layout. 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.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"name": "Default layout"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": […],
"background": null,
"item_assignments": []
}
: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 layout to modify
:statuscode 200: no error
:statuscode 400: The layout 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)/ticketlayouts/(id)/
Delete a layout.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/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 layout 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.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
-1
View File
@@ -97,7 +97,6 @@ overpayment
param param
passphrase passphrase
percental percental
personalization
pluggable pluggable
positionid positionid
pre pre
-43
View File
@@ -22,10 +22,8 @@
from django import forms from django import forms
from django.http import QueryDict from django.http import QueryDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.base.exporter import OrganizerLevelExportMixin from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
class FormFieldWrapperField(serializers.Field): class FormFieldWrapperField(serializers.Field):
@@ -144,12 +142,6 @@ class JobRunSerializer(serializers.Serializer):
allow_null=not v.required, allow_null=not v.required,
validators=v.validators, validators=v.validators,
) )
elif isinstance(v, DateFrameField):
self.fields[k] = SerializerDateFrameField(
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
else: else:
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required) self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
@@ -159,40 +151,5 @@ class JobRunSerializer(serializers.Serializer):
for k, v in self.fields.items(): for k, v in self.fields.items():
if isinstance(v, serializers.ManyRelatedField) and k not in data: if isinstance(v, serializers.ManyRelatedField) and k not in data:
data[k] = [] data[k] = []
for fk in self.fields.keys():
# Backwards compatibility for exports that used to take e.g. (date_from, date_to) or (event_date_from, event_date_to)
# and now only take date_range.
if fk.endswith("_range") and isinstance(self.fields[fk], SerializerDateFrameField) and fk not in data:
if fk.replace("_range", "_from") in data:
d_from = data.pop(fk.replace("_range", "_from"))
if d_from:
d_from = serializers.DateField().to_internal_value(d_from)
else:
d_from = None
if fk.replace("_range", "_to") in data:
d_to = data.pop(fk.replace("_range", "_to"))
if d_to:
d_to = serializers.DateField().to_internal_value(d_to)
else:
d_to = None
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
data = super().to_internal_value(data) data = super().to_internal_value(data)
return data return data
def is_valid(self, raise_exception=False):
super().is_valid(raise_exception=raise_exception)
fields_keys = set(self.fields.keys())
input_keys = set(self.initial_data.keys())
additional_fields = input_keys - fields_keys
if bool(additional_fields):
self._errors['fields'] = ['Additional fields not allowed: {}.'.format(list(additional_fields))]
if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors)
+1 -14
View File
@@ -95,12 +95,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
@transaction.atomic @transaction.atomic
def create(self, validated_data): def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None) meta_data = validated_data.pop('meta_data', None)
require_membership_types = validated_data.pop('require_membership_types', [])
variation = ItemVariation.objects.create(**validated_data) variation = ItemVariation.objects.create(**validated_data)
if require_membership_types:
variation.require_membership_types.add(*require_membership_types)
# Meta data # Meta data
if meta_data is not None: if meta_data is not None:
for key, value in meta_data.items(): for key, value in meta_data.items():
@@ -234,7 +230,7 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = Item model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description', fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission', 'personalized', 'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until', 'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling', 'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations', 'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
@@ -262,15 +258,6 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order')) 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')) Item.clean_available(data.get('available_from'), data.get('available_until'))
if data.get('personalized') and not data.get('admission'):
raise ValidationError(_('Only admission products can currently be personalized.'))
if data.get('admission') and 'personalized' not in data and not self.instance:
# Backwards compatibility
data['personalized'] = True
elif 'admission' in data and not data['admission']:
data['personalized'] = False
if data.get('issue_giftcard'): if data.get('issue_giftcard'):
if data.get('tax_rule') and data.get('tax_rule').rate > 0: if data.get('tax_rule') and data.get('tax_rule').rate > 0:
raise ValidationError( raise ValidationError(
-8
View File
@@ -118,10 +118,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
raise ValidationError( raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']} {'name': ['Do not specify name if you specified name_parts.']}
) )
if data.get('name_parts') and not isinstance(data.get('name_parts'), dict):
raise ValidationError({'name_parts': ['Invalid data type']})
if data.get('name_parts') and '_scheme' not in data.get('name_parts'): if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
@@ -845,10 +841,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError( raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']} {'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
) )
if data.get('attendee_name_parts') and not isinstance(data.get('attendee_name_parts'), dict):
raise ValidationError({'attendee_name_parts': ['Invalid data type']})
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'): if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
+2 -4
View File
@@ -158,14 +158,12 @@ class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
a.question_id: a for a in instance.answers.all() a.question_id: a for a in instance.answers.all()
} }
for answ_data in answers_data: for answ_data in answers_data:
if not answ_data.get('answer'):
continue
options = answ_data.pop('options', []) options = answ_data.pop('options', [])
if answ_data['question'].pk in qs_seen: if answ_data['question'].pk in qs_seen:
raise ValidationError(f'Question {answ_data["question"]} was sent twice.') raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
if answ_data['question'].pk in answercache: if answ_data['question'].pk in answercache:
a = answercache[answ_data['question'].pk] a = answercache[answ_data['question'].pk]
if isinstance(answ_data.get('answer'), File): if isinstance(answ_data['answer'], File):
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False) a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
a.answer = 'file://' + a.file.name a.answer = 'file://' + a.file.name
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep": elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
@@ -175,7 +173,7 @@ class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
setattr(a, attr, value) setattr(a, attr, value)
a.save() a.save()
else: else:
if isinstance(answ_data.get('answer'), File): if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer') an = answ_data.pop('answer')
a = instance.answers.create(**answ_data, answer='') a = instance.answers.create(**answ_data, answer='')
a.file.save(os.path.basename(an.name), an, save=False) a.file.save(os.path.basename(an.name), an, save=False)
-7
View File
@@ -79,13 +79,6 @@ class CustomerSerializer(I18nAwareModelSerializer):
validated_data['external_identifier'] = instance.external_identifier validated_data['external_identifier'] = instance.external_identifier
return super().update(instance, validated_data) return super().update(instance, validated_data)
def validate(self, data):
if data.get('name_parts') and not isinstance(data.get('name_parts'), dict):
raise ValidationError({'name_parts': ['Invalid data type']})
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].organizer.settings.name_scheme
return data
class CustomerCreateSerializer(CustomerSerializer): class CustomerCreateSerializer(CustomerSerializer):
send_email = serializers.BooleanField(default=False, required=False, allow_null=True) send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
+1 -3
View File
@@ -93,10 +93,8 @@ with scopes_disabled():
class CheckinListViewSet(viewsets.ModelViewSet): class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer serializer_class = CheckinListSerializer
queryset = CheckinList.objects.none() queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter) filter_backends = (DjangoFilterBackend,)
filterset_class = CheckinListFilter filterset_class = CheckinListFilter
ordering = ('subevent__date_from', 'name')
ordering_fields = ('subevent__date_from', 'id', 'name',)
def _get_permission_name(self, request): def _get_permission_name(self, request):
if request.path.endswith('/failed_checkins/'): if request.path.endswith('/failed_checkins/'):
-11
View File
@@ -21,7 +21,6 @@
# #
import datetime import datetime
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.timezone import now from django.utils.timezone import now
from oauth2_provider.contrib.rest_framework import OAuth2Authentication from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
@@ -34,9 +33,6 @@ from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.auth.permission import AnyAuthenticatedClientPermission from pretix.api.auth.permission import AnyAuthenticatedClientPermission
from pretix.api.auth.token import TeamTokenAuthentication from pretix.api.auth.token import TeamTokenAuthentication
from pretix.base.models import CachedFile from pretix.base.models import CachedFile
from pretix.helpers.images import (
IMAGE_TYPES, validate_uploaded_file_for_valid_image,
)
ALLOWED_TYPES = { ALLOWED_TYPES = {
'image/gif': {'.gif'}, 'image/gif': {'.gif'},
@@ -65,13 +61,6 @@ class UploadView(APIView):
name=file_obj.name, name=file_obj.name,
type=content_type type=content_type
)) ))
if content_type in IMAGE_TYPES:
try:
validate_uploaded_file_for_valid_image(file_obj)
except DjangoValidationError as e:
raise ValidationError(e.message)
cf = CachedFile.objects.create( cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1), expires=now() + datetime.timedelta(days=1),
date=now(), date=now(),
+4 -2
View File
@@ -42,6 +42,7 @@ from localflavor.fr.forms import FRZipCodeField
from localflavor.gb.forms import GBPostcodeField from localflavor.gb.forms import GBPostcodeField
from localflavor.gr.forms import GRPostalCodeField from localflavor.gr.forms import GRPostalCodeField
from localflavor.hr.forms import HRPostalCodeField from localflavor.hr.forms import HRPostalCodeField
from localflavor.id_.forms import IDPostCodeField
from localflavor.ie.forms import EircodeField from localflavor.ie.forms import EircodeField
from localflavor.il.forms import ILPostalCodeField from localflavor.il.forms import ILPostalCodeField
from localflavor.in_.forms import INZipCodeField from localflavor.in_.forms import INZipCodeField
@@ -79,8 +80,8 @@ COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED = {
# We don't presume this for countries we don't have knowledge about, there are countries in the # We don't presume this for countries we don't have knowledge about, there are countries in the
# world e.g. without zipcodes # world e.g. without zipcodes
'AR', 'AT', 'AU', 'BE', 'BR', 'CA', 'CH', 'CN', 'CU', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'AR', 'AT', 'AU', 'BE', 'BR', 'CA', 'CH', 'CN', 'CU', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR',
'GB', 'GR', 'HR', 'IE', 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'LT', 'LV', 'MA', 'MT', 'MX', 'NL', 'GB', 'GR', 'HR', 'ID', 'IE', 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'LT', 'LV', 'MA', 'MT', 'MX',
'NO', 'NZ', 'PK', 'PL', 'PT', 'RO', 'RU', 'SE', 'SG', 'SI', 'SK', 'TR', 'UA', 'US', 'ZA', 'NL', 'NO', 'NZ', 'PK', 'PL', 'PT', 'RO', 'RU', 'SE', 'SG', 'SI', 'SK', 'TR', 'UA', 'US', 'ZA',
} }
@@ -166,6 +167,7 @@ _zip_code_fields = {
'GB': GBPostcodeField, 'GB': GBPostcodeField,
'GR': GRPostalCodeField, 'GR': GRPostalCodeField,
'HR': HRPostalCodeField, 'HR': HRPostalCodeField,
'ID': IDPostCodeField,
'IE': EircodeField, 'IE': EircodeField,
'IL': ILPostalCodeField, 'IL': ILPostalCodeField,
'IN': INZipCodeField, 'IN': INZipCodeField,
+7 -7
View File
@@ -520,20 +520,20 @@ def base_placeholders(sender, **kwargs):
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display() lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
), ),
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'], 'url_remove', ['waiting_list_entry', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri( lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove' event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code, ) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri( lambda event: build_absolute_uri(
event, event,
'presale:event.waitinglist.remove', 'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5', ) + '?voucher=68CYU2H6ZTP3WLK5',
), ),
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_voucher', 'event'], 'url', ['waiting_list_entry', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri( lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code, ) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri( lambda event: build_absolute_uri(
event, event,
'presale:event.redeem', 'presale:event.redeem',
@@ -588,7 +588,7 @@ def base_placeholders(sender, **kwargs):
_('Sample Admission Ticket') _('Sample Admission Ticket')
), ),
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code, 'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
'68CYU2H6ZTP3WLK5' '68CYU2H6ZTP3WLK5'
), ),
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
+1 -22
View File
@@ -36,7 +36,7 @@ import io
import tempfile import tempfile
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from decimal import Decimal from decimal import Decimal
from typing import Optional, Tuple from typing import Tuple
import pytz import pytz
from defusedcsv import csv from defusedcsv import csv
@@ -84,27 +84,6 @@ class BaseExporter:
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
@property
def description(self) -> str:
"""
A description for this exporter.
"""
return ""
@property
def category(self) -> Optional[str]:
"""
A category name for this exporter, or ``None``.
"""
return None
@property
def featured(self) -> bool:
"""
If ``True``, this exporter will be highlighted.
"""
return False
@property @property
def identifier(self) -> str: def identifier(self) -> str:
""" """
+2 -5
View File
@@ -39,7 +39,7 @@ from zipfile import ZipFile
from django import forms from django import forms
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _
from pretix.base.models import QuestionAnswer from pretix.base.models import QuestionAnswer
@@ -49,10 +49,7 @@ from ..signals import register_data_exporters
class AnswerFilesExporter(BaseExporter): class AnswerFilesExporter(BaseExporter):
identifier = 'answerfiles' identifier = 'answerfiles'
verbose_name = _('Question answer file uploads') verbose_name = _('Answers to file upload questions')
category = pgettext_lazy('export_category', 'Order data')
description = _('Download a ZIP file including all files that have been uploaded by your customers while creating '
'an order.')
@property @property
def export_form_fields(self): def export_form_fields(self):
+1 -3
View File
@@ -36,7 +36,7 @@ from collections import OrderedDict
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import get_current_timezone from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy from django.utils.translation import gettext as _, gettext_lazy
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -48,8 +48,6 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'customerlist' identifier = 'customerlist'
verbose_name = gettext_lazy('Customer accounts') verbose_name = gettext_lazy('Customer accounts')
organizer_required_permission = 'can_manage_customers' organizer_required_permission = 'can_manage_customers'
category = pgettext_lazy('export_category', 'Customer accounts')
description = gettext_lazy('Download a spreadsheet of all currently registered customer accounts.')
@property @property
def additional_form_fields(self): def additional_form_fields(self):
+28 -17
View File
@@ -23,24 +23,22 @@ import json
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.translation import gettext, gettext_lazy
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment from pretix.base.models import Invoice, OrderPayment
from ..exporter import BaseExporter from ..exporter import BaseExporter
from ..signals import register_data_exporters from ..signals import register_data_exporters
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
class DekodiNREIExporter(BaseExporter): class DekodiNREIExporter(BaseExporter):
identifier = 'dekodi_nrei' identifier = 'dekodi_nrei'
verbose_name = 'dekodi NREI (JSON)' verbose_name = 'dekodi NREI (JSON)'
category = pgettext_lazy('export_category', 'Invoices')
description = gettext_lazy("Download invoices in a format that can be used by the dekodi NREI conversion software.")
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/ # Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
@@ -115,7 +113,7 @@ class DekodiNREIExporter(BaseExporter):
'PTNo14': p.info_data.get('reference') or '', 'PTNo14': p.info_data.get('reference') or '',
'PTNo15': p.full_id or '', 'PTNo15': p.full_id or '',
}) })
elif p.provider and p.provider.startswith('stripe'): elif p.provider.startswith('stripe'):
src = p.info_data.get("source", p.info_data) src = p.info_data.get("source", p.info_data)
payments.append({ payments.append({
'PTID': '81', 'PTID': '81',
@@ -194,12 +192,17 @@ class DekodiNREIExporter(BaseExporter):
def render(self, form_data): def render(self, form_data):
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent') qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
if form_data.get('date_range'): if form_data.get('date_from'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone) date_value = form_data.get('date_from')
if d_start: if isinstance(date_value, str):
qs = qs.filter(date__gte=d_start) date_value = dateutil.parser.parse(date_value).date()
if d_end: qs = qs.filter(date__gte=date_value)
qs = qs.filter(date__lte=d_end)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
jo = { jo = {
'Format': 'NREI', 'Format': 'NREI',
@@ -215,14 +218,22 @@ class DekodiNREIExporter(BaseExporter):
def export_form_fields(self): def export_form_fields(self):
return OrderedDict( return OrderedDict(
[ [
('date_range', ('date_from',
DateFrameField( forms.DateField(
label=gettext_lazy('Date range'), label=gettext_lazy('Start date'),
include_future_frames=False, widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=gettext_lazy('Only include invoices issued in this time frame. Note that the invoice date does ' help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.') 'not always correspond to the order or payment date.')
)), )),
('date_to',
forms.DateField(
label=gettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
] ]
) )
+1 -3
View File
@@ -35,7 +35,7 @@
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _
from ...control.forms.filter import get_all_payment_providers from ...control.forms.filter import get_all_payment_providers
from ..exporter import ListExporter from ..exporter import ListExporter
@@ -45,8 +45,6 @@ from ..signals import register_multievent_data_exporters
class EventDataExporter(ListExporter): class EventDataExporter(ListExporter):
identifier = 'eventdata' identifier = 'eventdata'
verbose_name = _('Event data') verbose_name = _('Event data')
category = pgettext_lazy('export_category', 'Event data')
description = _('Download a spreadsheet with information on all events in this organizer account.')
@cached_property @cached_property
def providers(self): def providers(self):
+25 -22
View File
@@ -38,15 +38,13 @@ from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
from zipfile import ZipFile from zipfile import ZipFile
import dateutil.parser
from django import forms from django import forms
from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.translation import gettext, gettext_lazy as _, pgettext
from django.utils.translation import (
gettext, gettext_lazy as _, pgettext, pgettext_lazy,
)
from pretix.base.models import Invoice, InvoiceLine, OrderPayment from pretix.base.models import Invoice, InvoiceLine, OrderPayment
@@ -59,24 +57,30 @@ from ..services.invoices import invoice_pdf_task
from ..signals import ( from ..signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
) )
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
class InvoiceExporterMixin: class InvoiceExporterMixin:
category = pgettext_lazy('export_category', 'Invoices')
@property @property
def invoice_exporter_form_fields(self): def invoice_exporter_form_fields(self):
return OrderedDict( return OrderedDict(
[ [
('date_range', ('date_from',
DateFrameField( forms.DateField(
label=_('Date range'), label=_('Start date'),
include_future_frames=False, widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=_('Only include invoices issued in this time frame. Note that the invoice date does ' help_text=_('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.') 'not always correspond to the order or payment date.')
)), )),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
('payment_provider', ('payment_provider',
forms.ChoiceField( forms.ChoiceField(
label=_('Payment provider'), label=_('Payment provider'),
@@ -108,12 +112,16 @@ class InvoiceExporterMixin:
) )
) )
qs = qs.filter(has_payment_with_provider=1) qs = qs.filter(has_payment_with_provider=1)
if form_data.get('date_range'): if form_data.get('date_from'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone) date_value = form_data.get('date_from')
if d_start: if isinstance(date_value, str):
qs = qs.filter(date__gte=d_start) date_value = dateutil.parser.parse(date_value).date()
if d_end: qs = qs.filter(date__gte=date_value)
qs = qs.filter(date__lte=d_end) if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
return qs return qs
@@ -121,7 +129,6 @@ class InvoiceExporterMixin:
class InvoiceExporter(InvoiceExporterMixin, BaseExporter): class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
identifier = 'invoices' identifier = 'invoices'
verbose_name = _('All invoices') verbose_name = _('All invoices')
description = _('Download all invoices created by the system as a ZIP file of PDF files.')
def render(self, form_data: dict, output_file=None): def render(self, form_data: dict, output_file=None):
qs = self.invoices_queryset(form_data).filter(shredded=False) qs = self.invoices_queryset(form_data).filter(shredded=False)
@@ -173,10 +180,6 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter): class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
identifier = 'invoicedata' identifier = 'invoicedata'
verbose_name = _('Invoice data') verbose_name = _('Invoice data')
description = _('Download a spreadsheet with the data of all invoices created by the system. The spreadsheet '
'includes two sheets, one with a line for every invoice, and one with a line for every position of '
'every invoice.')
featured = True
@property @property
def additional_form_fields(self): def additional_form_fields(self):
+1 -6
View File
@@ -22,7 +22,7 @@
from django.db.models import Prefetch from django.db.models import Prefetch
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _
from openpyxl.styles import Alignment from openpyxl.styles import Alignment
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
@@ -48,8 +48,6 @@ def _min(a1, a2):
class ItemDataExporter(ListExporter): class ItemDataExporter(ListExporter):
identifier = 'itemdata' identifier = 'itemdata'
verbose_name = _('Product data') verbose_name = _('Product data')
category = pgettext_lazy('export_category', 'Product data')
description = _('Download a spreadsheet with details about all products and variations.')
def iterate_list(self, form_data): def iterate_list(self, form_data):
locales = self.event.settings.locales locales = self.event.settings.locales
@@ -75,7 +73,6 @@ class ItemDataExporter(ListExporter):
_("Free price input"), _("Free price input"),
_("Sales tax"), _("Sales tax"),
_("Is an admission ticket"), _("Is an admission ticket"),
_("Personalized ticket"),
_("Generate tickets"), _("Generate tickets"),
_("Waiting list"), _("Waiting list"),
_("Available from"), _("Available from"),
@@ -147,7 +144,6 @@ class ItemDataExporter(ListExporter):
_("Yes") if i.free_price else "", _("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "", str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "", _("Yes") if i.admission else "",
_("Yes") if i.personalized else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""), _("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "", _("Yes") if i.allow_waitinglist else "",
date_format(_max(i.available_from, v.available_from).astimezone(self.timezone), date_format(_max(i.available_from, v.available_from).astimezone(self.timezone),
@@ -191,7 +187,6 @@ class ItemDataExporter(ListExporter):
_("Yes") if i.free_price else "", _("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "", str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "", _("Yes") if i.admission else "",
_("Yes") if i.personalized else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""), _("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "", _("Yes") if i.allow_waitinglist else "",
date_format(i.available_from.astimezone(self.timezone), date_format(i.available_from.astimezone(self.timezone),
+1 -7
View File
@@ -38,8 +38,6 @@ from decimal import Decimal
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Prefetch from django.db.models import Prefetch
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import lazy
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
from ..exporter import BaseExporter from ..exporter import BaseExporter
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
@@ -48,10 +46,7 @@ from ..signals import register_data_exporters
class JSONExporter(BaseExporter): class JSONExporter(BaseExporter):
identifier = 'json' identifier = 'json'
verbose_name = lazy(lambda *args: gettext('Order data') + ' (JSON)', str)() verbose_name = 'Order data (JSON)'
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a structured JSON representation of all orders. This might be useful for the '
'import in third-party systems.')
def render(self, form_data): def render(self, form_data):
jo = { jo = {
@@ -83,7 +78,6 @@ class JSONExporter(BaseExporter):
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'), 'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
'tax_name': str(item.tax_rule.name) if item.tax_rule else None, 'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
'admission': item.admission, 'admission': item.admission,
'personalized': item.personalized,
'active': item.active, 'active': item.active,
'sales_channels': item.sales_channels, 'sales_channels': item.sales_channels,
'description': str(item.description), 'description': str(item.description),
+1 -3
View File
@@ -36,7 +36,7 @@ from collections import OrderedDict
from django import forms from django import forms
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPosition from pretix.base.models import OrderPosition
@@ -50,8 +50,6 @@ from ..signals import (
class MailExporter(BaseExporter): class MailExporter(BaseExporter):
identifier = 'mailaddrs' identifier = 'mailaddrs'
verbose_name = _('Email addresses (text file)') verbose_name = _('Email addresses (text file)')
category = pgettext_lazy('export_category', 'Order data')
description = _("Download a text file with all email addresses collected either from buyers or from ticket holders.")
def render(self, form_data: dict): def render(self, form_data: dict):
qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event') qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event')
+104 -78
View File
@@ -33,8 +33,10 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict from collections import OrderedDict
from datetime import date, datetime, time
from decimal import Decimal from decimal import Decimal
import dateutil
import pytz import pytz
from django import forms from django import forms
from django.db.models import ( from django.db.models import (
@@ -44,10 +46,8 @@ from django.db.models import (
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, now from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import ( from django.utils.translation import gettext as _, gettext_lazy, pgettext
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
)
from pretix.base.models import ( from pretix.base.models import (
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
@@ -63,24 +63,14 @@ from ...helpers.iter import chunked_iterable
from ..exporter import ( from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin, ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
) )
from ..forms.widgets import SplitDateTimePickerWidget
from ..signals import ( from ..signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
) )
from ..timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
class OrderListExporter(MultiSheetListExporter): class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist' identifier = 'orderlist'
verbose_name = gettext_lazy('Order data') verbose_name = gettext_lazy('Order data')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all orders. The spreadsheet will include three sheets, one '
'with a line for every order, one with a line for every order position, and one with '
'a line for every additional fee charged in an order.')
featured = True
@cached_property @cached_property
def providers(self): def providers(self):
@@ -115,25 +105,41 @@ class OrderListExporter(MultiSheetListExporter):
initial=False, initial=False,
required=False required=False
)), )),
('date_range', ('date_from',
DateFrameField( forms.DateField(
label=_('Date range'), label=_('Start date'),
include_future_frames=False, widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=_('Only include orders created within this date range.') help_text=_('Only include orders created on or after this date.')
)), )),
('event_date_range', ('date_to',
DateFrameField( forms.DateField(
label=_('Event date'), label=_('End date'),
include_future_frames=True, widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=_('Only include orders including at least one ticket for a date in this range. ' help_text=_('Only include orders created on or before this date.')
)),
('event_date_from',
forms.DateField(
label=_('Start event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date on or after this date. '
'Will also include other dates in case of mixed orders!')
)),
('event_date_to',
forms.DateField(
label=_('End event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date on or before this date. '
'Will also include other dates in case of mixed orders!') 'Will also include other dates in case of mixed orders!')
)), )),
] ]
d = OrderedDict(d) d = OrderedDict(d)
if not self.is_multievent and not self.event.has_subevents: if not self.is_multievent and not self.event.has_subevents:
del d['event_date_range'] del d['event_date_from']
del d['event_date_to']
return d return d
def _get_all_payment_methods(self, qs): def _get_all_payment_methods(self, qs):
@@ -176,27 +182,45 @@ class OrderListExporter(MultiSheetListExporter):
annotations = {} annotations = {}
filters = {} filters = {}
if form_data.get('date_range'): if form_data.get('date_from'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone) date_value = form_data.get('date_from')
if dt_start: if not isinstance(date_value, date):
filters[f'{rel}datetime__gte'] = dt_start date_value = dateutil.parser.parse(date_value).date()
if dt_end: datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
filters[f'{rel}datetime__lt'] = dt_end
if form_data.get('event_date_range'): filters[f'{rel}datetime__gte'] = datetime_value
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['event_date_range'], self.timezone)
if dt_start: if form_data.get('date_to'):
annotations['event_date_max'] = Case( date_value = form_data.get('date_to')
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')), if not isinstance(date_value, date):
default=F(f'{rel}event__date_from'), date_value = dateutil.parser.parse(date_value).date()
) datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
filters['event_date_max__gte'] = dt_start
if dt_end: filters[f'{rel}datetime__lte'] = datetime_value
annotations['event_date_min'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')), if form_data.get('event_date_from'):
default=F(f'{rel}event__date_from'), date_value = form_data.get('event_date_from')
) if not isinstance(date_value, date):
filters['event_date_min__lt'] = dt_end date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
annotations['event_date_max'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_max__gte'] = datetime_value
if form_data.get('event_date_to'):
date_value = form_data.get('event_date_to')
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
annotations['event_date_min'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_min__lte'] = datetime_value
if filters: if filters:
return qs.annotate(**annotations).filter(**filters) return qs.annotate(**annotations).filter(**filters)
@@ -752,10 +776,7 @@ class OrderListExporter(MultiSheetListExporter):
class PaymentListExporter(ListExporter): class PaymentListExporter(ListExporter):
identifier = 'paymentlist' identifier = 'paymentlist'
verbose_name = gettext_lazy('Payments and refunds') verbose_name = gettext_lazy('Order payments and refunds')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all payments or refunds of every order.')
featured = True
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -834,8 +855,6 @@ class PaymentListExporter(ListExporter):
class QuotaListExporter(ListExporter): class QuotaListExporter(ListExporter):
identifier = 'quotalist' identifier = 'quotalist'
verbose_name = gettext_lazy('Quota availabilities') verbose_name = gettext_lazy('Quota availabilities')
category = pgettext_lazy('export_category', 'Product data')
description = gettext_lazy('Download a spreadsheet of all quotas including their current availability.')
def iterate_list(self, form_data): def iterate_list(self, form_data):
has_subevents = self.event.has_subevents has_subevents = self.event.has_subevents
@@ -889,17 +908,21 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist' identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions') verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards' organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
@property @property
def additional_form_fields(self): def additional_form_fields(self):
d = [ d = [
('date_range', ('date_from',
DateFrameField( forms.DateField(
label=_('Date range'), label=_('Start date'),
include_future_frames=False, widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False required=False,
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)), )),
] ]
d = OrderedDict(d) d = OrderedDict(d)
@@ -910,12 +933,22 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
card__issuer=self.organizer, card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event') ).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_range'): if form_data.get('date_from'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone) date_value = form_data.get('date_from')
if dt_start: if isinstance(date_value, str):
qs = qs.filter(datetime__gte=dt_start) date_value = dateutil.parser.parse(date_value).date()
if dt_end: qs = qs.filter(
qs = qs.filter(datetime__lt=dt_end) datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [ headers = [
_('Gift card code'), _('Gift card code'),
@@ -945,8 +978,6 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
class GiftcardRedemptionListExporter(ListExporter): class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist' identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Gift card redemptions') verbose_name = gettext_lazy('Gift card redemptions')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all payments or refunds that involve gift cards.')
def iterate_list(self, form_data): def iterate_list(self, form_data):
payments = OrderPayment.objects.filter( payments = OrderPayment.objects.filter(
@@ -992,18 +1023,14 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist' identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards') verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards' organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.')
@property @property
def additional_form_fields(self): def additional_form_fields(self):
return OrderedDict( return OrderedDict(
[ [
('date', forms.SplitDateTimeField( ('date', forms.DateTimeField(
label=_('Show value at'), label=_('Show value at'),
required=False, initial=now(),
widget=SplitDateTimePickerWidget(),
help_text=_('Defaults to the time of report.')
)), )),
('testmode', forms.ChoiceField( ('testmode', forms.ChoiceField(
label=_('Test mode'), label=_('Test mode'),
@@ -1031,13 +1058,12 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
) )
def iterate_list(self, form_data): def iterate_list(self, form_data):
d = form_data.get('date') or now()
s = GiftCardTransaction.objects.filter( s = GiftCardTransaction.objects.filter(
card=OuterRef('pk'), card=OuterRef('pk'),
datetime__lte=d datetime__lte=form_data['date']
).order_by().values('card').annotate(s=Sum('value')).values('s') ).order_by().values('card').annotate(s=Sum('value')).values('s')
qs = self.organizer.issued_gift_cards.filter( qs = self.organizer.issued_gift_cards.filter(
issuance__lte=d issuance__lte=form_data['date']
).annotate( ).annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00')), cached_value=Coalesce(Subquery(s), Decimal('0.00')),
).order_by('issuance').prefetch_related( ).order_by('issuance').prefetch_related(
@@ -1052,11 +1078,11 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
if form_data.get('state') == 'empty': if form_data.get('state') == 'empty':
qs = qs.filter(cached_value=0) qs = qs.filter(cached_value=0)
elif form_data.get('state') == 'valid_value': elif form_data.get('state') == 'valid_value':
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=d)) qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=form_data['date']))
elif form_data.get('state') == 'expired_value': elif form_data.get('state') == 'expired_value':
qs = qs.exclude(cached_value=0).filter(expires__lt=d) qs = qs.exclude(cached_value=0).filter(expires__lt=form_data['date'])
elif form_data.get('state') == 'expired': elif form_data.get('state') == 'expired':
qs = qs.filter(expires__lt=d) qs = qs.filter(expires__lt=form_data['date'])
headers = [ headers = [
_('Gift card code'), _('Gift card code'),
-2
View File
@@ -39,8 +39,6 @@ from ..signals import (
class WaitingListExporter(ListExporter): class WaitingListExporter(ListExporter):
identifier = 'waitinglist' identifier = 'waitinglist'
verbose_name = _('Waiting list') verbose_name = _('Waiting list')
category = pgettext_lazy('export_category', 'Waiting list')
description = _('Download a spread sheet with all your waiting list data.')
# map selected status to label and queryset-filter # map selected status to label and queryset-filter
status_filters = [ status_filters = [
+5 -5
View File
@@ -531,7 +531,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
code='aspect_ratio_not_3_by_4', code='aspect_ratio_not_3_by_4',
) )
except Exception as exc: except Exception as exc:
logger.exception('Could not parse image') logger.exception('foo')
# Pillow doesn't recognize it as an image. # Pillow doesn't recognize it as an image.
if isinstance(exc, ValidationError): if isinstance(exc, ValidationError):
raise raise
@@ -575,7 +575,7 @@ class BaseQuestionsForm(forms.Form):
add_fields = {} add_fields = {}
if item.ask_attendee_data and event.settings.attendee_names_asked: if item.admission and event.settings.attendee_names_asked:
add_fields['attendee_name_parts'] = NamePartsFormField( add_fields['attendee_name_parts'] = NamePartsFormField(
max_length=255, max_length=255,
required=event.settings.attendee_names_required and not self.all_optional, required=event.settings.attendee_names_required and not self.all_optional,
@@ -584,7 +584,7 @@ class BaseQuestionsForm(forms.Form):
label=_('Attendee name'), label=_('Attendee name'),
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts), initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
) )
if item.ask_attendee_data and event.settings.attendee_emails_asked: if item.admission and event.settings.attendee_emails_asked:
add_fields['attendee_email'] = forms.EmailField( add_fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required and not self.all_optional, required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'), label=_('Attendee email'),
@@ -595,7 +595,7 @@ class BaseQuestionsForm(forms.Form):
} }
) )
) )
if item.ask_attendee_data and event.settings.attendee_company_asked: if item.admission and event.settings.attendee_company_asked:
add_fields['company'] = forms.CharField( add_fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional, required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'), label=_('Company'),
@@ -603,7 +603,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.company if cartpos else orderpos.company), initial=(cartpos.company if cartpos else orderpos.company),
) )
if item.ask_attendee_data and event.settings.attendee_addresses_asked: if item.admission and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField( add_fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional, required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'), label=_('Address'),
+9 -3
View File
@@ -30,6 +30,7 @@ from django.urls import get_script_prefix
from django.utils import timezone, translation from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils.translation.trans_real import ( from django.utils.translation.trans_real import (
check_for_language, get_supported_language_variant, language_code_re, check_for_language, get_supported_language_variant, language_code_re,
parse_accept_lang_header, parse_accept_lang_header,
@@ -127,7 +128,12 @@ def get_language_from_user_settings(request: HttpRequest) -> str:
return lang_code return lang_code
def get_language_from_cookie(request: HttpRequest) -> str: def get_language_from_session_or_cookie(request: HttpRequest) -> str:
if hasattr(request, 'session'):
lang_code = request.session.get(LANGUAGE_SESSION_KEY)
if lang_code in _supported and lang_code is not None and check_for_language(lang_code):
return lang_code
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
try: try:
return get_supported_language_variant(lang_code) return get_supported_language_variant(lang_code)
@@ -181,14 +187,14 @@ def get_language_from_request(request: HttpRequest) -> str:
return ( return (
get_language_from_user_settings(request) get_language_from_user_settings(request)
or get_language_from_customer_settings(request) or get_language_from_customer_settings(request)
or get_language_from_cookie(request) or get_language_from_session_or_cookie(request)
or get_language_from_browser(request) or get_language_from_browser(request)
or get_language_from_event(request) or get_language_from_event(request)
or get_default_language() or get_default_language()
) )
else: else:
return ( return (
get_language_from_cookie(request) get_language_from_session_or_cookie(request)
or get_language_from_customer_settings(request) or get_language_from_customer_settings(request)
or get_language_from_user_settings(request) or get_language_from_user_settings(request)
or get_language_from_browser(request) or get_language_from_browser(request)
@@ -1,27 +0,0 @@
# Generated by Django 3.2.16 on 2022-12-21 08:59
from django.db import migrations, models
def item_set_personalized(apps, schema_editor):
# We cannot really know if a position was bundled or an add-on, but we can at least guess
Item = apps.get_model("pretixbase", "Item")
Item.objects.filter(admission=True).update(personalized=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0226_itemvariationmetavalue'),
]
operations = [
migrations.AddField(
model_name='item',
name='personalized',
field=models.BooleanField(default=False),
),
migrations.RunPython(
item_set_personalized,
migrations.RunPython.noop,
),
]
@@ -1,68 +0,0 @@
# Generated by Django 3.2.16 on 2023-01-18 11:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0227_item_personalized'),
]
operations = [
migrations.CreateModel(
name='ScheduledOrganizerExport',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('export_identifier', models.CharField(max_length=190)),
('export_form_data', models.JSONField(default=dict)),
('locale', models.CharField(max_length=250)),
('mail_additional_recipients', models.TextField()),
('mail_additional_recipients_cc', models.TextField()),
('mail_additional_recipients_bcc', models.TextField()),
('mail_subject', models.CharField(max_length=250)),
('mail_template', models.TextField()),
('schedule_rrule', models.TextField(null=True)),
('schedule_rrule_time', models.TimeField()),
('schedule_next_run', models.DateTimeField(blank=True, null=True)),
('error_counter', models.IntegerField(default=0)),
('error_last_message', models.TextField(null=True)),
('timezone', models.CharField(default='UTC', max_length=100)),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_exports', to='pretixbase.organizer')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='ScheduledEventExport',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('export_identifier', models.CharField(max_length=190)),
('export_form_data', models.JSONField(default=dict)),
('locale', models.CharField(max_length=250)),
('mail_additional_recipients', models.TextField()),
('mail_additional_recipients_cc', models.TextField()),
('mail_additional_recipients_bcc', models.TextField()),
('mail_subject', models.CharField(max_length=250)),
('mail_template', models.TextField()),
('schedule_rrule', models.TextField(null=True)),
('schedule_rrule_time', models.TimeField()),
('schedule_next_run', models.DateTimeField(blank=True, null=True)),
('error_counter', models.IntegerField(default=0)),
('error_last_message', models.TextField(null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_exports', to='pretixbase.event')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]
-1
View File
@@ -30,7 +30,6 @@ from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue, Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
SubEvent, SubEventMetaValue, generate_invite_token, SubEvent, SubEventMetaValue, generate_invite_token,
) )
from .exports import ScheduledEventExport, ScheduledOrganizerExport
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import ( from .items import (
+3 -53
View File
@@ -35,17 +35,14 @@ from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection, models from django.db import models
from django.db.models import ( from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
)
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField from pretix.base.models.fields import MultiStringField
from pretix.helpers import PostgresWindowFrame
class CheckinList(LoggedModel): class CheckinList(LoggedModel):
@@ -143,54 +140,7 @@ class CheckinList(LoggedModel):
@property @property
def inside_count(self): def inside_count(self):
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]: return self.positions_inside.count()
# Use the simple query that works on all databases
return self.positions_inside.count()
# Use the PostgreSQL-specific query using Window functions, which is a lot faster.
# On a real-world example with ~100k tickets, of which ~17k are checked in, we observed
# a speed-up from 29s (old) to a few hundred milliseconds (new)!
# Why is this so much faster? The regular query get's PostgreSQL all busy with filtering
# the tickets both by their belonging the event and checkin status at the same time, while
# this query just iterates over all successful checkins on the list, and -- by the power
# of window functions -- asks "is this an entry that is followed by no exit?". Then we
# dedupliate by position and count it up.
cl = self
base_q, base_params = (
Checkin.all.filter(successful=True, position__in=cl.positions, list=cl)
.annotate(
cnt_exists_after=Window(
expression=Count("position_id", filter=Q(type=Value("exit"))),
partition_by=[F("position_id"), F("list_id")],
order_by=F("datetime").asc(),
frame=PostgresWindowFrame(
"ROWS", start="1 following", end="unbounded following"
),
)
)
.values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params()
)
with connection.cursor() as cursor:
cursor.execute(
f"""
SELECT COUNT(*) FROM (
SELECT COUNT("position_id")
FROM ({str(base_q)} ) s
WHERE "type" = %s AND "cnt_exists_after" = 0
GROUP BY "position_id"
) a;
""",
[
*base_params,
"entry",
],
)
rows = cursor.fetchall()
if rows:
return rows[0][0]
return 0
@property @property
@scopes_disabled() @scopes_disabled()
-2
View File
@@ -632,7 +632,6 @@ class Event(EventMixin, LoggedModel):
return super().presale_has_ended return super().presale_has_ended
def delete_all_orders(self, really=False): def delete_all_orders(self, really=False):
from .checkin import Checkin
from .orders import ( from .orders import (
OrderFee, OrderPayment, OrderPosition, OrderRefund, Transaction, OrderFee, OrderPayment, OrderPosition, OrderRefund, Transaction,
) )
@@ -646,7 +645,6 @@ class Event(EventMixin, LoggedModel):
OrderFee.objects.filter(order__event=self).delete() OrderFee.objects.filter(order__event=self).delete()
OrderRefund.objects.filter(order__event=self).delete() OrderRefund.objects.filter(order__event=self).delete()
OrderPayment.objects.filter(order__event=self).delete() OrderPayment.objects.filter(order__event=self).delete()
Checkin.objects.filter(list__event=self).delete()
self.orders.all().delete() self.orders.all().delete()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
-139
View File
@@ -1,139 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import datetime, timedelta
import pytz
from dateutil.rrule import rrulestr
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import LoggedModel
from pretix.base.validators import RRuleValidator, multimail_validate
class AbstractScheduledExport(LoggedModel):
id = models.BigAutoField(primary_key=True)
export_identifier = models.CharField(
max_length=190,
verbose_name=_("Export"),
)
export_form_data = models.JSONField(
default=dict,
encoder=DjangoJSONEncoder,
)
owner = models.ForeignKey(
"pretixbase.User",
on_delete=models.PROTECT,
)
locale = models.CharField(
verbose_name=_('Language'),
max_length=250
)
mail_additional_recipients = models.TextField(
verbose_name=_('Additional recipients'),
null=False, blank=True, validators=[multimail_validate],
help_text=_("You can specify multiple recipients separated by commas.")
)
mail_additional_recipients_cc = models.TextField(
verbose_name=_('Additional recipients (Cc)'),
null=False, blank=True, validators=[multimail_validate],
help_text=_("You can specify multiple recipients separated by commas.")
)
mail_additional_recipients_bcc = models.TextField(
verbose_name=_('Additional recipients (Bcc)'),
null=False, blank=True, validators=[multimail_validate],
help_text=_("You can specify multiple recipients separated by commas.")
)
mail_subject = models.CharField(
verbose_name=_('Subject'),
max_length=250
)
mail_template = models.TextField(
verbose_name=_('Message'),
)
schedule_rrule = models.TextField(
null=True, blank=True, validators=[RRuleValidator()]
)
schedule_rrule_time = models.TimeField(
verbose_name=_("Requested start time"),
help_text=_("The actual start time might be delayed depending on system load."),
)
schedule_next_run = models.DateTimeField(null=True, blank=True)
error_counter = models.IntegerField(default=0)
error_last_message = models.TextField(null=True, blank=True)
class Meta:
abstract = True
def __str__(self):
return self.mail_subject
def compute_next_run(self):
tz = self.tz
r = rrulestr(self.schedule_rrule)
base_dt = now().astimezone(tz).replace(tzinfo=None)
if now().astimezone(tz).time() < self.schedule_rrule_time:
base_dt -= timedelta(days=1)
new_d = r.after(base_dt, inc=False)
if not new_d:
self.schedule_next_run = None
return
try:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
except pytz.exceptions.AmbiguousTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz, is_dst=False)
except pytz.exceptions.NonExistentTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time) + timedelta(hours=1), tz)
class ScheduledEventExport(AbstractScheduledExport):
event = models.ForeignKey(
"pretixbase.Event", on_delete=models.CASCADE, related_name="scheduled_exports"
)
@property
def tz(self):
return self.event.timezone
class ScheduledOrganizerExport(AbstractScheduledExport):
organizer = models.ForeignKey(
"pretixbase.Organizer", on_delete=models.CASCADE, related_name="scheduled_exports"
)
timezone = models.CharField(max_length=100,
default=settings.TIME_ZONE,
verbose_name=_('Timezone'))
@property
def tz(self):
return pytz.timezone(self.timezone)
+3 -17
View File
@@ -62,7 +62,6 @@ from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice from pretix.base.models.tax import TaxedPrice
from ...helpers.images import ImageSizeValidator
from .event import Event, SubEvent from .event import Event, SubEvent
@@ -311,8 +310,6 @@ class Item(LoggedModel):
:type tax_rate: decimal.Decimal :type tax_rate: decimal.Decimal
:param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise) :param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise)
:type admission: bool :type admission: bool
:param personalized: ``True``, if attendee information should be collected for this ticket
:type personalized: bool
:param picture: A product picture to be shown next to the product description :param picture: A product picture to be shown next to the product description
:type picture: File :type picture: File
:param available_from: The date this product goes on sale :param available_from: The date this product goes on sale
@@ -399,14 +396,8 @@ class Item(LoggedModel):
admission = models.BooleanField( admission = models.BooleanField(
verbose_name=_("Is an admission ticket"), verbose_name=_("Is an admission ticket"),
help_text=_( help_text=_(
'Whether or not buying this product allows a person to enter your event' 'Whether or not buying this product allows a person to enter '
), 'your event'
default=False
)
personalized = models.BooleanField(
verbose_name=_("Is a personalized ticket"),
help_text=_(
'Whether or not buying this product allows to enter attendee information'
), ),
default=False default=False
) )
@@ -430,8 +421,7 @@ class Item(LoggedModel):
picture = models.ImageField( picture = models.ImageField(
verbose_name=_("Product picture"), verbose_name=_("Product picture"),
null=True, blank=True, max_length=255, null=True, blank=True, max_length=255,
upload_to=itempicture_upload_to, upload_to=itempicture_upload_to
validators=[ImageSizeValidator()]
) )
available_from = models.DateTimeField( available_from = models.DateTimeField(
verbose_name=_("Available from"), verbose_name=_("Available from"),
@@ -588,10 +578,6 @@ class Item(LoggedModel):
return self.event.settings.show_quota_left return self.event.settings.show_quota_left
return self.show_quota_left return self.show_quota_left
@property
def ask_attendee_data(self):
return self.admission and self.personalized
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False): def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
price = price if price is not None else self.default_price price = price if price is not None else self.default_price
+2 -2
View File
@@ -808,7 +808,7 @@ class Order(LockModel, LoggedModel):
return True return True
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool) ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
for cp in positions: for cp in positions:
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all(): if (cp.item.admission and ask_names) or cp.item.questions.all():
return True return True
return False # nothing there to modify return False # nothing there to modify
@@ -2688,7 +2688,7 @@ class CartPosition(AbstractPosition):
category_key = (self.item.category.position, self.item.category.id) if self.item.category_id is not None else (0, 0) category_key = (self.item.category.position, self.item.category.id) if self.item.category_id is not None else (0, 0)
item_key = self.item.position, self.item_id item_key = self.item.position, self.item_id
variation_key = (self.variation.position, self.variation.id) if self.variation_id is not None else (0, 0) variation_key = (self.variation.position, self.variation.id) if self.variation_id is not None else (0, 0)
line_key = (self.price, (self.voucher_id or 0), (self.seat.sorting_rank if self.seat_id else 0), self.pk) line_key = (self.price, (self.voucher_id or 0), (self.seat.sorting_rank if self.seat_id else None), self.pk)
sort_key = subevent_key + category_key + item_key + variation_key + line_key sort_key = subevent_key + category_key + item_key + variation_key + line_key
if self.addon_to_id: if self.addon_to_id:
+1 -10
View File
@@ -23,7 +23,6 @@ import json
from decimal import Decimal from decimal import Decimal
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _, pgettext from django.utils.translation import gettext_lazy as _, pgettext
@@ -150,15 +149,7 @@ class TaxRule(LoggedModel):
rate = models.DecimalField( rate = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=2, decimal_places=2,
validators=[ verbose_name=_("Tax rate")
MaxValueValidator(
limit_value=Decimal("100.00"),
),
MinValueValidator(
limit_value=Decimal("0.00"),
),
],
verbose_name=_("Tax rate"),
) )
price_includes_tax = models.BooleanField( price_includes_tax = models.BooleanField(
verbose_name=_("The configured product prices include the tax amount"), verbose_name=_("The configured product prices include the tax amount"),
+10 -72
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, Union
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models, transaction from django.db import models, transaction
@@ -28,16 +27,14 @@ from django.db.models import F, Q, Sum
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager from django_scopes import ScopedManager
from i18nfield.strings import LazyI18nString
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import User, Voucher from pretix.base.models import Voucher
from pretix.base.services.mail import SendMailException, mail, render_mail from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from ...helpers.format import format_map
from .base import LoggedModel from .base import LoggedModel
from .event import Event, SubEvent from .event import Event, SubEvent
from .items import Item, ItemVariation from .items import Item, ItemVariation
@@ -216,74 +213,15 @@ class WaitingListEntry(LoggedModel):
self.voucher = v self.voucher = v
self.save() self.save()
self.send_mail(
self.event.settings.mail_subject_waiting_list,
self.event.settings.mail_text_waiting_list,
get_email_context(
event=self.event,
waiting_list_entry=self,
waiting_list_voucher=v,
event_or_subevent=self.subevent or self.event,
),
user=user,
auth=auth,
)
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.waitinglist.email.sent',
user: User=None, headers: dict=None, sender: str=None, auth=None, auto_email=True,
attach_other_files: list=None, attach_cached_files: list=None):
"""
Sends an email to the entry's contact address.
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, and ``recipient``
parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
"""
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region): with language(self.locale, self.event.settings.region):
recipient = self.email mail(
self.email,
try: self.event.settings.mail_subject_waiting_list,
email_content = render_mail(template, context) self.event.settings.mail_text_waiting_list,
subject = format_map(subject, context) get_email_context(event=self.event, waiting_list_entry=self),
mail( self.event,
recipient, subject, template, context, locale=self.locale
self.event, )
self.locale,
headers=headers,
sender=sender,
auto_email=auto_email,
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
}
)
@staticmethod @staticmethod
def clean_itemvar(event, item, variation): def clean_itemvar(event, item, variation):
+2 -25
View File
@@ -35,7 +35,6 @@
import copy import copy
import hashlib import hashlib
import itertools import itertools
import json
import logging import logging
import os import os
import re import re
@@ -47,15 +46,12 @@ from collections import OrderedDict
from functools import partial from functools import partial
from io import BytesIO from io import BytesIO
import jsonschema
from arabic_reshaper import ArabicReshaper from arabic_reshaper import ArabicReshaper
from bidi.algorithm import get_display from bidi.algorithm import get_display
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Max, Min from django.db.models import Max, Min
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
@@ -744,9 +740,9 @@ class Renderer:
if o['content'] == 'other' or o['content'] == 'other_i18n': if o['content'] == 'other' or o['content'] == 'other_i18n':
if o['content'] == 'other_i18n': if o['content'] == 'other_i18n':
text = str(LazyI18nString(o.get('text_i18n', {}))) text = str(LazyI18nString(o['text_i18n']))
else: else:
text = o.get('text', '') text = o['text']
def replace(x): def replace(x):
if x.group(1).startswith('itemmeta:'): if x.group(1).startswith('itemmeta:'):
@@ -979,22 +975,3 @@ class Renderer:
output.write(outbuffer) output.write(outbuffer)
outbuffer.seek(0) outbuffer.seek(0)
return outbuffer return outbuffer
@deconstructible
class PdfLayoutValidator:
def __call__(self, value):
if not isinstance(value, dict):
try:
val = json.loads(value)
except ValueError:
raise ValidationError(_('Your layout file is not a valid JSON file.'))
else:
val = value
with open(finders.find('schema/pdf-layout.schema.json'), 'r') as f:
schema = json.loads(f.read())
try:
jsonschema.validate(val, schema)
except jsonschema.ValidationError as e:
e = str(e).replace('%', '%%')
raise ValidationError(_('Your layout file is not a valid layout. Error message: {}').format(e))
+3 -3
View File
@@ -59,10 +59,10 @@ class RelativeDateWrapper:
def date(self, event) -> datetime.date: def date(self, event) -> datetime.date:
from .models import SubEvent from .models import SubEvent
if isinstance(self.data, datetime.datetime): if isinstance(self.data, datetime.date):
return self.data.date()
elif isinstance(self.data, datetime.date):
return self.data return self.data
elif isinstance(self.data, datetime.datetime):
return self.data.date()
else: else:
if self.data.minutes_before is not None: if self.data.minutes_before is not None:
raise ValueError('A minute-based relative datetime can not be used as a date') raise ValueError('A minute-based relative datetime can not be used as a date')
+1 -1
View File
@@ -140,7 +140,7 @@ error_messages = {
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the ' 'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
'product %(base)s.'), 'product %(base)s.'),
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), 'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': _('One of the products you selected can only be bought as an add-on to another product.'), 'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'),
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'), 'bundled_only': _('One of the products you selected can only be bought part of a bundle.'),
'seat_required': _('You need to select a specific seat.'), 'seat_required': _('You need to select a specific seat.'),
'seat_invalid': _('Please select a valid seat.'), 'seat_invalid': _('Please select a valid seat.'),
+10 -235
View File
@@ -19,49 +19,31 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import logging from typing import Any, Dict
from datetime import timedelta
from typing import Any, Dict, Union
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import connection, transaction from django.utils.timezone import override
from django.dispatch import receiver
from django.utils.timezone import now, override
from django.utils.translation import gettext from django.utils.translation import gettext
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.exporter import OrganizerLevelExportMixin from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import LazyLocaleException, language from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken, CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
User, cachedfile_name,
) )
from pretix.base.models.exports import ScheduledOrganizerExport
from pretix.base.services.mail import mail
from pretix.base.services.tasks import ( from pretix.base.services.tasks import (
EventTask, OrganizerTask, ProfiledEventTask, ProfiledOrganizerUserTask, ProfiledEventTask, ProfiledOrganizerUserTask,
) )
from pretix.base.signals import ( from pretix.base.signals import (
periodic_task, register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
) )
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
class ExportError(LazyLocaleException): class ExportError(LazyLocaleException):
pass pass
class ExportEmptyError(ExportError):
pass
@app.task(base=ProfiledEventTask, throws=(ExportError,), bind=True) @app.task(base=ProfiledEventTask, throws=(ExportError,), bind=True)
def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None: def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
def set_progress(val): def set_progress(val):
@@ -74,7 +56,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
file = CachedFile.objects.get(id=fileid) file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale, event.settings.region), override(event.settings.timezone): with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
responses = register_data_exporters.send(event) responses = register_data_exporters.send(event)
for recv, response in responses: for receiver, response in responses:
if not response: if not response:
continue continue
ex = response(event, event.organizer, set_progress) ex = response(event, event.organizer, set_progress)
@@ -124,16 +106,16 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
timezone = organizer.settings.timezone or settings.TIME_ZONE timezone = organizer.settings.timezone or settings.TIME_ZONE
region = organizer.settings.region region = organizer.settings.region
with language(locale, region), override(timezone): with language(locale, region), override(timezone):
if form_data.get('events') is not None and not form_data.get('all_events'): if form_data.get('events') is not None:
if isinstance(form_data['events'][0], str): if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer) events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else: else:
events = allowed_events.filter(pk__in=form_data.get('events'), organizer=organizer) events = allowed_events.filter(pk__in=form_data.get('events'))
else: else:
events = allowed_events.filter(organizer=organizer) events = allowed_events
responses = register_multievent_data_exporters.send(organizer) responses = register_multievent_data_exporters.send(organizer)
for recv, response in responses: for receiver, response in responses:
if not response: if not response:
continue continue
ex = response(events, organizer, set_progress) ex = response(events, organizer, set_progress)
@@ -156,210 +138,3 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
f = ContentFile(data) f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f) file.file.save(cachedfile_name(file, file.filename), f)
return file.pk return file.pk
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
with language(schedule.locale, context.settings.region), override(schedule.tz):
file = CachedFile(web_download=False)
file.date = now()
file.expires = now() + timedelta(hours=24)
file.save()
def _handle_error(msg, soft=False):
context.log_action(
'pretix.event.export.schedule.failed',
data={
'id': schedule.id,
'export_identifier': schedule.export_identifier,
'export_form_data': schedule.export_form_data,
'reason': msg,
'soft': soft,
}
)
if schedule.owner.is_active:
mail(
email=schedule.owner.email,
subject=gettext('Export failed'),
template='pretixbase/email/export_failed.txt',
context={
'configuration_url': config_url,
'reason': msg,
'soft': soft,
},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
locale=schedule.locale,
)
if not soft:
schedule.error_counter += 1
schedule.error_last_message = msg
schedule.save(update_fields=['error_counter', 'error_last_message'])
if not has_permission:
_handle_error(gettext('Permission denied.'))
return
try:
if not exporter:
raise ExportError("Export type not found.")
d = exporter.render(schedule.export_form_data)
if d is None:
raise ExportEmptyError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
filesize = len(data)
if filesize > 20 * 1024 * 1024: # 20 MB
raise ExportError(
gettext('Your exported data exceeded the size limit for scheduled exports.')
)
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
except ExportEmptyError as e:
_handle_error(str(e), soft=True)
except ExportError as e:
_handle_error(str(e), soft=False)
except Exception:
logger.exception("Scheduled export failed.")
try:
retry_func()
except MaxRetriesExceededError:
_handle_error('Internal Error')
else:
schedule.error_counter = 0
schedule.save(update_fields=['error_counter'])
to = [r for r in schedule.mail_additional_recipients.split(",") if r]
cc = [r for r in schedule.mail_additional_recipients_cc.split(",") if r]
bcc = [r for r in schedule.mail_additional_recipients_bcc.split(",") if r]
if to:
# If there is an explicit To, the owner is Cc. Otherwise, the owner is To. Yes, this is
# purely cosmetical and has policital reasons.
cc.append(schedule.owner.email)
else:
to.append(schedule.owner.email)
mail(
email=to,
cc=cc,
bcc=bcc,
subject=schedule.mail_subject,
template=LazyI18nString(schedule.mail_template),
context=get_email_context(event=context) if isinstance(context, Event) else {},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
locale=schedule.locale,
attach_cached_files=[file],
)
context.log_action(
'pretix.event.export.schedule.executed',
data={
'id': schedule.id,
'export_identifier': schedule.export_identifier,
'export_form_data': schedule.export_form_data,
'result_file_size': filesize,
'result_file_name': file.file.name,
}
)
@app.task(base=OrganizerTask, bind=True, max_retries=5, default_retry_delay=120)
def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> None:
schedule = organizer.scheduled_exports.get(pk=schedule)
allowed_events = schedule.owner.get_events_with_permission('can_view_orders')
if schedule.export_form_data.get('events') is not None and not schedule.export_form_data.get('all_events'):
if isinstance(schedule.export_form_data['events'][0], str):
events = allowed_events.filter(slug__in=schedule.export_form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(pk__in=schedule.export_form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(organizer=organizer)
responses = register_multievent_data_exporters.send(organizer)
exporter = None
for recv, response in responses:
if not response:
continue
ex = response(events, organizer)
if ex.identifier == schedule.export_identifier:
exporter = ex
break
has_permission = schedule.owner.is_active
if isinstance(exporter, OrganizerLevelExportMixin):
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
has_permission = False
_run_scheduled_export(
schedule,
organizer,
exporter,
build_absolute_uri(
'control:organizer.export',
kwargs={
'organizer': organizer.slug,
}
) + f'?identifier={schedule.export_identifier}&scheduled={schedule.pk}',
self.retry,
has_permission,
)
@app.task(base=EventTask, bind=True, max_retries=5, default_retry_delay=120)
def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule = event.scheduled_exports.get(pk=schedule)
responses = register_data_exporters.send(event)
exporter = None
for recv, response in responses:
if not response:
continue
ex = response(event, event.organizer)
if ex.identifier == schedule.export_identifier:
exporter = ex
break
has_permission = schedule.owner.is_active and schedule.owner.has_event_permission(event.organizer, event, 'can_view_orders')
_run_scheduled_export(
schedule,
event,
exporter,
build_absolute_uri(
'control:event.orders.export',
kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}
) + f'?identifier={schedule.export_identifier}&scheduled={schedule.pk}',
self.retry,
has_permission,
)
@receiver(signal=periodic_task)
@scopes_disabled()
@transaction.atomic
def run_scheduled_exports(sender, **kwargs):
qs = ScheduledEventExport.objects.filter(
schedule_next_run__lt=now(),
error_counter__lt=5,
).select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked).select_related('event')
for s in qs:
scheduled_event_export.apply_async(kwargs={
'event': s.event_id,
'schedule': s.pk,
})
s.compute_next_run()
s.save(update_fields=['schedule_next_run'])
qs = ScheduledOrganizerExport.objects.filter(
schedule_next_run__lt=now(),
error_counter__lt=5,
).select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked).select_related('organizer')
for s in qs:
scheduled_organizer_export.apply_async(kwargs={
'organizer': s.organizer_id,
'schedule': s.pk,
})
s.compute_next_run()
s.save(update_fields=['schedule_next_run'])
+4 -5
View File
@@ -100,7 +100,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None, position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None, customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None, attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None): plain_text_only=False, no_order_links=False):
""" """
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -211,7 +211,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900] subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900]
signature = "" signature = ""
bcc = list(bcc or []) bcc = []
settings_holder = event or organizer settings_holder = event or organizer
@@ -305,7 +305,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
send_task = mail_send_task.si( send_task = mail_send_task.si(
to=[email] if isinstance(email, str) else list(email), to=[email] if isinstance(email, str) else list(email),
cc=cc,
bcc=bcc, bcc=bcc,
subject=subject, subject=subject,
body=body_plain, body=body_plain,
@@ -358,11 +357,11 @@ class CustomEmail(EmailMultiAlternatives):
@app.task(base=TransactionAwareTask, bind=True, acks_late=True) @app.task(base=TransactionAwareTask, bind=True, acks_late=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str, def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int = None, position: int = None, headers: dict = None, cc: List[str] = None, bcc: List[str] = None, event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None,
invoices: List[int] = None, order: int = None, attach_tickets=False, user=None, invoices: List[int] = None, order: int = None, attach_tickets=False, user=None,
organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None, organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None,
attach_other_files: List[str] = None) -> bool: attach_other_files: List[str] = None) -> bool:
email = CustomEmail(subject, body, sender, to=to, cc=cc, bcc=bcc, headers=headers) email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None: if html is not None:
html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET) html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
html_with_cid, cid_images = replace_images_with_cid_paths(html) html_with_cid, cid_images = replace_images_with_cid_paths(html)
-6
View File
@@ -1068,8 +1068,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
any_payment_failed = True any_payment_failed = True
except Exception: except Exception:
logger.exception('Error during payment attempt') logger.exception('Error during payment attempt')
else:
order.refresh_from_db()
pending_sum = order.pending_sum pending_sum = order.pending_sum
free_order_flow = ( free_order_flow = (
@@ -1343,7 +1341,6 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
class OrderChangeManager: class OrderChangeManager:
error_messages = { error_messages = {
'order_canceled': _('You cannot change a canceled order.'),
'product_without_variation': _('You need to select a variation of the product.'), 'product_without_variation': _('You need to select a variation of the product.'),
'quota': _('The quota {name} does not have enough capacity left to perform the operation.'), 'quota': _('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': _('There is no quota defined that allows this operation.'), 'quota_missing': _('There is no quota defined that allows this operation.'),
@@ -1391,9 +1388,6 @@ class OrderChangeManager:
self._invoice_dirty = False self._invoice_dirty = False
self._invoices = [] self._invoices = []
if order.status == Order.STATUS_CANCELED:
raise OrderError(self.error_messages['order_canceled'])
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]): def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation']) raise OrderError(self.error_messages['product_without_variation'])
-25
View File
@@ -109,31 +109,6 @@ class EventTask(app.Task):
return ret return ret
class OrganizerTask(app.Task):
def __call__(self, *args, **kwargs):
if 'organizer_id' in kwargs:
organizer_id = kwargs.get('organizer_id')
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
del kwargs['organizer_id']
kwargs['organizer'] = organizer
elif 'organizer' in kwargs:
organizer_id = kwargs.get('organizer')
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
kwargs['organizer'] = organizer
else:
args = list(args)
organizer_id = args[0]
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
args[0] = organizer
with scope(organizer=organizer):
ret = super().__call__(*args, **kwargs)
return ret
class OrganizerUserTask(app.Task): class OrganizerUserTask(app.Task):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
organizer_id = kwargs['organizer'] organizer_id = kwargs['organizer']
+5 -5
View File
@@ -206,7 +206,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Ask for attendee names"), label=_("Ask for attendee names"),
help_text=_("Ask for a name for all personalized tickets."), help_text=_("Ask for a name for all tickets which include admission to the event."),
) )
}, },
'attendee_names_required': { 'attendee_names_required': {
@@ -229,10 +229,10 @@ DEFAULTS = {
label=_("Ask for email addresses per ticket"), label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent " help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
"only to that email address. If you enable this option, the system will additionally ask for " "only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every personalized ticket. This might be useful if you want to " "individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders. However, " "obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation by default only to the one primary email address, not to " "pretix will send the order confirmation by default only to the one primary email address, not to "
"the per-attendee addresses. You can however enable this in the email settings."), "the per-attendee addresses. You can however enable this in the E-mail settings."),
) )
}, },
'attendee_emails_required': { 'attendee_emails_required': {
@@ -242,7 +242,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Require email addresses per ticket"), label=_("Require email addresses per ticket"),
help_text=_("Require customers to fill in individual email addresses for all personalized tickets. See the " help_text=_("Require customers to fill in individual e-mail addresses for all admission tickets. See the "
"above option for more details. One email address for the order confirmation will always be " "above option for more details. One email address for the order confirmation will always be "
"required regardless of this setting."), "required regardless of this setting."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
@@ -2574,7 +2574,7 @@ Your {organizer} team"""))
label=_("Attendee data explanation"), label=_("Attendee data explanation"),
widget=I18nTextarea, widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}, widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the questions asked for every personalized product. You can use it e.g. to explain " help_text=_("This text will be shown above the questions asked for every admission product. You can use it e.g. to explain "
"why you need information from them.") "why you need information from them.")
) )
}, },
+2 -4
View File
@@ -218,10 +218,8 @@ class EmailAddressShredder(BaseDataShredder):
o.meta_info = json.dumps(d) o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'email', 'customer']) o.save(update_fields=['meta_info', 'email', 'customer'])
for le in self.event.logentry_set.filter( for le in self.event.logentry_set.filter(action_type__contains="order.email"):
Q(action_type__contains="order.email") | Q(action_type__contains="position.email"), shred_log_fields(le, banlist=['recipient', 'message', 'subject'])
):
shred_log_fields(le, banlist=['recipient', 'message', 'subject', 'full_mail'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"): for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"):
shred_log_fields(le, banlist=['old_email', 'new_email']) shred_log_fields(le, banlist=['old_email', 'new_email'])
@@ -1,12 +0,0 @@
{% load i18n %}
{% trans "Your export failed." %}
{% trans "Reason:" %} {{ reason }}
{% if not soft %}
{% trans "If your export fails five times in a row, it will no longer be sent." %}
{% endif %}
{% trans "Configuration link:" %}
{{ configuration_url }}
@@ -1,18 +0,0 @@
{% load i18n %}
{% with widget.subwidgets.0 as widget %}
{% include widget.template_name %}
{% endwith %}
<div class="row" data-display-dependency-value="custom" data-display-dependency="#{{ widget.subwidgets.0.attrs.id }}">
<br>
<div class="col-sm-6">
{% with widget.subwidgets.1 as widget %}
{% include widget.template_name %}
{% endwith %}
</div>
<div class="col-sm-6">
{% with widget.subwidgets.2 as widget %}
{% include widget.template_name %}
{% endwith %}
</div>
</div>
{% spaceless %}{% for widget in widget.subwidgets %}{% endfor %}{% endspaceless %}
-433
View File
@@ -1,433 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import calendar
from datetime import date, datetime, time, timedelta
from itertools import groupby
from typing import Optional, Tuple
import pytz
from django import forms
from django.core.exceptions import ValidationError
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy, pgettext_lazy
from rest_framework import serializers
from pretix.helpers.daterange import daterange
def _quarter_start(ref_d):
return ref_d.replace(day=1, month=1 + (ref_d.month - 1) // 3 * 3)
def _week_start(ref_d):
return ref_d - timedelta(ref_d.weekday())
REPORTING_DATE_TIMEFRAMES = (
# (identifier, label, start_inclusive, end_inclusive, includes_future, optgroup, describe)
(
'days_today',
pgettext_lazy('reporting_timeframe', 'Today'),
lambda ref_d: ref_d,
lambda ref_d, start_d: start_d,
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_yesterday',
pgettext_lazy('reporting_timeframe', 'Yesterday'),
lambda ref_d: ref_d - timedelta(days=1),
lambda ref_d, start_d: start_d,
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_last7',
pgettext_lazy('reporting_timeframe', 'Last 7 days'),
lambda ref_d: ref_d - timedelta(days=6),
lambda ref_d, start_d: start_d + timedelta(days=6),
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_last14',
pgettext_lazy('reporting_timeframe', 'Last 14 days'),
lambda ref_d: ref_d - timedelta(days=13),
lambda ref_d, start_d: start_d + timedelta(days=13),
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_tomorrow',
pgettext_lazy('reporting_timeframe', 'Tomorrow'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d,
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_next7',
pgettext_lazy('reporting_timeframe', 'Next 7 days'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_next14',
pgettext_lazy('reporting_timeframe', 'Next 14 days'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d + timedelta(days=13),
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'week_this',
pgettext_lazy('reporting_timeframe', 'Current week'),
lambda ref_d: _week_start(ref_d),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_to_date',
pgettext_lazy('reporting_timeframe', 'Current week to date'),
lambda ref_d: _week_start(ref_d),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_previous',
pgettext_lazy('reporting_timeframe', 'Previous week'),
lambda ref_d: _week_start(ref_d) - timedelta(days=7),
lambda ref_d, start_d: start_d + timedelta(days=6),
False,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_next',
pgettext_lazy('reporting_timeframe', 'Next week'),
lambda ref_d: _week_start(ref_d + timedelta(days=7)),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'month_this',
pgettext_lazy('reporting_timeframe', 'Current month'),
lambda ref_d: ref_d.replace(day=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
True,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_to_date',
pgettext_lazy('reporting_timeframe', 'Current month to date'),
lambda ref_d: ref_d.replace(day=1),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_previous',
pgettext_lazy('reporting_timeframe', 'Previous month'),
lambda ref_d: (ref_d.replace(day=1) - timedelta(days=1)).replace(day=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
False,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_next',
pgettext_lazy('reporting_timeframe', 'Next month'),
lambda ref_d: ref_d.replace(day=calendar.monthrange(ref_d.year, ref_d.month)[1]) + timedelta(days=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
True,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'quarter_this',
pgettext_lazy('reporting_timeframe', 'Current quarter'),
lambda ref_d: _quarter_start(ref_d),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
True,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_to_date',
pgettext_lazy('reporting_timeframe', 'Current quarter to date'),
lambda ref_d: _quarter_start(ref_d),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_previous',
pgettext_lazy('reporting_timeframe', 'Previous quarter'),
lambda ref_d: _quarter_start(_quarter_start(ref_d) - timedelta(days=1)),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
False,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_next',
pgettext_lazy('reporting_timeframe', 'Next quarter'),
lambda ref_d: ref_d.replace(
day=calendar.monthrange(ref_d.year, _quarter_start(ref_d).month + 2)[1], month=_quarter_start(ref_d).month + 2
) + timedelta(days=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
True,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'year_this',
pgettext_lazy('reporting_timeframe', 'Current year'),
lambda ref_d: ref_d.replace(day=1, month=1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
True,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_to_date',
pgettext_lazy('reporting_timeframe', 'Current year to date'),
lambda ref_d: ref_d.replace(day=1, month=1),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_previous',
pgettext_lazy('reporting_timeframe', 'Previous year'),
lambda ref_d: (ref_d.replace(day=1, month=1) - timedelta(days=1)).replace(day=1, month=1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
False,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_next',
pgettext_lazy('reporting_timeframe', 'Next year'),
lambda ref_d: ref_d.replace(day=1, month=1, year=ref_d.year + 1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
True,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'future',
pgettext_lazy('reporting_timeframe', 'All future (excluding today)'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: None,
True,
pgettext_lazy('reporting_timeframe', 'Other'),
lambda start_d, end_d: date_format(start_d, "SHORT_DATE_FORMAT") + ' ',
),
(
'past',
pgettext_lazy('reporting_timeframe', 'All past (including today)'),
lambda ref_d: None,
lambda ref_d, start_d: ref_d,
True, # technically false, but only makes sense to have in a selection that also allows the future, otherwise redundant
pgettext_lazy('reporting_timeframe', 'Other'),
lambda start_d, end_d: ' ' + date_format(end_d, "SHORT_DATE_FORMAT"),
),
)
class DateFrameWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/dateframe.html'
def __init__(self, *args, **kwargs):
self.timeframe_choices = kwargs.pop('timeframe_choices')
widgets = (
forms.Select(choices=self.timeframe_choices),
forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'Start')}),
forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'End')}),
)
super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value):
if not value:
return ['unset', None, None]
if '/' in value:
return [
'custom',
date.fromisoformat(value.split('/', 1)[0]),
date.fromisoformat(value.split('/', 1)[-1]),
]
return [value, None, None]
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['required'] = self.timeframe_choices[0][0] == 'unset'
return ctx
def _describe_timeframe(label, start, end, future, describe):
d_start = start(now())
d_end = end(now(), d_start)
details = describe(d_start, d_end)
return f'{label} ({details})'
class DateFrameField(forms.MultiValueField):
default_error_messages = {
**forms.MultiValueField.default_error_messages,
'inconsistent': gettext_lazy('The end date must be after the start date.'),
}
def __init__(self, *args, **kwargs):
include_future_frames = kwargs.pop('include_future_frames')
top_choices = [('custom', gettext_lazy('Custom timeframe'))]
if not kwargs.get('required', True):
top_choices.insert(0, ('unset', pgettext_lazy('reporting_timeframe', 'All time')))
_choices = []
for grouper, group in groupby(REPORTING_DATE_TIMEFRAMES, key=lambda i: i[5]):
options = [
(identifier, _describe_timeframe(label, start, end, future, describe))
for identifier, label, start, end, future, group, describe in group
if include_future_frames or not future
]
if options:
_choices.append((grouper, options))
timeframe_choices = [
('', top_choices)
] + _choices
fields = (
forms.ChoiceField(
choices=timeframe_choices,
required=True
),
forms.DateField(
required=False
),
forms.DateField(
required=False
),
)
if 'widget' not in kwargs:
kwargs['widget'] = DateFrameWidget(timeframe_choices=timeframe_choices)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
def compress(self, data_list):
if not data_list:
return None
if data_list[0] == 'unset':
return None
elif data_list[0] == 'custom':
return f'{data_list[1].isoformat() if data_list[1] else ""}/{data_list[2].isoformat() if data_list[2] else ""}'
else:
return data_list[0]
def has_changed(self, initial, data):
if initial is None:
initial = self.widget.decompress(initial)
return super().has_changed(initial, data)
def clean(self, value):
if not value:
return None
if value[0] == 'custom':
if not value[1] and not value[2]:
raise ValidationError(self.error_messages['incomplete'])
if value[1] and value[2] and self.fields[2].to_python(value[2]) < self.fields[1].to_python(value[1]):
raise ValidationError(self.error_messages['inconsistent'])
return super().clean(value)
class SerializerDateFrameField(serializers.CharField):
def to_internal_value(self, data):
if data is None:
return None
try:
resolve_timeframe_to_dates_inclusive(now(), data, pytz.UTC)
except:
raise ValidationError("Invalid date frame")
def to_representation(self, value):
if value is None:
return None
return value
def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of dates
where the first element ist the first possible date value within the timeframe and the second
element is the last possible date value in the timeframe.
Both returned values may be ``None`` for an unlimited interval.
"""
if isinstance(ref_dt, datetime):
ref_dt = ref_dt.astimezone(timezone).date()
if "/" in frame:
start, end = frame.split("/", 1)
return date.fromisoformat(start) if start else None, date.fromisoformat(end) if end else None
for idf, label, start, end, includes_future, *args in REPORTING_DATE_TIMEFRAMES:
if frame == idf:
d_start = start(ref_dt)
d_end = end(ref_dt, d_start)
return d_start, d_end
raise ValueError(f"Invalid timeframe '{frame}'")
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
where the first element ist the first possible datetime within the timeframe and the second
element is the first possible datetime value *not* in the timeframe.
Both returned values may be ``None`` for an unlimited interval.
"""
d_start, d_end = resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone)
dt_start = make_aware(datetime.combine(d_start, time(0, 0, 0)), timezone) if d_start else None
dt_end = make_aware(datetime.combine(d_end + timedelta(days=1), time(0, 0, 0)), timezone) if d_end else None
return dt_start, dt_end
+5 -21
View File
@@ -19,12 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
from dateutil.rrule import rrulestr
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of # 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>. # the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
@@ -38,6 +32,11 @@ from django.utils.translation import gettext_lazy as _
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
class BanlistValidator: class BanlistValidator:
@@ -102,18 +101,3 @@ class EmailBanlistValidator(BanlistValidator):
banlist = [ banlist = [
settings.PRETIX_EMAIL_NONE_VALUE, settings.PRETIX_EMAIL_NONE_VALUE,
] ]
def multimail_validate(val):
s = val.split(',')
for part in s:
validate_email(part.strip())
return s
class RRuleValidator:
def __call__(self, value):
try:
rrulestr(value)
except Exception:
raise ValidationError("Not a valid rrule.")
+1 -1
View File
@@ -78,7 +78,7 @@ class BaseQuestionsViewMixin:
form.pos = cartpos or orderpos form.pos = cartpos or orderpos
form.show_copy_answers_to_addon_button = form.pos.addon_to and ( form.show_copy_answers_to_addon_button = form.pos.addon_to and (
set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or
(form.pos.addon_to.item.ask_attendee_data and form.pos.item.ask_attendee_data and ( (form.pos.addon_to.item.admission and form.pos.item.admission and (
self.request.event.settings.attendee_names_asked or self.request.event.settings.attendee_names_asked or
self.request.event.settings.attendee_emails_asked or self.request.event.settings.attendee_emails_asked or
self.request.event.settings.attendee_company_asked or self.request.event.settings.attendee_company_asked or
+3 -5
View File
@@ -28,7 +28,7 @@ from celery import states
from celery.result import AsyncResult from celery.result import AsyncResult
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponse, JsonResponse, QueryDict from django.http import HttpResponse, JsonResponse, QueryDict
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.test import RequestFactory from django.test import RequestFactory
@@ -149,8 +149,6 @@ class AsyncMixin:
return redirect(self.get_success_url(value)) return redirect(self.get_success_url(value))
def error(self, exception): def error(self, exception):
if isinstance(exception, PermissionDenied):
raise exception
messages.error(self.request, self.get_error_message(exception)) messages.error(self.request, self.get_error_message(exception))
if "ajax" in self.request.POST or "ajax" in self.request.GET: if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({ return JsonResponse({
@@ -339,8 +337,8 @@ class AsyncPostView(AsyncMixin, View):
depend on the request object unless specifically supported by this class. File upload is currently also depend on the request object unless specifically supported by this class. File upload is currently also
not supported. not supported.
""" """
known_errortypes = ['ValidationError', 'PermissionDenied'] known_errortypes = ['ValidationError']
expected_exceptions = (ValidationError, PermissionDenied) expected_exceptions = (ValidationError,)
task_base = ProfiledEventTask task_base = ProfiledEventTask
def async_set_progress(self, percentage): def async_set_progress(self, percentage):
+1 -8
View File
@@ -51,9 +51,6 @@ from django_scopes.forms import SafeModelMultipleChoiceField
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from ...base.forms import I18nModelForm from ...base.forms import I18nModelForm
from ...helpers.images import (
IMAGE_EXTS, validate_uploaded_file_for_valid_image,
)
# Import for backwards compatibility with okd import paths # Import for backwards compatibility with okd import paths
from ...base.forms.widgets import ( # noqa from ...base.forms.widgets import ( # noqa
@@ -217,16 +214,12 @@ class ExtValidationMixin:
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs) data = super().clean(*args, **kwargs)
if isinstance(data, UploadedFile): if isinstance(data, File):
filename = data.name filename = data.name
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
ext = ext.lower() ext = ext.lower()
if ext not in self.ext_whitelist: if ext not in self.ext_whitelist:
raise forms.ValidationError(_("Filetype not allowed!")) raise forms.ValidationError(_("Filetype not allowed!"))
if ext in IMAGE_EXTS:
validate_uploaded_file_for_valid_image(data)
return data return data
+10 -7
View File
@@ -34,13 +34,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
from decimal import Decimal
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator from django.core.validators import MaxValueValidator, validate_email
from django.db.models import Prefetch, Q, prefetch_related_objects from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import ( from django.forms import (
CheckboxSelectMultiple, formset_factory, inlineformset_factory, CheckboxSelectMultiple, formset_factory, inlineformset_factory,
@@ -66,7 +65,6 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import ( from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
) )
from pretix.base.validators import multimail_validate
from pretix.control.forms import ( from pretix.control.forms import (
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField, MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
SplitDateTimePickerWidget, SplitDateTimePickerWidget,
@@ -137,8 +135,6 @@ class EventWizardBasicsForm(I18nModelForm):
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate " help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
"here in percent. If you have a more complicated tax situation, you can add more tax rates and " "here in percent. If you have a more complicated tax situation, you can add more tax rates and "
"detailed configuration later."), "detailed configuration later."),
max_value=Decimal("100.00"),
min_value=Decimal("0.00"),
required=False required=False
) )
@@ -865,6 +861,13 @@ class InvoiceSettingsForm(SettingsForm):
return data return data
def multimail_validate(val):
s = val.split(',')
for part in s:
validate_email(part.strip())
return s
def contains_web_channel_validate(val): def contains_web_channel_validate(val):
if "web" not in val: if "web" not in val:
raise ValidationError(_("The online shop must be selected to receive these emails.")) raise ValidationError(_("The online shop must be selected to receive these emails."))
@@ -1203,8 +1206,8 @@ class MailSettingsForm(SettingsForm):
'mail_text_resend_link': ['event', 'order'], 'mail_text_resend_link': ['event', 'order'],
'mail_subject_resend_link': ['event', 'order'], 'mail_subject_resend_link': ['event', 'order'],
'mail_subject_resend_link_attendee': ['event', 'order'], 'mail_subject_resend_link_attendee': ['event', 'order'],
'mail_text_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'], 'mail_text_waiting_list': ['event', 'waiting_list_entry'],
'mail_subject_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'], 'mail_subject_waiting_list': ['event', 'waiting_list_entry'],
'mail_text_resend_all_links': ['event', 'orders'], 'mail_text_resend_all_links': ['event', 'orders'],
'mail_subject_resend_all_links': ['event', 'orders'], 'mail_subject_resend_all_links': ['event', 'orders'],
'mail_attach_ical_description': ['event', 'event_or_subevent'], 'mail_attach_ical_description': ['event', 'event_or_subevent'],
-103
View File
@@ -1,103 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from pytz import common_timezones
from pretix.base.models import ScheduledEventExport
from pretix.base.models.exports import ScheduledOrganizerExport
class ScheduledEventExportForm(forms.ModelForm):
class Meta:
model = ScheduledEventExport
fields = ['mail_additional_recipients', 'mail_additional_recipients_cc', 'mail_additional_recipients_bcc',
'mail_subject', 'mail_template', 'schedule_rrule_time', 'locale']
widgets = {
'mail_additional_recipients': forms.TextInput,
'mail_additional_recipients_cc': forms.TextInput,
'mail_additional_recipients_bcc': forms.TextInput,
'schedule_rrule_time': forms.TimeInput(attrs={'class': 'timepickerfield', 'autocomplete': 'off'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
locale_names = dict(settings.LANGUAGES)
self.fields['locale'] = forms.ChoiceField(
label=_('Language'),
choices=[(a, locale_names[a]) for a in self.instance.event.settings.locales]
)
def clean_mail_additional_recipients(self):
d = self.cleaned_data['mail_additional_recipients'].replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError(_('Please enter less than 25 recipients.'))
return d
def clean_mail_additional_recipients_cc(self):
d = self.cleaned_data['mail_additional_recipients_cc'].replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError(_('Please enter less than 25 recipients.'))
return d
def clean_mail_additional_recipients_bcc(self):
d = self.cleaned_data['mail_additional_recipients_bcc'].replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError(_('Please enter less than 25 recipients.'))
return d
class ScheduledOrganizerExportForm(forms.ModelForm):
class Meta:
model = ScheduledOrganizerExport
fields = ['mail_additional_recipients', 'mail_additional_recipients_cc', 'mail_additional_recipients_bcc',
'mail_subject', 'mail_template', 'schedule_rrule_time', 'locale', 'timezone']
widgets = {
'mail_additional_recipients': forms.TextInput,
'mail_additional_recipients_cc': forms.TextInput,
'mail_additional_recipients_bcc': forms.TextInput,
'schedule_rrule_time': forms.TimeInput(attrs={'class': 'timepickerfield', 'autocomplete': 'off'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
locale_names = dict(settings.LANGUAGES)
self.fields['locale'] = forms.ChoiceField(
label=_('Language'),
choices=[(a, locale_names[a]) for a in self.instance.organizer.settings.locales]
)
self.fields['timezone'] = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Timezone"),
)
def clean_mail_additional_recipients(self):
return self.cleaned_data['mail_additional_recipients'].replace(' ', '')
def clean_mail_additional_recipients_cc(self):
return self.cleaned_data['mail_additional_recipients_cc'].replace(' ', '')
def clean_mail_additional_recipients_bcc(self):
return self.cleaned_data['mail_additional_recipients_bcc'].replace(' ', '')
+1 -1
View File
@@ -2264,7 +2264,7 @@ class DeviceFilterForm(FilterForm):
state = forms.ChoiceField( state = forms.ChoiceField(
label=_('Device status'), label=_('Device status'),
choices=[ choices=[
('all', _('All devices')), ('', _('All devices')),
('active', _('Active devices')), ('active', _('Active devices')),
('revoked', _('Revoked devices')) ('revoked', _('Revoked devices'))
], ],
+1 -37
View File
@@ -295,7 +295,6 @@ class ItemCreateForm(I18nModelForm):
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
kwargs.setdefault('initial', {}) kwargs.setdefault('initial', {})
kwargs['initial'].setdefault('admission', True) kwargs['initial'].setdefault('admission', True)
kwargs['initial'].setdefault('personalized', True)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all() self.fields['category'].queryset = self.instance.event.categories.all()
@@ -404,8 +403,6 @@ class ItemCreateForm(I18nModelForm):
self.instance.sales_channels = list(get_all_sales_channels().keys()) self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1 self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
if not self.instance.admission:
self.instance.personalized = False
instance = super().save(*args, **kwargs) instance = super().save(*args, **kwargs)
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'): if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):
@@ -497,7 +494,6 @@ class ItemCreateForm(I18nModelForm):
'internal_name', 'internal_name',
'category', 'category',
'admission', 'admission',
'personalized',
'default_price', 'default_price',
'tax_rule', 'tax_rule',
] ]
@@ -592,33 +588,13 @@ class ItemUpdateForm(I18nModelForm):
'tax_rule', 'tax_rule',
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.") _("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
) )
if d.get('admission'): if d['admission']:
self.add_error( self.add_error(
'admission', 'admission',
_( _(
"Gift card products should not be admission products at the same time." "Gift card products should not be admission products at the same time."
) )
) )
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
_(
"If a valid membership is required, at least one valid membership type needs to be selected."
)
)
if not d.get('admission'):
d['personalized'] = False
if d.get('grant_membership_type'):
if not d['grant_membership_type'].transferable and not d['personalized']:
self.add_error(
'personalized' if d['admission'] else 'admission',
_("Your product grants a non-transferable membership and should therefore be a personalized "
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
"want the membership to be non-personalized, set the membership type to be transferable.")
)
return d return d
def clean_picture(self): def clean_picture(self):
@@ -639,7 +615,6 @@ class ItemUpdateForm(I18nModelForm):
'active', 'active',
'sales_channels', 'sales_channels',
'admission', 'admission',
'personalized',
'description', 'description',
'picture', 'picture',
'default_price', 'default_price',
@@ -817,17 +792,6 @@ class ItemVariationForm(I18nModelForm):
}), }),
} }
def clean(self):
d = super().clean()
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
_(
"If a valid membership is required, at least one valid membership type needs to be selected."
)
)
return d
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit) instance = super().save(commit)
self.meta_fields = [] self.meta_fields = []
+1 -5
View File
@@ -227,10 +227,6 @@ class ExporterForm(forms.Form):
elif isinstance(v, models.QuerySet): elif isinstance(v, models.QuerySet):
data[k] = [m.pk for m in v] data[k] = [m.pk for m in v]
if 'all_events' in self.fields and 'events' in self.fields:
if not data.get('all_events') and not data.get('events'):
raise ValidationError(_('Please select some events.'))
return data return data
@@ -270,7 +266,7 @@ class OtherOperationsForm(forms.Form):
notify = forms.BooleanField( notify = forms.BooleanField(
label=_('Notify user'), label=_('Notify user'),
required=False, required=False,
initial=False, initial=True,
help_text=_( help_text=_(
'Send an email to the customer notifying that their order has been changed.' 'Send an email to the customer notifying that their order has been changed.'
) )
-251
View File
@@ -1,251 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import timedelta
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rrulestr
from django import forms
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
class RRuleForm(forms.Form):
# TODO: calendar.setfirstweekday
freq = forms.ChoiceField(
choices=[
('yearly', _('year(s)')),
('monthly', _('month(s)')),
('weekly', _('week(s)')),
('daily', _('day(s)')),
],
initial='weekly'
)
interval = forms.IntegerField(
label=_('Interval'),
initial=1,
min_value=1,
widget=forms.NumberInput(attrs={'min': '1'})
)
dtstart = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
initial=lambda: now().astimezone(get_current_timezone()).date()
)
end = forms.ChoiceField(
choices=[
('count', ''),
('until', ''),
('forever', ''),
],
initial='count',
widget=forms.RadioSelect
)
count = forms.IntegerField(
label=_('Number of repetitions'),
initial=10
)
until = forms.DateField(
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
label=_('Last date'),
required=True,
initial=lambda: now() + timedelta(days=30)
)
yearly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
yearly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
yearly_bymonth = forms.ChoiceField(
choices=[
(str(i), MONTHS[i]) for i in range(1, 13)
],
required=False
)
monthly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
monthly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
],
required=False,
widget=forms.CheckboxSelectMultiple
)
def parse_weekdays(self, value):
m = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if ',' in value:
return [m.get(a) for a in value.split(',')]
else:
return m.get(value)
def to_rrule(self):
rule_kwargs = {}
rule_kwargs['dtstart'] = self.cleaned_data['dtstart']
rule_kwargs['interval'] = self.cleaned_data['interval']
if self.cleaned_data['freq'] == 'yearly':
freq = YEARLY
if self.cleaned_data['yearly_same'] == "off":
rule_kwargs['bysetpos'] = int(self.cleaned_data['yearly_bysetpos'])
rule_kwargs['byweekday'] = self.parse_weekdays(self.cleaned_data['yearly_byweekday'])
rule_kwargs['bymonth'] = int(self.cleaned_data['yearly_bymonth'])
elif self.cleaned_data['freq'] == 'monthly':
freq = MONTHLY
if self.cleaned_data['monthly_same'] == "off":
rule_kwargs['bysetpos'] = int(self.cleaned_data['monthly_bysetpos'])
rule_kwargs['byweekday'] = self.parse_weekdays(self.cleaned_data['monthly_byweekday'])
elif self.cleaned_data['freq'] == 'weekly':
freq = WEEKLY
if self.cleaned_data['weekly_byweekday']:
rule_kwargs['byweekday'] = [self.parse_weekdays(a) for a in self.cleaned_data['weekly_byweekday']]
elif self.cleaned_data['freq'] == 'daily':
freq = DAILY
if self.cleaned_data['end'] == 'count':
rule_kwargs['count'] = self.cleaned_data['count']
elif self.cleaned_data['end'] == 'until':
rule_kwargs['until'] = self.cleaned_data['until']
return rrule(freq, **rule_kwargs)
@staticmethod
def initial_from_rrule(rule: rrule):
initial = {}
if isinstance(rule, str):
rule = rrulestr(rule)
_rule = rule._original_rule
initial['dtstart'] = rule._dtstart
initial['interval'] = rule._interval
if rule._freq == YEARLY:
initial['freq'] = 'yearly'
initial['yearly_bysetpos'] = _rule.get('bysetpos')
initial['yearly_byweekday'] = _rule.get('byweekday')
initial['yearly_bymonth'] = _rule.get('bymonth')
elif rule._freq == MONTHLY:
initial['freq'] = 'monthly'
initial['monthly_bysetpos'] = _rule.get('bysetpos')
initial['monthly_byweekday'] = _rule.get('byweekday')
elif rule._freq == WEEKLY:
initial['freq'] = 'weekly'
initial['weekly_byweekday'] = _rule.get('byweekday')
elif rule._freq == DAILY:
initial['freq'] = 'daily'
if rule._count:
initial['end'] = 'count'
initial['count'] = rule._count
elif rule._until:
initial['end'] = 'until'
initial['until'] = rule._until
else:
initial['end'] = 'forever'
return initial
+157 -5
View File
@@ -19,15 +19,17 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
from datetime import datetime from datetime import datetime, timedelta
from urllib.parse import urlencode from urllib.parse import urlencode
from django import forms from django import forms
from django.forms import formset_factory from django.forms import formset_factory
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.urls import reverse from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm from pretix.base.forms import I18nModelForm
@@ -37,7 +39,6 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.rrule import RRuleForm
from pretix.helpers.money import change_decimal_field from pretix.helpers.money import change_decimal_field
@@ -439,15 +440,166 @@ class CheckinListFormSet(I18nInlineFormSet):
return form return form
class RRuleFormSetForm(RRuleForm): class RRuleForm(forms.Form):
# TODO: calendar.setfirstweekday
exclude = forms.BooleanField( exclude = forms.BooleanField(
label=_('Exclude these dates instead of adding them.'), label=_('Exclude these dates instead of adding them.'),
required=False required=False
) )
freq = forms.ChoiceField(
choices=[
('yearly', _('year(s)')),
('monthly', _('month(s)')),
('weekly', _('week(s)')),
('daily', _('day(s)')),
],
initial='weekly'
)
interval = forms.IntegerField(
label=_('Interval'),
initial=1,
min_value=1,
widget=forms.NumberInput(attrs={'min': '1'})
)
dtstart = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
initial=lambda: now().date()
)
end = forms.ChoiceField(
choices=[
('count', ''),
('until', ''),
],
initial='count',
widget=forms.RadioSelect
)
count = forms.IntegerField(
label=_('Number of repetitions'),
initial=10
)
until = forms.DateField(
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
label=_('Last date'),
required=True,
initial=lambda: now() + timedelta(days=30)
)
yearly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
yearly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
yearly_bymonth = forms.ChoiceField(
choices=[
(str(i), MONTHS[i]) for i in range(1, 13)
],
required=False
)
monthly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
monthly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
],
required=False,
widget=forms.CheckboxSelectMultiple
)
def parse_weekdays(self, value):
m = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if ',' in value:
return [m.get(a) for a in value.split(',')]
else:
return m.get(value)
RRuleFormSet = formset_factory( RRuleFormSet = formset_factory(
RRuleFormSetForm, RRuleForm,
can_order=False, can_delete=True, extra=1 can_order=False, can_delete=True, extra=1
) )
-10
View File
@@ -315,11 +315,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.organizer.changed': _('The organizer has been changed.'), 'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'), 'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.organizer.footerlinks.changed': _('The footer links have been changed.'), 'pretix.organizer.footerlinks.changed': _('The footer links have been changed.'),
'pretix.organizer.export.schedule.added': _('A scheduled export has been added.'),
'pretix.organizer.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'), 'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'), 'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.webhook.created': _('The webhook has been created.'), 'pretix.webhook.created': _('The webhook has been created.'),
@@ -414,11 +409,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'), 'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'),
'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'), 'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'),
'pretix.event.order.refund.failed': _('Refund {local_id} has failed.'), 'pretix.event.order.refund.failed': _('Refund {local_id} has failed.'),
'pretix.event.export.schedule.added': _('A scheduled export has been added.'),
'pretix.event.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.event.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.event.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.event.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.control.auth.user.created': _('The user has been created.'), 'pretix.control.auth.user.created': _('The user has been created.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'), 'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
@@ -40,7 +40,6 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/rrule.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/variations.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/variations.js" %}"></script>
@@ -82,10 +82,8 @@
<thead> <thead>
<tr> <tr>
<th> <th>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %} <label aria-label="{% trans "select all rows for batch-operation" %}"
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th> </th>
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a> <th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th> <a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
@@ -127,7 +125,7 @@
{% for e in entries %} {% for e in entries %}
<tr> <tr>
<td> <td>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %} {% if "can_change_orders" in request.eventpermset %}
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/> <input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
{% endif %} {% endif %}
</td> </td>
@@ -200,7 +198,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %} {% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-sign-in" aria-hidden="true"></span> <span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Check-In selected attendees" %} {% trans "Check-In selected attendees" %}
@@ -209,8 +207,6 @@
<span class="fa fa-sign-out" aria-hidden="true"></span> <span class="fa fa-sign-out" aria-hidden="true"></span>
{% trans "Check-Out selected attendees" %} {% trans "Check-Out selected attendees" %}
</button> </button>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-danger btn-save" name="revert" value="true"> <button type="submit" class="btn btn-danger btn-save" name="revert" value="true">
<span class="fa fa-trash" aria-hidden="true"></span> <span class="fa fa-trash" aria-hidden="true"></span>
{% trans "Delete all check-ins of selected attendees" %} {% trans "Delete all check-ins of selected attendees" %}
@@ -36,16 +36,12 @@
</div> </div>
<div id="{{ item }}_preview" class="tab-pane mail-preview-group"> <div id="{{ item }}_preview" class="tab-pane mail-preview-group">
{% if request.event %} {% if request.event %}
{% for l, n in settings.LANGUAGES %} {% for l in request.event.settings.locales %}
{% if l in request.event.settings.locales %} <div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endif %}
{% endfor %} {% endfor %}
{% else %} {% else %}
{% for l, n in settings.LANGUAGES %} {% for l in request.organizer.settings.locales %}
{% if l in request.organizer.settings.locales %} <div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
@@ -90,7 +90,7 @@
</div> </div>
</div> </div>
<h4>{% trans "Attendee data (once per personalized ticket)" %}</h4> <h4>{% trans "Attendee data (once per admission ticket)" %}</h4>
{% bootstrap_field sform.attendee_names_asked_required layout="control" %} {% bootstrap_field sform.attendee_names_asked_required layout="control" %}
{% bootstrap_field sform.attendee_emails_asked_required layout="control" %} {% bootstrap_field sform.attendee_emails_asked_required layout="control" %}
@@ -20,19 +20,13 @@
<div class="col-md-9"> <div class="col-md-9">
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on"> <input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-user"></span> <span class="fa fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br> <strong>{% trans "Admission product" %}</strong><br>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event. Every purchase of this product represents one person who is allowed to enter your event.
By default, we will only offer ticket downloads for these products. By default, pretix will only ask for attendee information and offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Only purchases of such products will be considered "attendees" for most statistical
purposes or within some plugins.
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<div class="help-block"> <div class="help-block">
@@ -46,12 +40,12 @@
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" value="" name="{{ form.admission.html_name }}" {% if not form.admission.value %}checked{% endif %}> <input type="radio" value="" name="{{ form.admission.html_name }}" {% if not form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-cube"></span> <span class="fa fa-cube"></span>
<strong>{% trans "Non-admission product" %}</strong> <strong>{% trans "Non-admission product" %}</strong>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
A product that does not represent a person. By default, we will not offer ticket downloads A product that does not represent a person. By default, pretix will not ask for attendee information or offer
(but you can still enable ticket downloads in event settings or product settings). ticket downloads.
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<div class="help-block"> <div class="help-block">
@@ -64,47 +58,6 @@
</div> </div>
</div> </div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-circle-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.category layout="control" %} {% bootstrap_field form.category layout="control" %}
<div class="form-group"> <div class="form-group">
@@ -27,19 +27,13 @@
{% endfor %} {% endfor %}
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on"> <input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-user"></span> <span class="fa fa-fw fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br> <strong>{% trans "Admission product" %}</strong><br>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event. Every purchase of this product represents one person who is allowed to enter your event.
By default, we will only offer ticket downloads for these products. By default, pretix will only ask for attendee information and offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Only purchases of such products will be considered "attendees" for most statistical
purposes or within some plugins.
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<div class="help-block"> <div class="help-block">
@@ -57,8 +51,8 @@
<strong>{% trans "Non-admission product" %}</strong> <strong>{% trans "Non-admission product" %}</strong>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
A product that does not represent a person. By default, we will not offer ticket downloads A product that does not represent a person. By default, pretix will not ask for attendee information or offer
(but you can still enable ticket downloads in event settings or product settings). ticket downloads.
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<div class="help-block"> <div class="help-block">
@@ -71,52 +65,6 @@
</div> </div>
</div> </div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
{% for e in form.errors.personalized %}
<div class="alert alert-danger has-error">
{{ e }}
</div>
{% endfor %}
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-file-text-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.description layout="control" %} {% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.picture layout="control" %} {% bootstrap_field form.picture layout="control" %}
{% bootstrap_field form.require_approval layout="control" %} {% bootstrap_field form.require_approval layout="control" %}
@@ -81,11 +81,7 @@
</td> </td>
<td> <td>
{% if i.admission %} {% if i.admission %}
{% if i.personalized %} <span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket" %}"></span>
<span class="fa fa-id-card-o fa-fw text-muted" data-toggle="tooltip" title="{% trans "Personalized admission ticket" %}"></span>
{% else %}
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket without personalization" %}"></span>
{% endif %}
{% elif i.issue_giftcard %} {% elif i.issue_giftcard %}
<span class="fa fa-gift fa-fw text-muted" data-toggle="tooltip" title="{% trans "Gift card" %}"></span> <span class="fa fa-gift fa-fw text-muted" data-toggle="tooltip" title="{% trans "Gift card" %}"></span>
{% endif %} {% endif %}
@@ -76,7 +76,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<small>{% trans "All personalized products" %}</small> <small>{% trans "All admission products" %}</small>
{% endif %} {% endif %}
</td> </td>
<td class="dnd-container"> <td class="dnd-container">
@@ -184,11 +184,9 @@
<dt>{% trans "Order locale" %}</dt> <dt>{% trans "Order locale" %}</dt>
<dd> <dd>
{{ display_locale }} {{ display_locale }}
{% if "can_change_orders" in request.eventpermset %} <a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs"> <span class="fa fa-edit"></span>
<span class="fa fa-edit"></span> </a>
</a>
{% endif %}
</dd> </dd>
{% if order.status == "n" %} {% if order.status == "n" %}
<dt>{% trans "Expiry date" %}</dt> <dt>{% trans "Expiry date" %}</dt>
@@ -208,11 +206,9 @@
{{ order.customer.identifier }} {{ order.customer.email }} {{ order.customer.identifier }} {{ order.customer.email }}
</a> </a>
{% endif %} {% endif %}
{% if "can_change_orders" in request.eventpermset %} <a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs"> <span class="fa fa-edit"></span>
<span class="fa fa-edit"></span> </a>
</a>
{% endif %}
</dd> </dd>
{% endif %} {% endif %}
<dt>{% trans "Contact email" %}</dt> <dt>{% trans "Contact email" %}</dt>
@@ -221,23 +217,21 @@
{% if order.email and order.email_known_to_work %} {% if order.email and order.email_known_to_work %}
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span> <span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
{% endif %} {% endif %}
{% if "can_change_orders" in request.eventpermset %} <a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs"> <span class="fa fa-edit"></span>
<span class="fa fa-edit"></span> </a>
{% if order.email %}
<a href="{% url "control:event.order.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-envelope-o"></span>
</a> </a>
{% if order.email %} {% if order.status != "c" %}
<a href="{% url "control:event.order.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs"> <form class="form-inline helper-display-inline" method="post"
<span class="fa fa-envelope-o"></span> action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
</a> {% csrf_token %}
{% if order.status != "c" %} <button class="btn btn-default btn-xs">
<form class="form-inline helper-display-inline" method="post" {% trans "Resend link" %}
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> </button>
{% csrf_token %} </form>
<button class="btn btn-default btn-xs">
{% trans "Resend link" %}
</button>
</form>
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</dd> </dd>
@@ -245,11 +239,9 @@
<dt>{% trans "Phone number" %}</dt> <dt>{% trans "Phone number" %}</dt>
<dd> <dd>
{{ order.phone|default_if_none:""|phone_format }} {{ order.phone|default_if_none:""|phone_format }}
{% if "can_change_orders" in request.eventpermset %} <a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs"> <span class="fa fa-edit"></span>
<span class="fa fa-edit"></span> </a>
</a>
{% endif %}
</dd> </dd>
{% endif %} {% endif %}
{% if invoices %} {% if invoices %}
@@ -314,7 +306,7 @@
{% trans "Email invoices" %} {% trans "Email invoices" %}
</a> </a>
{% endif %} {% endif %}
{% if can_generate_invoice and 'can_change_orders' in request.eventpermset %} {% if can_generate_invoice %}
<br/> <br/>
<form class="form-inline helper-display-inline" method="post" <form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.geninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> action="{% url "control:event.order.geninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
@@ -325,7 +317,7 @@
</form> </form>
{% endif %} {% endif %}
</dd> </dd>
{% elif can_generate_invoice and 'can_change_orders' in request.eventpermset %} {% elif can_generate_invoice %}
<dt>{% trans "Invoices" %}</dt> <dt>{% trans "Invoices" %}</dt>
<dd> <dd>
<form class="form-inline helper-display-inline" method="post" <form class="form-inline helper-display-inline" method="post"
@@ -343,11 +335,11 @@
<div class="panel panel-default items"> <div class="panel panel-default items">
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right flip"> <div class="pull-right flip">
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
</a>
{% if 'can_change_orders' in request.eventpermset %} {% if 'can_change_orders' in request.eventpermset %}
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
</a>
&middot; <a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> &middot; <a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
{% trans "Change products" %} {% trans "Change products" %}
@@ -468,12 +460,12 @@
{% endif %} {% endif %}
{% if line.has_questions %} {% if line.has_questions %}
<dl> <dl>
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %} {% if line.item.admission and event.settings.attendee_names_asked %}
<dt>{% trans "Attendee name" %}</dt> <dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %} <dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd> <em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %} {% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %} {% if line.item.admission and event.settings.attendee_emails_asked %}
<dt>{% trans "Attendee email" %}</dt> <dt>{% trans "Attendee email" %}</dt>
<dd> <dd>
{% if line.attendee_email %} {% if line.attendee_email %}
@@ -496,7 +488,7 @@
{% endif %} {% endif %}
</dd> </dd>
{% endif %} {% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %} {% if line.item.admission and event.settings.attendee_company_asked %}
<dt> <dt>
{% trans "Attendee company" %} {% trans "Attendee company" %}
</dt> </dt>
@@ -504,7 +496,7 @@
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %} {% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
</dd> </dd>
{% endif %} {% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %} {% if line.item.admission and event.settings.attendee_addresses_asked %}
<dt> <dt>
{% trans "Attendee address" %} {% trans "Attendee address" %}
</dt> </dt>
@@ -762,7 +754,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if order.payment_refund_sum > 0 and "can_change_orders" in request.eventpermset %} {% if order.payment_refund_sum > 0 %}
<a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default"> <a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
{% trans "Create a refund" %} {% trans "Create a refund" %}
</a> </a>
@@ -950,11 +942,9 @@
{% bootstrap_field comment_form.comment show_help=True show_label=False %} {% bootstrap_field comment_form.comment show_help=True show_label=False %}
{% bootstrap_field comment_form.custom_followup_at %} {% bootstrap_field comment_form.custom_followup_at %}
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %} {% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
{% if "can_change_orders" in request.eventpermset %} <button class="btn btn-default">
<button class="btn btn-default"> {% trans "Update comment" %}
{% trans "Update comment" %} </button>
</button>
{% endif %}
</form> </form>
</div> </div>
</div> </div>
@@ -4,113 +4,35 @@
{% load order_overview %} {% load order_overview %}
{% block title %}{% trans "Data export" %}{% endblock %} {% block title %}{% trans "Data export" %}{% endblock %}
{% block content %} {% block content %}
<h1> <h1>
{% trans "Data export" %} {% trans "Data export" %}
{% if "identifier" in request.GET %}
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
{% endif %}
</h1> </h1>
{% if scheduled %} {% for e in exporters %}
<h2>{% trans "Scheduled exports" %}</h2> <details class="panel panel-default"
<ul class="list-group"> {% if request.GET.identifier == e.identifier or request.POST.exporter == e.identifier %}open{% endif %}>
{% for s in scheduled %} <summary class="panel-heading">
<li class="list-group-item logentry"> <h3 class="panel-title">
<div class="row"> {{ e.verbose_name }}
<div class="col-lg-5 col-md-4 col-xs-12"> <i class="fa fa-angle-down collapse-indicator"></i>
<span class="fa fa-fw fa-folder"></span> </h3>
{{ s.export_verbose_name }} </summary>
<br> <div id="{{ e.identifier }}">
<span class="text-muted"> <div class="panel-body">
<span class="fa fa-fw fa-user"></span> <form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
{{ s.owner.fullname|default:s.owner.email }} method="post" class="form-horizontal" data-asynctask data-asynctask-download
</span> data-asynctask-long>
</div> {% csrf_token %}
<div class="col-lg-5 col-md-6 col-xs-12"> <input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% if s.schedule_next_run %} {% bootstrap_form e.form layout='control' %}
<span class="fa fa-clock-o fa-fw"></span> <button class="btn btn-primary pull-right flip" type="submit">
{% trans "Next run:" %} <span class="icon icon-upload"></span> {% trans "Start export" %}
{{ s.schedule_next_run|date:"SHORT_DATETIME_FORMAT" }} </button>
{% else %} </form>
<span class="fa fa-clock-o fa-fw"></span> </div>
{% trans "No next run scheduled" %} </div>
{% endif %} </details>
{% if s.export_verbose_name == "?" %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Exporter not found" %}
</strong>
{% elif s.error_counter >= 5 %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Disabled due to multiple failures" %}
</strong>
{% elif s.error_counter > 0 %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Failed recently" %}
</strong>
{% endif %}
<span class="text-muted">
<br>
<span class="fa fa-fw fa-envelope-o"></span>
{{ s.mail_subject }}
</span>
</div>
<div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ s.export_identifier }}"/>
<input type="hidden" name="scheduled" value="{{ s.pk }}"/>
{% if s.export_verbose_name != "?" %}
<button type="submit" class="btn btn-default" title="{% trans "Run export now" %}" data-toggle="tooltip">
<span class="fa fa-download"></span>
</button>
<button formaction="{% url "control:event.orders.export.scheduled.run" organizer=request.organizer.slug event=request.event.slug pk=s.pk %}"
type="submit"
title="{% trans "Run export and send via email now. This will not change the next scheduled execution." %}"
data-toggle="tooltip" class="btn btn-default" data-no-asynctask>
<span class="fa fa-play" aria-hidden="true"></span>
</button>
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span>
</a>
{% endif %}
<a href="{% url "control:event.orders.export.scheduled.delete" event=request.event.slug organizer=request.event.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span>
</a>
</form>
</div>
</div>
</li>
{% endfor %}
</ul>
{% if is_paginated %}
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endif %}
{% regroup exporters by category as category_list %}
{% for c, c_ex in category_list %}
{% if c %}
<h2>{{ c }}</h2>
{% else %}
<h2>{% trans "Other exports" %}</h2>
{% endif %}
<div class="list-group large-link-group">
{% for e in c_ex %}
<a class="list-group-item" href="?identifier={{ e.identifier }}">
<h4>
{{ e.verbose_name }}
{% if e.featured %}
<span class="fa fa-star text-success" data-toggle="tooltip"
title="{% trans "Recommended for new users" %}" aria-hidden="true"></span>
{% endif %}
</h4>
{% if e.description %}
<p>
{{ e.description }}
</p>
{% endif %}
</a>
{% endfor %}
</div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
@@ -1,19 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete scheduled export" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete scheduled export" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the scheduled export <strong>{{ export }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:event.orders.export" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}
@@ -1,51 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Data export" %}{% endblock %}
{% block content %}
<h1>
{% trans "Data export" %}
{% if exporter %}
<small>
{{ exporter.verbose_name }}
</small>
{% endif %}
</h1>
{% if exporter.description %}
<p class="help-block">{{ exporter.description }}</p>
{% endif %}
{% if schedule_form %}
{% bootstrap_form_errors schedule_form layout='control' %}
{% endif %}
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ exporter.identifier }}"/>
<fieldset>
<legend>{% trans "Export options" %}</legend>
{% bootstrap_form exporter.form layout='control' %}
</fieldset>
{% if schedule_form %}
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% trans "Save" %}
</button>
</div>
{% else %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-download" aria-hidden="true"></span>
{% trans "Start export" %}
</button>
<button formaction="{{ request.get_full_path }}" name="schedule" value="start" type="submit"
class="btn btn-default btn-alternative" data-no-asynctask>
<span class="fa fa-clock-o" aria-hidden="true"></span>
{% trans "Schedule export" %}
</button>
</div>
{% endif %}
</form>
{% endblock %}
@@ -1,141 +0,0 @@
{% load i18n %}
{% load bootstrap3 %}
{% load captureas %}
<fieldset>
<legend>{% trans "Schedule" %}</legend>
{% bootstrap_field schedule_form.schedule_rrule_time layout='control' %}
{% if schedule_form.timezone %}
{% bootstrap_field schedule_form.timezone layout='control' %}
{% endif %}
{% bootstrap_form_errors rrule_form layout='control' %}
{% bootstrap_field rrule_form.dtstart layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Repetition schedule" %}</label>
<div class="col-md-9">
<div class="form-inline rrule-form">
{% captureas ffield_freq %}
{% bootstrap_field rrule_form.freq layout="inline" %}
{% endcaptureas %}
{% captureas ffield_interval %}
{% bootstrap_field rrule_form.interval layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_bysetpos %}
{% bootstrap_field rrule_form.yearly_bysetpos layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_byweekday %}
{% bootstrap_field rrule_form.yearly_byweekday layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_bymonth %}
{% bootstrap_field rrule_form.yearly_bymonth layout="inline" %}
{% endcaptureas %}
{% captureas ffield_monthly_bysetpos %}
{% bootstrap_field rrule_form.monthly_bysetpos layout="inline" %}
{% endcaptureas %}
{% captureas ffield_monthly_byweekday %}
{% bootstrap_field rrule_form.monthly_byweekday layout="inline" %}
{% endcaptureas %}
{% captureas ffield_count %}
{% bootstrap_field rrule_form.count layout="inline" %}
{% endcaptureas %}
{% captureas ffield_until %}
{% bootstrap_field rrule_form.until layout="inline" %}
{% endcaptureas %}
{% blocktrans trimmed with freq=ffield_freq interval=ffield_interval start=ffield_dtstart %}
Repeat every {{ interval }} {{ freq }}
{% endblocktrans %}<br>
<div class="repeat-yearly">
<div class="radio">
<label>
{{ rrule_form.yearly_same.0 }}
{% trans "At the same date every year" %}
</label><br>
<label>
{{ rrule_form.yearly_same.1 }}
{% blocktrans trimmed with setpos=ffield_yearly_bysetpos weekday=ffield_yearly_byweekday month=ffield_yearly_bymonth %}
On the {{ setpos }} {{ weekday }} of {{ month }}
{% endblocktrans %}<br>
</label>
</div>
</div>
<div class="repeat-monthly">
<div class="radio">
<label>
{{ rrule_form.monthly_same.0 }}
{% trans "At the same date every month" %}
</label><br>
<label>
{{ rrule_form.monthly_same.1 }}
{% blocktrans trimmed with setpos=ffield_monthly_bysetpos weekday=ffield_monthly_byweekday %}
On the {{ setpos }} {{ weekday }}
{% endblocktrans %}<br>
</label>
</div>
</div>
<div class="repeat-weekly">
{% bootstrap_field rrule_form.weekly_byweekday layout="inline" %}
</div>
<div class="repeat-until">
<div class="radio">
<label>
{{ rrule_form.end.0 }}
{% blocktrans trimmed with count=ffield_count %}
Repeat for {{ count }} times
{% endblocktrans %}
</label><br>
<label>
{{ rrule_form.end.1 }}
{% blocktrans trimmed with until=ffield_until %}
Repeat until {{ until }}
{% endblocktrans %}<br>
</label><br>
<label>
{{ rrule_form.end.2 }}
{% blocktrans trimmed %}
Forever
{% endblocktrans %}<br>
</label>
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Email" %}</legend>
<div class="alert alert-info">
{% trans "Every time your schedule is executed, the report will be sent via email." %}
{% trans "Please note the following limitations:" %}
<ul>
<li>
{% trans "Email is not a strongly encrypted medium. We only recommend using this for exports that output e.g. statistical data, not for reports that include sensitive personal data." %}
</li>
<li>
{% trans "Email is not made for large files. If your export ends up to be larger than 20 megabytes, it will not be sent." %}
</li>
</ul>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="id_schedule-owner">{% trans "Owner" %}</label>
<div class="col-md-9">
<input type="text" name="schedule-owner" value="{{ schedule_form.instance.owner.email }}" disabled
class="form-control" title=""
id="id_schedule-owner">
<div class="help-block">
{% trans "The export will be performed using the owner's permission level, i.e. if the owner loses access to the data, the report will stop." %}
{% trans "The owner will receive the result as well as any error messages." %}
{% trans "The additional recipients you add below will only receive an email if the report was successful." %}
{% trans "All recipients of the export will be able to see who the owner of the report is." %}
</div>
</div>
</div>
{% bootstrap_field schedule_form.mail_additional_recipients layout='control' %}
{% bootstrap_field schedule_form.mail_additional_recipients_cc layout='control' %}
{% bootstrap_field schedule_form.mail_additional_recipients_bcc layout='control' %}
{% bootstrap_field schedule_form.locale layout='control' %}
{% bootstrap_field schedule_form.mail_subject layout='control' %}
{% bootstrap_field schedule_form.mail_template layout='control' %}
</fieldset>
@@ -1,110 +1,37 @@
{% extends "pretixcontrol/organizers/base.html" %} {% extends "pretixcontrol/event/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load order_overview %} {% load order_overview %}
{% block title %}{% trans "Data export" %}{% endblock %} {% block title %}{% trans "Data export" %}{% endblock %}
{% block content %} {% block content %}
<h1> <h1>
{% trans "Data export" %} {% trans "Data export" %}
{% if "identifier" in request.GET %}
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
{% endif %}
</h1> </h1>
{% if scheduled %} {% for e in exporters %}
<h2>{% trans "Scheduled exports" %}</h2> <details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
<ul class="list-group"> <summary class="panel-heading">
{% for s in scheduled %} <h3 class="panel-title">
<li class="list-group-item logentry"> {{ e.verbose_name }}
<div class="row"> <i class="fa fa-angle-down collapse-indicator"></i>
<div class="col-lg-5 col-md-4 col-xs-12"> </h3>
<span class="fa fa-fw fa-folder"></span> </summary>
{{ s.export_verbose_name }} <div id="{{ e.identifier }}">
<br> <div class="panel-body">
<span class="text-muted"> <form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}"
<span class="fa fa-fw fa-user"></span> method="post" class="form-horizontal" data-asynctask data-asynctask-download
{{ s.owner.fullname|default:s.owner.email }} data-asynctask-long>
</span> {% csrf_token %}
</div> <input type="hidden" name="exporter" value="{{ e.identifier }}" />
<div class="col-lg-5 col-md-6 col-xs-12"> {% bootstrap_form e.form layout='control' %}
{% if s.schedule_next_run %} <button class="btn btn-primary pull-right flip" type="submit">
<span class="fa fa-clock-o fa-fw"></span> <span class="icon icon-upload"></span> {% trans "Start export" %}
{% trans "Next run:" %} </button>
{{ s.schedule_next_run|date:"SHORT_DATETIME_FORMAT" }} </form>
{% else %} </div>
<span class="fa fa-clock-o fa-fw"></span> </div>
{% trans "No next run scheduled" %} </details>
{% endif %}
{% if s.export_verbose_name == "?" %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Exporter not found" %}
</strong>
{% elif s.error_counter >= 5 %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Disabled due to multiple failures" %}
</strong>
{% elif s.error_counter > 0 %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Failed recently" %}
</strong>
{% endif %}
<span class="text-muted">
<br>
<span class="fa fa-fw fa-envelope-o"></span>
{{ s.mail_subject }}
</span>
</div>
<div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ s.export_identifier }}"/>
<input type="hidden" name="scheduled" value="{{ s.pk }}"/>
{% if s.export_verbose_name != "?" %}
<button type="submit" class="btn btn-default" title="{% trans "Run export now and download result" %}" data-toggle="tooltip">
<span class="fa fa-download"></span>
</button>
<button formaction="{% url "control:organizer.export.scheduled.run" organizer=request.organizer.slug pk=s.pk %}"
type="submit"
title="{% trans "Run export and send via email now. This will not change the next scheduled execution." %}"
data-toggle="tooltip" class="btn btn-default" data-no-asynctask>
<span class="fa fa-play" aria-hidden="true"></span>
</button>
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span>
</a>
{% endif %}
<a href="{% url "control:organizer.export.scheduled.delete" organizer=request.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span>
</a>
</form>
</div>
</div>
</li>
{% endfor %}
</ul>
{% if is_paginated %}
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endif %}
{% regroup exporters by category as category_list %}
{% for c, c_ex in category_list %}
{% if c %}
<h2>{{ c }}</h2>
{% else %}
<h2>{% trans "Other exports" %}</h2>
{% endif %}
<div class="list-group large-link-group">
{% for e in c_ex %}
<a class="list-group-item" href="?identifier={{ e.identifier }}">
<h4>{{ e.verbose_name }}</h4>
{% if e.description %}
<p>
{{ e.description }}
</p>
{% endif %}
</a>
{% endfor %}
</div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
@@ -1,19 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete scheduled export" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete scheduled export" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the scheduled export <strong>{{ export }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.export" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}
@@ -1,52 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Data export" %}{% endblock %}
{% block content %}
<h1>
{% trans "Data export" %}
{% if exporter %}
<small>
{{ exporter.verbose_name }}
</small>
{% endif %}
</h1>
{% if exporter.description %}
<p class="help-block">{{ exporter.description }}</p>
{% endif %}
{% if schedule_form %}
{% bootstrap_form_errors schedule_form layout='control' %}
{% endif %}
<form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ exporter.identifier }}"/>
<fieldset>
<legend>{% trans "Export options" %}</legend>
{% bootstrap_form exporter.form layout='control' %}
</fieldset>
{% if schedule_form %}
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% trans "Save" %}
</button>
</div>
{% else %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-download" aria-hidden="true"></span>
{% trans "Start export" %}
</button>
<button formaction="{{ request.get_full_path }}" name="schedule" value="start" type="submit"
class="btn btn-default btn-alternative" data-no-asynctask>
<span class="fa fa-clock-o" aria-hidden="true"></span>
{% trans "Schedule export" %}
</button>
</div>
{% endif %}
</form>
{% endblock %}
@@ -458,9 +458,7 @@
</form> </form>
</div> </div>
</div> </div>
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script> <script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script> <script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only"> <img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
-6
View File
@@ -207,10 +207,6 @@ urlpatterns = [
re_path(r'^organizer/(?P<organizer>[^/]+)/logs', organizer.LogView.as_view(), name='organizer.log'), re_path(r'^organizer/(?P<organizer>[^/]+)/logs', organizer.LogView.as_view(), name='organizer.log'),
re_path(r'^organizer/(?P<organizer>[^/]+)/export/$', organizer.ExportView.as_view(), name='organizer.export'), re_path(r'^organizer/(?P<organizer>[^/]+)/export/$', organizer.ExportView.as_view(), name='organizer.export'),
re_path(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'), re_path(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'),
re_path(r'^organizer/(?P<organizer>[^/]+)/export/(?P<pk>[^/]+)/run$', organizer.RunScheduledExportView.as_view(),
name='organizer.export.scheduled.run'),
re_path(r'^organizer/(?P<organizer>[^/]+)/export/(?P<pk>[^/]+)/delete$', organizer.DeleteScheduledExportView.as_view(),
name='organizer.export.scheduled.delete'),
re_path(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'), re_path(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
re_path(r'^events/$', main.EventList.as_view(), name='events'), re_path(r'^events/$', main.EventList.as_view(), name='events'),
re_path(r'^events/add$', main.EventWizard.as_view(), name='events.add'), re_path(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
@@ -390,8 +386,6 @@ urlpatterns = [
re_path(r'^orders/import/$', orderimport.ImportView.as_view(), name='event.orders.import'), re_path(r'^orders/import/$', orderimport.ImportView.as_view(), name='event.orders.import'),
re_path(r'^orders/import/(?P<file>[^/]+)/$', orderimport.ProcessView.as_view(), name='event.orders.import.process'), re_path(r'^orders/import/(?P<file>[^/]+)/$', orderimport.ProcessView.as_view(), name='event.orders.import.process'),
re_path(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'), re_path(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
re_path(r'^orders/export/(?P<pk>[^/]+)/run$', orders.RunScheduledExportView.as_view(), name='event.orders.export.scheduled.run'),
re_path(r'^orders/export/(?P<pk>[^/]+)/delete$', orders.DeleteScheduledExportView.as_view(), name='event.orders.export.scheduled.delete'),
re_path(r'^orders/export/do$', orders.ExportDoView.as_view(), name='event.orders.export.do'), re_path(r'^orders/export/do$', orders.ExportDoView.as_view(), name='event.orders.export.do'),
re_path(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'), re_path(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'), re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
-6
View File
@@ -353,12 +353,6 @@ class Recover(TemplateView):
user.save() user.save()
messages.success(request, _('You can now login using your new password.')) messages.success(request, _('You can now login using your new password.'))
user.log_action('pretix.control.auth.user.forgot_password.recovered') user.log_action('pretix.control.auth.user.forgot_password.recovered')
has_redis = settings.HAS_REDIS
if has_redis:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
rc.delete('pretix_pwreset_%s' % user.id)
return redirect('control:auth.login') return redirect('control:auth.login')
else: else:
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
+3 -10
View File
@@ -34,7 +34,6 @@
import dateutil.parser import dateutil.parser
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Exists, Max, OuterRef, Prefetch, Subquery from django.db.models import Exists, Max, OuterRef, Prefetch, Subquery
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
@@ -43,7 +42,7 @@ from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware, now from django.utils.timezone import is_aware, make_aware, now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView from django.views.generic import DeleteView, ListView
from pytz import UTC from pytz import UTC
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
@@ -57,7 +56,6 @@ from pretix.control.forms.filter import (
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import CreateView, PaginationMixin, UpdateView from pretix.control.views import CreateView, PaginationMixin, UpdateView
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
@@ -173,7 +171,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView): class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
template_name = 'pretixcontrol/organizers/device_bulk_edit.html' template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
permission = ('can_change_orders', 'can_checkin_orders') permission = 'can_change_orders'
context_object_name = 'device' context_object_name = 'device'
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -183,16 +181,11 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
def get_queryset(self): def get_queryset(self):
return super().get_queryset().prefetch_related(None).order_by() return super().get_queryset().prefetch_related(None).order_by()
def get_error_url(self):
return self.get_success_url(None)
@transaction.atomic() @transaction.atomic()
def async_post(self, request, *args, **kwargs): def async_post(self, request, *args, **kwargs):
self.list = get_object_or_404(request.event.checkin_lists.all(), pk=kwargs.get("list")) self.list = get_object_or_404(request.event.checkin_lists.all(), pk=kwargs.get("list"))
positions = self.get_queryset() positions = self.get_queryset()
if request.POST.get('revert') == 'true': if request.POST.get('revert') == 'true':
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders', request=request):
raise PermissionDenied()
for op in positions: for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING): if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
Checkin.objects.filter(position=op, list=self.list).delete() Checkin.objects.filter(position=op, list=self.list).delete()
@@ -395,7 +388,7 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
return super().form_invalid(form) return super().form_invalid(form)
class CheckinListDelete(EventPermissionRequiredMixin, CompatDeleteView): class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
model = CheckinList model = CheckinList
template_name = 'pretixcontrol/checkin/list_delete.html' template_name = 'pretixcontrol/checkin/list_delete.html'
permission = 'can_change_event_settings' permission = 'can_change_event_settings'
+2 -2
View File
@@ -35,6 +35,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.edit import DeleteView
from pretix.base.models import CartPosition, Discount from pretix.base.models import CartPosition, Discount
from pretix.control.forms.discounts import DiscountForm from pretix.control.forms.discounts import DiscountForm
@@ -44,11 +45,10 @@ from pretix.control.permissions import (
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
from ...base.channels import get_all_sales_channels from ...base.channels import get_all_sales_channels
from ...helpers.compat import CompatDeleteView
from . import CreateView, PaginationMixin, UpdateView from . import CreateView, PaginationMixin, UpdateView
class DiscountDelete(EventPermissionRequiredMixin, CompatDeleteView): class DiscountDelete(EventPermissionRequiredMixin, DeleteView):
model = Discount model = Discount
template_name = 'pretixcontrol/items/discount_delete.html' template_name = 'pretixcontrol/items/discount_delete.html'
permission = 'can_change_items' permission = 'can_change_items'
+2 -3
View File
@@ -58,7 +58,7 @@ from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from django.views.generic import FormView, ListView from django.views.generic import DeleteView, FormView, ListView
from django.views.generic.base import TemplateView, View from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -94,7 +94,6 @@ from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota, Item, ItemCategory, ItemMetaProperty, Question, Quota,
) )
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
from ...helpers.compat import CompatDeleteView
from ...helpers.format import format_map from ...helpers.format import format_map
from ..logdisplay import OVERVIEW_BANLIST from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView from . import CreateView, PaginationMixin, UpdateView
@@ -1244,7 +1243,7 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
return super().form_invalid(form) return super().form_invalid(form)
class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, CompatDeleteView): class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, DeleteView):
model = TaxRule model = TaxRule
template_name = 'pretixcontrol/event/tax_delete.html' template_name = 'pretixcontrol/event/tax_delete.html'
permission = 'can_change_event_settings' permission = 'can_change_event_settings'
+7 -6
View File
@@ -56,6 +56,7 @@ from django.utils.translation import gettext, gettext_lazy as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import DeleteView
from django_countries.fields import Country from django_countries.fields import Country
from pretix.api.serializers.item import ( from pretix.api.serializers.item import (
@@ -84,7 +85,6 @@ from pretix.control.signals import item_forms, item_formsets
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
from ...base.channels import get_all_sales_channels from ...base.channels import get_all_sales_channels
from ...helpers.compat import CompatDeleteView
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
@@ -189,8 +189,9 @@ def reorder_items(request, organizer, event):
return HttpResponse() return HttpResponse()
class CategoryDelete(EventPermissionRequiredMixin, CompatDeleteView): class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
model = ItemCategory model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category_delete.html' template_name = 'pretixcontrol/items/category_delete.html'
permission = 'can_change_items' permission = 'can_change_items'
context_object_name = 'category' context_object_name = 'category'
@@ -522,7 +523,7 @@ def reorder_questions(request, organizer, event):
return HttpResponse() return HttpResponse()
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView): class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
model = Question model = Question
template_name = 'pretixcontrol/items/question_delete.html' template_name = 'pretixcontrol/items/question_delete.html'
permission = 'can_change_items' permission = 'can_change_items'
@@ -1089,7 +1090,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
return super().form_invalid(form) return super().form_invalid(form)
class QuotaDelete(EventPermissionRequiredMixin, CompatDeleteView): class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
model = Quota model = Quota
template_name = 'pretixcontrol/items/quota_delete.html' template_name = 'pretixcontrol/items/quota_delete.html'
permission = 'can_change_items' permission = 'can_change_items'
@@ -1203,7 +1204,7 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
initial['tax_rule'] = trs[0] initial['tax_rule'] = trs[0]
if self.copy_from: if self.copy_from:
fields = ('name', 'internal_name', 'category', 'admission', 'personalized', 'default_price', 'tax_rule') fields = ('name', 'internal_name', 'category', 'admission', 'default_price', 'tax_rule')
for f in fields: for f in fields:
initial[f] = getattr(self.copy_from, f) initial[f] = getattr(self.copy_from, f)
initial['copy_from'] = self.copy_from initial['copy_from'] = self.copy_from
@@ -1463,7 +1464,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
return f return f
class ItemDelete(EventPermissionRequiredMixin, CompatDeleteView): class ItemDelete(EventPermissionRequiredMixin, DeleteView):
model = Item model = Item
template_name = 'pretixcontrol/item/delete.html' template_name = 'pretixcontrol/item/delete.html'
permission = 'can_change_items' permission = 'can_change_items'
-3
View File
@@ -127,9 +127,6 @@ class OAuthApplicationDeleteView(ApplicationDelete):
self.object.save() self.object.save()
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)
def form_valid(self, form):
return self.delete(self.request, self.args, self.kwargs)
class AuthorizationListView(ListView): class AuthorizationListView(ListView):
template_name = 'pretixcontrol/oauth/authorized.html' template_name = 'pretixcontrol/oauth/authorized.html'
+34 -207
View File
@@ -77,7 +77,7 @@ from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedFile, CachedTicket, Checkin, Invoice, CachedCombinedTicket, CachedFile, CachedTicket, Checkin, Invoice,
InvoiceAddress, Item, ItemVariation, LogEntry, Order, QuestionAnswer, InvoiceAddress, Item, ItemVariation, LogEntry, Order, QuestionAnswer,
Quota, ScheduledEventExport, generate_secret, Quota, generate_secret,
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund, CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
@@ -87,7 +87,7 @@ from pretix.base.payment import PaymentException
from pretix.base.secrets import assign_ticket_secret from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets from pretix.base.services import tickets
from pretix.base.services.cancelevent import cancel_event from pretix.base.services.cancelevent import cancel_event
from pretix.base.services.export import export, scheduled_event_export from pretix.base.services.export import export
from pretix.base.services.invoices import ( from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task, generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
invoice_qualified, regenerate_invoice, invoice_qualified, regenerate_invoice,
@@ -111,7 +111,6 @@ from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.exports import ScheduledEventExportForm
from pretix.control.forms.filter import ( from pretix.control.forms.filter import (
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm, EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
RefundFilterForm, RefundFilterForm,
@@ -123,11 +122,9 @@ from pretix.control.forms.orders import (
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm, OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm, OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
) )
from pretix.control.forms.rrule import RRuleForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import order_search_forms from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -386,8 +383,8 @@ class OrderDetail(OrderView):
p.has_questions = ( p.has_questions = (
p.additional_fields or p.additional_fields or
(p.item.ask_attendee_data and self.request.event.settings.attendee_names_asked) or (p.item.admission and self.request.event.settings.attendee_names_asked) or
(p.item.ask_attendee_data and self.request.event.settings.attendee_emails_asked) or (p.item.admission and self.request.event.settings.attendee_emails_asked) or
p.item.questions.all() p.item.questions.all()
) )
p.cache_answers() p.cache_answers()
@@ -2240,40 +2237,20 @@ class OrderGo(EventPermissionRequiredMixin, View):
class ExportMixin: class ExportMixin:
@cached_property @cached_property
def exporters(self): def exporters(self):
exporters = []
responses = register_data_exporters.send(self.request.event) responses = register_data_exporters.send(self.request.event)
return sorted( id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
[response(self.request.event, self.request.organizer) for r, response in responses if response], for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower()) if id and ex.identifier != id:
)
@cached_property
def exporter(self):
id = self.request.GET.get("identifier") or self.request.POST.get("exporter") or self.request.GET.get("exporter")
if not id:
return None
for ex in self.exporters:
if id != ex.identifier:
continue continue
if self.scheduled: # Use form parse cycle to generate useful defaults
initial = self.scheduled.export_form_data test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier) test_form.is_valid()
test_form.fields = ex.export_form_fields initial = {
for k in initial: k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
if initial[k] and k in test_form.fields: }
try:
initial[k] = test_form.fields[k].to_python(initial[k])
except Exception:
pass
else:
# Use form parse cycle to generate useful defaults
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
test_form.is_valid()
initial = {
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
}
ex.form = ExporterForm( ex.form = ExporterForm(
data=(self.request.POST if self.request.method == 'POST' else None), data=(self.request.POST if self.request.method == 'POST' else None),
@@ -2281,27 +2258,12 @@ class ExportMixin:
initial=initial initial=initial
) )
ex.form.fields = ex.export_form_fields ex.form.fields = ex.export_form_fields
return ex exporters.append(ex)
return exporters
def get_scheduled_queryset(self):
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
request=self.request):
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
else:
qs = self.request.event.scheduled_exports
return qs.select_related('owner').order_by('export_identifier', 'schedule_next_run')
@cached_property
def scheduled(self):
if "scheduled" in self.request.POST:
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.POST.get("scheduled"))
elif "scheduled" in self.request.GET:
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.GET.get("scheduled"))
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['exporters'] = self.exporters ctx['exporters'] = self.exporters
ctx['exporter'] = self.exporter
return ctx return ctx
@@ -2309,7 +2271,7 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
permission = 'can_view_orders' permission = 'can_view_orders'
known_errortypes = ['ExportError'] known_errortypes = ['ExportError']
task = export task = export
template_name = 'pretixcontrol/orders/export_form.html' template_name = 'pretixcontrol/orders/export.html'
def get_success_message(self, value): def get_success_message(self, value):
return None return None
@@ -2326,6 +2288,16 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
def get_check_url(self, task_id, ajax): def get_check_url(self, task_id, ajax):
return self.request.path + '?async_id=%s&exporter=%s' % (task_id, self.exporter.identifier) + ('&ajax=1' if ajax else '') return self.request.path + '?async_id=%s&exporter=%s' % (task_id, self.exporter.identifier) + ('&ajax=1' if ajax else '')
@cached_property
def exporter(self):
if self.request.method == "POST":
identifier = self.request.POST.get("exporter")
else:
identifier = self.request.GET.get("exporter")
for ex in self.exporters:
if ex.identifier == identifier:
return ex
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY: if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request) return self.get_result(request)
@@ -2339,165 +2311,20 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
'organizer': self.request.event.organizer.slug 'organizer': self.request.event.organizer.slug
})) }))
if self.scheduled: if not self.exporter.form.is_valid():
data = self.scheduled.export_form_data messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
else: return self.get(request, *args, **kwargs)
if not self.exporter.form.is_valid():
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(request, *args, **kwargs)
data = self.exporter.form.cleaned_data
cf = CachedFile(web_download=True, session_key=request.session.session_key) cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now() cf.date = now()
cf.expires = now() + timedelta(hours=24) cf.expires = now() + timedelta(hours=24)
cf.save() cf.save()
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, data) return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data)
class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView): class ExportView(EventPermissionRequiredMixin, ExportMixin, TemplateView):
permission = 'can_view_orders' permission = 'can_view_orders'
paginate_by = 25 template_name = 'pretixcontrol/orders/export.html'
context_object_name = 'scheduled'
def get_template_names(self):
if self.exporter:
return ['pretixcontrol/orders/export_form.html']
return ['pretixcontrol/orders/export.html']
@transaction.atomic()
def post(self, request, *args, **kwargs):
if request.POST.get("schedule") == "save":
if self.exporter.form.is_valid() and self.rrule_form.is_valid() and self.schedule_form.is_valid():
self.schedule_form.instance.export_identifier = self.exporter.identifier
self.schedule_form.instance.export_form_data = self.exporter.form.cleaned_data
self.schedule_form.instance.schedule_rrule = str(self.rrule_form.to_rrule())
self.schedule_form.instance.error_counter = 0
self.schedule_form.instance.error_last_message = None
self.schedule_form.instance.compute_next_run()
self.schedule_form.instance.save()
if self.schedule_form.instance.schedule_next_run:
messages.success(
request,
_('Your export schedule has been saved. The next export will start around {datetime}.').format(
datetime=date_format(self.schedule_form.instance.schedule_next_run, 'SHORT_DATETIME_FORMAT')
)
)
else:
messages.warning(request, _('Your export schedule has been saved, but no next export is planned.'))
self.request.event.log_action(
'pretix.event.export.schedule.changed' if self.scheduled else 'pretix.event.export.schedule.added',
user=self.request.user, data={
'id': self.schedule_form.instance.id,
'export_identifier': self.exporter.identifier,
'export_form_data': self.exporter.form.cleaned_data,
'schedule_rrule': self.schedule_form.instance.schedule_rrule,
**self.schedule_form.cleaned_data,
}
)
return redirect(reverse('control:event.orders.export', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}))
else:
return super().get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
@cached_property
def rrule_form(self):
if self.scheduled:
initial = RRuleForm.initial_from_rrule(self.scheduled.schedule_rrule)
else:
initial = {}
return RRuleForm(
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
prefix="rrule",
initial=initial
)
@cached_property
def schedule_form(self):
instance = self.scheduled or ScheduledEventExport(
event=self.request.event,
owner=self.request.user,
)
if not self.scheduled:
initial = {
"mail_subject": gettext("Export: {title}").format(title=self.exporter.verbose_name),
"mail_template": gettext("Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
name=str(self.request.event.name)
),
"schedule_rrule_time": time(4, 0, 0),
}
else:
initial = {}
return ScheduledEventExportForm(
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
prefix="schedule",
instance=instance,
initial=initial,
)
def get_queryset(self):
return self.get_scheduled_queryset()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if "schedule" in self.request.POST or self.scheduled:
ctx['schedule_form'] = self.schedule_form
ctx['rrule_form'] = self.rrule_form
elif not self.exporter:
for s in ctx['scheduled']:
try:
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][0].verbose_name
except IndexError:
s.export_verbose_name = "?"
return ctx
class DeleteScheduledExportView(EventPermissionRequiredMixin, ExportMixin, CompatDeleteView):
permission = 'can_view_orders'
template_name = 'pretixcontrol/orders/export_delete.html'
context_object_name = 'export'
def get_queryset(self):
return self.get_scheduled_queryset()
def get_success_url(self):
return reverse('control:event.orders.export', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
})
@transaction.atomic()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
self.request.event.log_action('pretix.event.export.schedule.deleted', user=self.request.user, data={
'id': self.object.id,
})
return redirect(self.get_success_url())
class RunScheduledExportView(EventPermissionRequiredMixin, ExportMixin, View):
def post(self, request, *args, **kwargs):
s = get_object_or_404(self.get_scheduled_queryset(), pk=kwargs.get('pk'))
scheduled_event_export.apply_async(
kwargs={
'event': s.event_id,
'schedule': s.pk,
},
# Scheduled exports usually run on the low-prio queue "background" but if they're manually triggered,
# we run them with normal priority
queue='default',
)
messages.success(self.request, _('Your export is queued to start soon. The results will be send via email. '
'Depending on system load and type and size of export, this may take a few '
'minutes.'))
return redirect(reverse('control:event.orders.export', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}))
class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView): class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
+66 -259
View File
@@ -34,7 +34,7 @@
import json import json
import re import re
from datetime import time, timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
import bleach import bleach
@@ -53,10 +53,9 @@ from django.forms import DecimalField
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, now from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic import ( from django.views.generic import (
CreateView, DeleteView, DetailView, FormView, ListView, TemplateView, CreateView, DeleteView, DetailView, FormView, ListView, TemplateView,
@@ -72,7 +71,7 @@ from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry, CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer, Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
ScheduledOrganizerExport, Team, TeamInvite, User, Team, TeamInvite, User,
) )
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
@@ -82,13 +81,12 @@ from pretix.base.models.giftcards import (
from pretix.base.models.orders import CancellationRequest from pretix.base.models.orders import CancellationRequest
from pretix.base.models.organizer import TeamAPIToken from pretix.base.models.organizer import TeamAPIToken
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
from pretix.base.services.export import multiexport, scheduled_organizer_export from pretix.base.services.export import multiexport
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
from pretix.base.settings import SETTINGS_AFFECTING_CSS from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.base.signals import register_multievent_data_exporters from pretix.base.signals import register_multievent_data_exporters
from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.exports import ScheduledOrganizerExportForm
from pretix.control.forms.filter import ( from pretix.control.forms.filter import (
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm, CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
OrganizerFilterForm, TeamFilterForm, OrganizerFilterForm, TeamFilterForm,
@@ -102,7 +100,6 @@ from pretix.control.forms.organizer import (
OrganizerSettingsForm, OrganizerUpdateForm, SSOClientForm, SSOProviderForm, OrganizerSettingsForm, OrganizerUpdateForm, SSOClientForm, SSOProviderForm,
TeamForm, WebHookForm, TeamForm, WebHookForm,
) )
from pretix.control.forms.rrule import RRuleForm
from pretix.control.logdisplay import OVERVIEW_BANLIST from pretix.control.logdisplay import OVERVIEW_BANLIST
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin, AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
@@ -111,7 +108,6 @@ from pretix.control.signals import nav_organizer
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import GroupConcat from pretix.helpers import GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import build_absolute_uri as build_global_uri
@@ -627,7 +623,7 @@ class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
return super().form_invalid(form) return super().form_invalid(form)
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = Team model = Team
template_name = 'pretixcontrol/organizers/team_delete.html' template_name = 'pretixcontrol/organizers/team_delete.html'
permission = 'can_change_teams' permission = 'can_change_teams'
@@ -863,19 +859,12 @@ class DeviceQueryMixin:
@cached_property @cached_property
def request_data(self): def request_data(self):
if self.request.method == "POST": if self.request.method == "POST":
d = self.request.POST return self.request.POST
else: return self.request.GET
d = self.request.GET
d = d.copy()
d.setdefault('state', 'active')
return d
@cached_property @cached_property
def filter_form(self): def filter_form(self):
return DeviceFilterForm( return DeviceFilterForm(data=self.request_data, request=self.request)
data=self.request_data,
request=self.request,
)
def get_queryset(self): def get_queryset(self):
qs = self.request.organizer.devices.prefetch_related( qs = self.request.organizer.devices.prefetch_related(
@@ -1518,74 +1507,15 @@ class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class ExportMixin: class ExportMixin:
@cached_property @cached_property
def exporter(self): def exporters(self):
id = self.request.GET.get("identifier") or self.request.POST.get("exporter") or self.request.GET.get("exporter") exporters = []
if not id: events = self.request.user.get_events_with_permission('can_view_orders', request=self.request).filter(
return None
for ex in self.exporters:
if id != ex.identifier:
continue
if self.scheduled:
initial = self.scheduled.export_form_data
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
for k in initial:
if initial[k] and k in test_form.fields:
try:
initial[k] = test_form.fields[k].to_python(initial[k])
except Exception:
pass
else:
# Use form parse cycle to generate useful defaults
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
test_form.is_valid()
initial = {
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
}
if 'events' not in initial:
initial.setdefault('all_events', True)
ex.form = ExporterForm(
data=(self.request.POST if self.request.method == 'POST' else None),
prefix=ex.identifier,
initial=initial
)
ex.form.fields = ex.export_form_fields
if not isinstance(ex, OrganizerLevelExportMixin):
ex.form.fields.update([
('all_events',
forms.BooleanField(
label=_("All events (that I have access to)"),
required=False
)),
('events',
forms.ModelMultipleChoiceField(
queryset=self.events,
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'scrolling-multiple-choice',
'data-inverse-dependency': f'#id_{ex.identifier}-all_events',
}
),
label=_('Events'),
required=False
)),
])
return ex
@cached_property
def events(self):
return self.request.user.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer organizer=self.request.organizer
) )
@cached_property
def exporters(self):
responses = register_multievent_data_exporters.send(self.request.organizer) responses = register_multievent_data_exporters.send(self.request.organizer)
id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
raw_exporters = [ raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events, self.request.organizer) response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses for r, response in responses
if response if response
] ]
@@ -1596,37 +1526,50 @@ class ExportMixin:
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request) self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
) )
] ]
return sorted( for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
raw_exporters, if id and ex.identifier != id:
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower()) continue
)
# Use form parse cycle to generate useful defaults
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
test_form.is_valid()
initial = {
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
}
ex.form = ExporterForm(
data=(self.request.POST if self.request.method == 'POST' else None),
prefix=ex.identifier,
initial=initial
)
ex.form.fields = ex.export_form_fields
if not isinstance(ex, OrganizerLevelExportMixin):
ex.form.fields.update([
('events',
forms.ModelMultipleChoiceField(
queryset=events,
initial=events,
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
label=_('Events'),
required=True
)),
])
exporters.append(ex)
return exporters
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['exporter'] = self.exporter
ctx['exporters'] = self.exporters ctx['exporters'] = self.exporters
return ctx return ctx
def get_scheduled_queryset(self):
if not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
request=self.request):
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
else:
qs = self.request.organizer.scheduled_exports
return qs.select_related('owner').order_by('export_identifier', 'schedule_next_run')
@cached_property
def scheduled(self):
if "scheduled" in self.request.POST:
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.POST.get("scheduled"))
elif "scheduled" in self.request.GET:
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.GET.get("scheduled"))
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView): class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
known_errortypes = ['ExportError'] known_errortypes = ['ExportError']
task = multiexport task = multiexport
template_name = 'pretixcontrol/organizers/export_form.html' template_name = 'pretixcontrol/organizers/export.html'
def get_success_message(self, value): def get_success_message(self, value):
return None return None
@@ -1639,6 +1582,12 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
'organizer': self.request.organizer.slug 'organizer': self.request.organizer.slug
}) })
@cached_property
def exporter(self):
for ex in self.exporters:
if ex.identifier == self.request.POST.get("exporter"):
return ex
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY: if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request) return self.get_result(request)
@@ -1651,13 +1600,9 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
'organizer': self.request.organizer.slug 'organizer': self.request.organizer.slug
}) })
if self.scheduled: if not self.exporter.form.is_valid():
data = self.scheduled.export_form_data messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
else: return self.get(request, *args, **kwargs)
if not self.exporter.form.is_valid():
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(request, *args, **kwargs)
data = self.exporter.form.cleaned_data
cf = CachedFile(web_download=True, session_key=request.session.session_key) cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now() cf.date = now()
@@ -1670,151 +1615,13 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
provider=self.exporter.identifier, provider=self.exporter.identifier,
device=None, device=None,
token=None, token=None,
form_data=data, form_data=self.exporter.form.cleaned_data,
staff_session=self.request.user.has_active_staff_session(self.request.session.session_key) staff_session=self.request.user.has_active_staff_session(self.request.session.session_key)
) )
class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView): class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, TemplateView):
paginate_by = 25 template_name = 'pretixcontrol/organizers/export.html'
context_object_name = 'scheduled'
def get_template_names(self):
if self.exporter:
return ['pretixcontrol/organizers/export_form.html']
return ['pretixcontrol/organizers/export.html']
@transaction.atomic()
def post(self, request, *args, **kwargs):
if request.POST.get("schedule") == "save":
if self.exporter.form.is_valid() and self.rrule_form.is_valid() and self.schedule_form.is_valid():
self.schedule_form.instance.export_identifier = self.exporter.identifier
self.schedule_form.instance.export_form_data = self.exporter.form.cleaned_data
self.schedule_form.instance.schedule_rrule = str(self.rrule_form.to_rrule())
self.schedule_form.instance.error_counter = 0
self.schedule_form.instance.error_last_message = None
self.schedule_form.instance.compute_next_run()
self.schedule_form.instance.save()
if self.schedule_form.instance.schedule_next_run:
messages.success(
request,
_('Your export schedule has been saved. The next export will start around {datetime}.').format(
datetime=date_format(self.schedule_form.instance.schedule_next_run, 'SHORT_DATETIME_FORMAT')
)
)
else:
messages.warning(request, _('Your export schedule has been saved, but no next export is planned.'))
self.request.organizer.log_action(
'pretix.organizer.export.schedule.changed' if self.scheduled else 'pretix.organizer.export.schedule.added',
user=self.request.user, data={
'id': self.schedule_form.instance.id,
'export_identifier': self.exporter.identifier,
'export_form_data': self.exporter.form.cleaned_data,
'schedule_rrule': self.schedule_form.instance.schedule_rrule,
**self.schedule_form.cleaned_data,
}
)
return redirect(reverse('control:organizer.export', kwargs={
'organizer': self.request.organizer.slug
}))
else:
return super().get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
@cached_property
def rrule_form(self):
if self.scheduled:
initial = RRuleForm.initial_from_rrule(self.scheduled.schedule_rrule)
else:
initial = {}
return RRuleForm(
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
prefix="rrule",
initial=initial
)
@cached_property
def schedule_form(self):
instance = self.scheduled or ScheduledOrganizerExport(
organizer=self.request.organizer,
owner=self.request.user,
timezone=get_current_timezone().zone,
)
if not self.scheduled:
initial = {
"mail_subject": gettext("Export: {title}").format(title=self.exporter.verbose_name),
"mail_template": gettext("Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
name=str(self.request.organizer.name)
),
"schedule_rrule_time": time(4, 0, 0),
}
else:
initial = {}
return ScheduledOrganizerExportForm(
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
prefix="schedule",
instance=instance,
initial=initial,
)
def get_queryset(self):
return self.get_scheduled_queryset()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if "schedule" in self.request.POST or self.scheduled:
ctx['schedule_form'] = self.schedule_form
ctx['rrule_form'] = self.rrule_form
elif not self.exporter:
for s in ctx['scheduled']:
try:
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][0].verbose_name
except IndexError:
s.export_verbose_name = "?"
return ctx
class DeleteScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, DeleteView):
template_name = 'pretixcontrol/organizers/export_delete.html'
context_object_name = 'export'
def get_queryset(self):
return self.get_scheduled_queryset()
def get_success_url(self):
return reverse('control:organizer.export', kwargs={
'organizer': self.request.organizer.slug
})
@transaction.atomic()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
self.request.organizer.log_action('pretix.organizer.export.schedule.deleted', user=self.request.user, data={
'id': self.object.id,
})
return redirect(self.get_success_url())
class RunScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, View):
def post(self, request, *args, **kwargs):
s = get_object_or_404(self.get_scheduled_queryset(), pk=kwargs.get('pk'))
scheduled_organizer_export.apply_async(
kwargs={
'organizer': s.organizer_id,
'schedule': s.pk,
},
# Scheduled exports usually run on the low-prio queue "background" but if they're manually triggered,
# we run them with normal priority
queue='default',
)
messages.success(self.request, _('Your export is queued to start soon. The results will be send via email. '
'Depending on system load and type and size of export, this may take a few '
'minutes.'))
return redirect(reverse('control:organizer.export', kwargs={
'organizer': self.request.organizer.slug
}))
class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView): class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
@@ -1894,7 +1701,7 @@ class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
return super().form_invalid(form) return super().form_invalid(form)
class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = Gate model = Gate
template_name = 'pretixcontrol/organizers/gate_delete.html' template_name = 'pretixcontrol/organizers/gate_delete.html'
permission = 'can_change_organizer_settings' permission = 'can_change_organizer_settings'
@@ -1985,7 +1792,7 @@ class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionR
return super().form_invalid(form) return super().form_invalid(form)
class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = EventMetaProperty model = EventMetaProperty
template_name = 'pretixcontrol/organizers/property_delete.html' template_name = 'pretixcontrol/organizers/property_delete.html'
permission = 'can_change_organizer_settings' permission = 'can_change_organizer_settings'
@@ -2108,7 +1915,7 @@ class MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
return super().form_invalid(form) return super().form_invalid(form)
class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = MembershipType model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_delete.html' template_name = 'pretixcontrol/organizers/membershiptype_delete.html'
permission = 'can_change_organizer_settings' permission = 'can_change_organizer_settings'
@@ -2221,7 +2028,7 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
return super().form_invalid(form) return super().form_invalid(form)
class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = CustomerSSOProvider model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_delete.html' template_name = 'pretixcontrol/organizers/ssoprovider_delete.html'
permission = 'can_change_organizer_settings' permission = 'can_change_organizer_settings'
@@ -2349,7 +2156,7 @@ class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
return super().form_invalid(form) return super().form_invalid(form)
class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = CustomerSSOClient model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_delete.html' template_name = 'pretixcontrol/organizers/ssoclient_delete.html'
permission = 'can_change_organizer_settings' permission = 'can_change_organizer_settings'
@@ -2612,7 +2419,7 @@ class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequired
}) })
class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
template_name = 'pretixcontrol/organizers/customer_membership_delete.html' template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
permission = 'can_manage_customers' permission = 'can_manage_customers'
context_object_name = 'membership' context_object_name = 'membership'
+39 -7
View File
@@ -36,7 +36,7 @@ import copy
from collections import defaultdict from collections import defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from dateutil.rrule import rruleset from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
@@ -52,7 +52,9 @@ from django.utils.functional import cached_property
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View from django.views import View
from django.views.generic import CreateView, FormView, ListView, UpdateView from django.views.generic import (
CreateView, DeleteView, FormView, ListView, UpdateView,
)
from pretix.base.models import CartPosition, LogEntry from pretix.base.models import CartPosition, LogEntry
from pretix.base.models.checkin import CheckinList from pretix.base.models.checkin import CheckinList
@@ -78,7 +80,6 @@ from pretix.control.signals import subevent_forms
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers import GroupConcat from pretix.helpers import GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
@@ -147,7 +148,7 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM
return ctx return ctx
class SubEventDelete(EventPermissionRequiredMixin, CompatDeleteView): class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
model = SubEvent model = SubEvent
template_name = 'pretixcontrol/subevents/delete.html' template_name = 'pretixcontrol/subevents/delete.html'
permission = 'can_change_settings' permission = 'can_change_settings'
@@ -788,10 +789,41 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
if f in self.rrule_formset.deleted_forms: if f in self.rrule_formset.deleted_forms:
continue continue
if f.cleaned_data['exclude']: rule_kwargs = {}
s.exrule(f.to_rrule()) rule_kwargs['dtstart'] = f.cleaned_data['dtstart']
rule_kwargs['interval'] = f.cleaned_data['interval']
if f.cleaned_data['freq'] == 'yearly':
freq = YEARLY
if f.cleaned_data['yearly_same'] == "off":
rule_kwargs['bysetpos'] = int(f.cleaned_data['yearly_bysetpos'])
rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['yearly_byweekday'])
rule_kwargs['bymonth'] = int(f.cleaned_data['yearly_bymonth'])
elif f.cleaned_data['freq'] == 'monthly':
freq = MONTHLY
if f.cleaned_data['monthly_same'] == "off":
rule_kwargs['bysetpos'] = int(f.cleaned_data['monthly_bysetpos'])
rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['monthly_byweekday'])
elif f.cleaned_data['freq'] == 'weekly':
freq = WEEKLY
if f.cleaned_data['weekly_byweekday']:
rule_kwargs['byweekday'] = [f.parse_weekdays(a) for a in f.cleaned_data['weekly_byweekday']]
elif f.cleaned_data['freq'] == 'daily':
freq = DAILY
if f.cleaned_data['end'] == 'count':
rule_kwargs['count'] = f.cleaned_data['count']
else: else:
s.rrule(f.to_rrule()) rule_kwargs['until'] = f.cleaned_data['until']
if f.cleaned_data['exclude']:
s.exrule(rrule(freq, **rule_kwargs))
else:
s.rrule(rrule(freq, **rule_kwargs))
return s return s
+3 -7
View File
@@ -400,13 +400,9 @@ def items_select2(request, **kwargs):
except ValueError: except ValueError:
page = 1 page = 1
q = Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query) qs = request.event.items.filter(
try: name__icontains=i18ncomp(query)
if query.isdigit(): ).order_by(
q |= Q(pk=int(query))
except ValueError:
pass
qs = request.event.items.filter(q).order_by(
F('category__position').asc(nulls_first=True), F('category__position').asc(nulls_first=True),
'category', 'category',
'position', 'position',
+3 -4
View File
@@ -53,7 +53,7 @@ from django.utils.safestring import mark_safe
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import ( from django.views.generic import (
CreateView, ListView, TemplateView, UpdateView, View, CreateView, DeleteView, ListView, TemplateView, UpdateView, View,
) )
from pretix.base.models import CartPosition, LogEntry, Voucher from pretix.base.models import CartPosition, LogEntry, Voucher
@@ -66,7 +66,6 @@ from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import voucher_form_class from pretix.control.signals import voucher_form_class
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
@@ -183,7 +182,7 @@ class VoucherTags(EventPermissionRequiredMixin, TemplateView):
return VoucherTagFilterForm(data=self.request.GET, event=self.request.event) return VoucherTagFilterForm(data=self.request.GET, event=self.request.event)
class VoucherDeleteCarts(EventPermissionRequiredMixin, CompatDeleteView): class VoucherDeleteCarts(EventPermissionRequiredMixin, DeleteView):
model = Voucher model = Voucher
template_name = 'pretixcontrol/vouchers/delete_carts.html' template_name = 'pretixcontrol/vouchers/delete_carts.html'
permission = 'can_change_vouchers' permission = 'can_change_vouchers'
@@ -215,7 +214,7 @@ class VoucherDeleteCarts(EventPermissionRequiredMixin, CompatDeleteView):
}) })
class VoucherDelete(EventPermissionRequiredMixin, CompatDeleteView): class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
model = Voucher model = Voucher
template_name = 'pretixcontrol/vouchers/delete.html' template_name = 'pretixcontrol/vouchers/delete.html'
permission = 'can_change_vouchers' permission = 'can_change_vouchers'
+2 -2
View File
@@ -48,6 +48,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext from django.utils.translation import gettext_lazy as _, pgettext
from django.views import View from django.views import View
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.edit import DeleteView
from pretix.base.models import Item, Quota, WaitingListEntry from pretix.base.models import Item, Quota, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException from pretix.base.models.waitinglist import WaitingListException
@@ -57,7 +58,6 @@ from pretix.control.forms.waitinglist import WaitingListEntryTransferForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from ...helpers.compat import CompatDeleteView
from . import UpdateView from . import UpdateView
@@ -338,7 +338,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
return '{}_waitinglist'.format(self.request.event.slug) return '{}_waitinglist'.format(self.request.event.slug)
class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView): class EntryDelete(EventPermissionRequiredMixin, DeleteView):
model = WaitingListEntry model = WaitingListEntry
template_name = 'pretixcontrol/waitinglist/delete.html' template_name = 'pretixcontrol/waitinglist/delete.html'
permission = 'can_change_orders' permission = 'can_change_orders'
-30
View File
@@ -22,39 +22,9 @@
import datetime import datetime
import sys import sys
from django.forms import Form
from django.views.generic.detail import (
BaseDetailView, SingleObjectTemplateResponseMixin,
)
from django.views.generic.edit import DeletionMixin, FormMixin
def date_fromisocalendar(isoyear, isoweek, isoday): def date_fromisocalendar(isoyear, isoweek, isoday):
if sys.version_info < (3, 8): if sys.version_info < (3, 8):
return datetime.datetime.strptime(f'{isoyear}-W{isoweek}-{isoday}', "%G-W%V-%u") return datetime.datetime.strptime(f'{isoyear}-W{isoweek}-{isoday}', "%G-W%V-%u")
else: else:
return datetime.datetime.fromisocalendar(isoyear, isoweek, isoday) return datetime.datetime.fromisocalendar(isoyear, isoweek, isoday)
class CompatDeleteView(SingleObjectTemplateResponseMixin, DeletionMixin, FormMixin, BaseDetailView):
"""
This is a DeleteView variant that allows us to create subclasses that work on both Django 3.2 and
Django 4.0.
"""
form_class = Form
template_name_suffix = "_confirm_delete"
def post(self, request, *args, **kwargs):
# Set self.object before the usual form processing flow.
# Inlined because having DeletionMixin as the first base, for
# get_success_url(), makes leveraging super() with ProcessFormView
# overly complex.
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
return self.delete(self.request, self.args, self.kwargs)
+1 -40
View File
@@ -22,7 +22,7 @@
import contextlib import contextlib
from django.db import transaction from django.db import transaction
from django.db.models import Aggregate, Expression, Field, Lookup, Value from django.db.models import Aggregate, Field, Lookup
class DummyRollbackException(Exception): class DummyRollbackException(Exception):
@@ -103,42 +103,3 @@ class NotEqual(Lookup):
rhs, rhs_params = self.process_rhs(compiler, connection) rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params params = lhs_params + rhs_params
return '%s <> %s' % (lhs, rhs), params return '%s <> %s' % (lhs, rhs), params
class PostgresWindowFrame(Expression):
template = "%(frame_type)s BETWEEN %(start)s AND %(end)s"
def __init__(self, frame_type=None, start=None, end=None):
self.frame_type = frame_type
self.start = Value(start)
self.end = Value(end)
def set_source_expressions(self, exprs):
self.start, self.end = exprs
def get_source_expressions(self):
return [self.start, self.end]
def as_sql(self, compiler, connection):
return (
self.template
% {
"frame_type": self.frame_type,
"start": self.start.value,
"end": self.end.value,
},
[],
)
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self)
def get_group_by_cols(self, alias=None):
return []
def __str__(self):
return self.template % {
"frame_type": self.frame_type,
"start": self.start.value,
"end": self.end.value,
}
-85
View File
@@ -1,85 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from io import BytesIO
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError
IMAGE_TYPES = {'image/gif', 'image/jpeg', 'image/png'}
IMAGE_EXTS = {'.gif', '.jpg', '.jpeg', '.png'}
logger = logging.getLogger(__name__)
def validate_uploaded_file_for_valid_image(f):
if f is None:
return None
from PIL import Image
# We need to get a file object for Pillow. We might have a path or we might
# have to read the data into memory.
if hasattr(f, 'temporary_file_path'):
file = f.temporary_file_path()
else:
if hasattr(f, 'read'):
file = BytesIO(f.read())
else:
file = BytesIO(f['content'])
try:
try:
image = Image.open(file)
# verify() must be called immediately after the constructor.
image.verify()
except DecompressionBombError:
raise ValidationError(_(
"The file you uploaded has a very large number of pixels, please upload a picture with smaller dimensions."
))
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
if image.width * image.height > MAX_IMAGE_PIXELS:
raise ValidationError(_(
"The file you uploaded has a very large number of pixels, please upload a picture with smaller dimensions."
))
except Exception as exc:
logger.exception('Could not parse image')
# Pillow doesn't recognize it as an image.
if isinstance(exc, ValidationError):
raise
raise ValidationError(_(
"Upload a valid image. The file you uploaded was either not an image or a corrupted image."
)) from exc
if hasattr(f, 'seek') and callable(f.seek):
f.seek(0)
class ImageSizeValidator:
def __call__(self, image):
if image.width * image.height > MAX_IMAGE_PIXELS:
raise ValidationError(_(
"The file you uploaded has a very large number of pixels, please upload a picture with smaller dimensions."
))
return image
File diff suppressed because it is too large Load Diff
+49 -57
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n" "POT-Creation-Date: 2022-12-14 13:09+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n" "PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n" "Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -132,18 +132,18 @@ msgstr ""
msgid "Mercado Pago" msgid "Mercado Pago"
msgstr "" msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:164 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:163
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue" msgid "Continue"
msgstr "المتابعة" msgstr "المتابعة"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:221
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:157 #: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:188 #: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …" msgid "Confirming your payment …"
msgstr "جاري تأكيد الدفع الخاص بك …" msgstr "جاري تأكيد الدفع الخاص بك …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:247 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:246
msgid "Payment method unavailable" msgid "Payment method unavailable"
msgstr "" msgstr ""
@@ -165,11 +165,11 @@ msgstr "إجمالي الإيرادات"
msgid "Contacting Stripe …" msgid "Contacting Stripe …"
msgstr "جاري الاتصال بStripe …" msgstr "جاري الاتصال بStripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:65 #: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
msgid "Total" msgid "Total"
msgstr "المجموع" msgstr "المجموع"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:164 #: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
msgid "Contacting your bank …" msgid "Contacting your bank …"
msgstr "جاري الاتصال بالبنك الذي تتعامل معه …" msgstr "جاري الاتصال بالبنك الذي تتعامل معه …"
@@ -367,11 +367,11 @@ msgid ""
msgstr "" msgstr ""
"لا يمكننا الوصول إلى الخادم حاليا، حاول مرة أخرى من فضلك. رمز الخطأ : {code}" "لا يمكننا الوصول إلى الخادم حاليا، حاول مرة أخرى من فضلك. رمز الخطأ : {code}"
#: pretix/static/pretixbase/js/asynctask.js:215 #: pretix/static/pretixbase/js/asynctask.js:208
msgid "We are processing your request …" msgid "We are processing your request …"
msgstr "جاري معالجة طلبك …" msgstr "جاري معالجة طلبك …"
#: pretix/static/pretixbase/js/asynctask.js:223 #: pretix/static/pretixbase/js/asynctask.js:216
msgid "" msgid ""
"We are currently sending your request to the server. If this takes longer " "We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this " "than one minute, please check your internet connection and then reload this "
@@ -380,7 +380,7 @@ msgstr ""
"نعمل الآن على ارسال طلبك إلى الخادم، إذا أستغرقت العملية أكثر من دقيقة، يرجى " "نعمل الآن على ارسال طلبك إلى الخادم، إذا أستغرقت العملية أكثر من دقيقة، يرجى "
"التحقق من اتصالك بالإنترنت ثم أعد تحميل الصفحة مرة أخرى." "التحقق من اتصالك بالإنترنت ثم أعد تحميل الصفحة مرة أخرى."
#: pretix/static/pretixbase/js/asynctask.js:285 #: pretix/static/pretixbase/js/asynctask.js:276
#: pretix/static/pretixcontrol/js/ui/main.js:71 #: pretix/static/pretixcontrol/js/ui/main.js:71
msgid "Close message" msgid "Close message"
msgstr "أغلق الرسالة" msgstr "أغلق الرسالة"
@@ -486,48 +486,48 @@ msgstr "الدقائق"
msgid "Check-in QR" msgid "Check-in QR"
msgstr "QR الدخول" msgstr "QR الدخول"
#: pretix/static/pretixcontrol/js/ui/editor.js:385 #: pretix/static/pretixcontrol/js/ui/editor.js:384
msgid "The PDF background file could not be loaded for the following reason:" msgid "The PDF background file could not be loaded for the following reason:"
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:" msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
#: pretix/static/pretixcontrol/js/ui/editor.js:654 #: pretix/static/pretixcontrol/js/ui/editor.js:653
msgid "Group of objects" msgid "Group of objects"
msgstr "مجموعة من العناصر" msgstr "مجموعة من العناصر"
#: pretix/static/pretixcontrol/js/ui/editor.js:660 #: pretix/static/pretixcontrol/js/ui/editor.js:659
msgid "Text object" msgid "Text object"
msgstr "عنصر نص" msgstr "عنصر نص"
#: pretix/static/pretixcontrol/js/ui/editor.js:662 #: pretix/static/pretixcontrol/js/ui/editor.js:661
msgid "Barcode area" msgid "Barcode area"
msgstr "منطقة باركود" msgstr "منطقة باركود"
#: pretix/static/pretixcontrol/js/ui/editor.js:664 #: pretix/static/pretixcontrol/js/ui/editor.js:663
msgid "Image area" msgid "Image area"
msgstr "منطقة صورة" msgstr "منطقة صورة"
#: pretix/static/pretixcontrol/js/ui/editor.js:666 #: pretix/static/pretixcontrol/js/ui/editor.js:665
msgid "Powered by pretix" msgid "Powered by pretix"
msgstr "مدعوم من pretix" msgstr "مدعوم من pretix"
#: pretix/static/pretixcontrol/js/ui/editor.js:668 #: pretix/static/pretixcontrol/js/ui/editor.js:667
msgid "Object" msgid "Object"
msgstr "عنصر" msgstr "عنصر"
#: pretix/static/pretixcontrol/js/ui/editor.js:672 #: pretix/static/pretixcontrol/js/ui/editor.js:671
msgid "Ticket design" msgid "Ticket design"
msgstr "تصميم التذكرة" msgstr "تصميم التذكرة"
#: pretix/static/pretixcontrol/js/ui/editor.js:962 #: pretix/static/pretixcontrol/js/ui/editor.js:961
msgid "Saving failed." msgid "Saving failed."
msgstr "فشلت عملية الحفظ." msgstr "فشلت عملية الحفظ."
#: pretix/static/pretixcontrol/js/ui/editor.js:1030 #: pretix/static/pretixcontrol/js/ui/editor.js:1011
#: pretix/static/pretixcontrol/js/ui/editor.js:1069 #: pretix/static/pretixcontrol/js/ui/editor.js:1050
msgid "Error while uploading your PDF file, please try again." msgid "Error while uploading your PDF file, please try again."
msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى." msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى."
#: pretix/static/pretixcontrol/js/ui/editor.js:1054 #: pretix/static/pretixcontrol/js/ui/editor.js:1035
msgid "Do you really want to leave the editor without saving your changes?" msgid "Do you really want to leave the editor without saving your changes?"
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟" msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
@@ -557,31 +557,31 @@ msgid ""
"darker shade." "darker shade."
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق." msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
#: pretix/static/pretixcontrol/js/ui/main.js:463 #: pretix/static/pretixcontrol/js/ui/main.js:454
msgid "All" msgid "All"
msgstr "الكل" msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:464 #: pretix/static/pretixcontrol/js/ui/main.js:455
msgid "None" msgid "None"
msgstr "لا شيء" msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:465 #: pretix/static/pretixcontrol/js/ui/main.js:456
msgid "Search query" msgid "Search query"
msgstr "البحث في الاستفسارات" msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:468 #: pretix/static/pretixcontrol/js/ui/main.js:459
msgid "Selected only" msgid "Selected only"
msgstr "المختارة فقط" msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:871 #: pretix/static/pretixcontrol/js/ui/main.js:862
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا" msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:907 #: pretix/static/pretixcontrol/js/ui/main.js:898
msgid "Click to close" msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة" msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:979 #: pretix/static/pretixcontrol/js/ui/main.js:970
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!" msgstr "لم تقم بحفظ التعديلات!"
@@ -866,93 +866,85 @@ msgid "Open seat selection"
msgstr "خيارات المقاعد" msgstr "خيارات المقاعد"
#: pretix/static/pretixpresale/js/widget/widget.js:56 #: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid ""
"Some or all ticket categories are currently sold out. If you want, you can "
"add yourself to the waiting list. We will then notify if seats are available "
"again."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#, fuzzy #, fuzzy
#| msgid "Load more" #| msgid "Load more"
msgctxt "widget" msgctxt "widget"
msgid "Load more" msgid "Load more"
msgstr "تحميل المزيد" msgstr "تحميل المزيد"
#: pretix/static/pretixpresale/js/widget/widget.js:59 #: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo" msgid "Mo"
msgstr "الإثنين" msgstr "الإثنين"
#: pretix/static/pretixpresale/js/widget/widget.js:60 #: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "Tu" msgid "Tu"
msgstr "الثلاثاء" msgstr "الثلاثاء"
#: pretix/static/pretixpresale/js/widget/widget.js:61 #: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "We" msgid "We"
msgstr "الأربعاء" msgstr "الأربعاء"
#: pretix/static/pretixpresale/js/widget/widget.js:62 #: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "Th" msgid "Th"
msgstr "الخميس" msgstr "الخميس"
#: pretix/static/pretixpresale/js/widget/widget.js:63 #: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "Fr" msgid "Fr"
msgstr "الجمعة" msgstr "الجمعة"
#: pretix/static/pretixpresale/js/widget/widget.js:64 #: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "Sa" msgid "Sa"
msgstr "السبت" msgstr "السبت"
#: pretix/static/pretixpresale/js/widget/widget.js:65 #: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "Su" msgid "Su"
msgstr "الأحد" msgstr "الأحد"
#: pretix/static/pretixpresale/js/widget/widget.js:68 #: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "January" msgid "January"
msgstr "يناير" msgstr "يناير"
#: pretix/static/pretixpresale/js/widget/widget.js:69 #: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "February" msgid "February"
msgstr "فبراير" msgstr "فبراير"
#: pretix/static/pretixpresale/js/widget/widget.js:70 #: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "March" msgid "March"
msgstr "مارس" msgstr "مارس"
#: pretix/static/pretixpresale/js/widget/widget.js:71 #: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "April" msgid "April"
msgstr "أبريل" msgstr "أبريل"
#: pretix/static/pretixpresale/js/widget/widget.js:72 #: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "May" msgid "May"
msgstr "مايو" msgstr "مايو"
#: pretix/static/pretixpresale/js/widget/widget.js:73 #: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "June" msgid "June"
msgstr "يونيو" msgstr "يونيو"
#: pretix/static/pretixpresale/js/widget/widget.js:74 #: pretix/static/pretixpresale/js/widget/widget.js:73
msgid "July" msgid "July"
msgstr "يوليو" msgstr "يوليو"
#: pretix/static/pretixpresale/js/widget/widget.js:75 #: pretix/static/pretixpresale/js/widget/widget.js:74
msgid "August" msgid "August"
msgstr "أغسطس" msgstr "أغسطس"
#: pretix/static/pretixpresale/js/widget/widget.js:76 #: pretix/static/pretixpresale/js/widget/widget.js:75
msgid "September" msgid "September"
msgstr "سبتمبر" msgstr "سبتمبر"
#: pretix/static/pretixpresale/js/widget/widget.js:77 #: pretix/static/pretixpresale/js/widget/widget.js:76
msgid "October" msgid "October"
msgstr "أكتوبر" msgstr "أكتوبر"
#: pretix/static/pretixpresale/js/widget/widget.js:78 #: pretix/static/pretixpresale/js/widget/widget.js:77
msgid "November" msgid "November"
msgstr "نوفمبر" msgstr "نوفمبر"
#: pretix/static/pretixpresale/js/widget/widget.js:79 #: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "December" msgid "December"
msgstr "ديسمبر" msgstr "ديسمبر"
File diff suppressed because it is too large Load Diff

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