forked from CGM_Public/pretix_original
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b40c9f44c |
@@ -9,7 +9,6 @@ updates:
|
||||
directory: "/src"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/pretix/static/npm_dir"
|
||||
schedule:
|
||||
|
||||
@@ -17,9 +17,6 @@ on:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Spellcheck
|
||||
|
||||
@@ -15,9 +15,6 @@ on:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
@@ -15,9 +15,6 @@ on:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
isort:
|
||||
name: isort
|
||||
|
||||
@@ -15,9 +15,6 @@ on:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -81,6 +78,5 @@ jobs:
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: src/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.10'
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
Requirements
|
||||
@@ -318,27 +318,6 @@ example::
|
||||
(venv)$ python -m pretix rebuild
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
System updates
|
||||
--------------
|
||||
|
||||
After system updates, such as updates to a new Ubuntu or Debian release, you might be using a new Python version.
|
||||
That's great, but requires some adjustments. First, adjust any old version paths in your nginx configuration file.
|
||||
Then, re-create your Python environment::
|
||||
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 freeze > /tmp/pip-backup.txt
|
||||
$ rm -rf /var/pretix/venv
|
||||
$ python3 -m venv /var/pretix/venv
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip wheel setuptools
|
||||
(venv)$ pip3 install -r /tmp/pip-backup.txt
|
||||
|
||||
Then, proceed like after any plugin installation::
|
||||
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
|
||||
|
||||
@@ -48,11 +48,10 @@ Possible permissions are:
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
We try to avoid any breaking changes to our API to avoid hassle on your end. If possible, we'll
|
||||
build new features in a way that keeps all pre-existing API usage unchanged. In some cases,
|
||||
this might not be possible or only possible with restrictions. In these case, any
|
||||
backwards-incompatible changes will be prominently noted in the "Changes to the REST API"
|
||||
section of our release notes. If possible, we will announce them multiple releases in advance.
|
||||
We currently see pretix' API as a beta-stage feature. We therefore do not give any guarantees
|
||||
for compatibility between feature releases of pretix (such as 1.5 and 1.6). However, as always,
|
||||
we try not to break things when we don't need to. Any backwards-incompatible changes will be
|
||||
prominently noted in the release notes.
|
||||
|
||||
We treat the following types of changes as *backwards-compatible* so we ask you to make sure
|
||||
that your clients can deal with them properly:
|
||||
@@ -61,7 +60,6 @@ that your clients can deal with them properly:
|
||||
* Support of new HTTP methods for a given API endpoint
|
||||
* Support of new query parameters for a given API endpoint
|
||||
* New fields contained in API responses
|
||||
* New possible values of enumeration-like fields
|
||||
* Response body structure or message texts on failed requests (``4xx``, ``5xx`` response codes)
|
||||
|
||||
We treat the following types of changes as *backwards-incompatible*:
|
||||
@@ -192,9 +190,6 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
|
||||
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
|
||||
specifiers in requests
|
||||
(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
|
||||
|
||||
@@ -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 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 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 event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
|
||||
@@ -35,12 +35,6 @@ tax_rule integer The internal ID
|
||||
admission boolean ``true`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(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
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
@@ -164,7 +158,7 @@ meta_data object Values set for
|
||||
|
||||
.. 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
|
||||
-----
|
||||
@@ -219,7 +213,6 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"personalized": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
@@ -336,7 +329,6 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"personalized": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
@@ -434,7 +426,6 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"personalized": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
@@ -519,7 +510,6 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"personalized": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
@@ -636,7 +626,6 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"personalized": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
|
||||
@@ -76,10 +76,6 @@ The exporter class
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: description
|
||||
|
||||
.. autoattribute:: category
|
||||
|
||||
.. autoattribute:: export_form_fields
|
||||
|
||||
.. automethod:: render
|
||||
|
||||
@@ -17,13 +17,9 @@ Field Type Description
|
||||
id integer Internal layout ID
|
||||
name string Internal layout description
|
||||
default boolean ``true`` if this is the default layout
|
||||
layout list Dynamic layout specification. Each list element
|
||||
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.
|
||||
layout object Layout specification for libpretixprint
|
||||
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``).
|
||||
└ item integer Item ID
|
||||
===================================== ========================== =======================================================
|
||||
@@ -62,7 +58,7 @@ Endpoints
|
||||
"name": "Default layout",
|
||||
"default": true,
|
||||
"layout": {…},
|
||||
"background": null,
|
||||
"background": {},
|
||||
"item_assignments": []
|
||||
}
|
||||
]
|
||||
@@ -100,7 +96,7 @@ Endpoints
|
||||
"name": "Default layout",
|
||||
"default": true,
|
||||
"layout": {…},
|
||||
"background": null,
|
||||
"background": {},
|
||||
"item_assignments": []
|
||||
}
|
||||
|
||||
@@ -151,122 +147,3 @@ Endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
: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
|
||||
|
||||
@@ -97,7 +97,6 @@ overpayment
|
||||
param
|
||||
passphrase
|
||||
percental
|
||||
personalization
|
||||
pluggable
|
||||
positionid
|
||||
pre
|
||||
|
||||
@@ -22,10 +22,8 @@
|
||||
from django import forms
|
||||
from django.http import QueryDict
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
||||
|
||||
|
||||
class FormFieldWrapperField(serializers.Field):
|
||||
@@ -144,12 +142,6 @@ class JobRunSerializer(serializers.Serializer):
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
elif isinstance(v, DateFrameField):
|
||||
self.fields[k] = SerializerDateFrameField(
|
||||
required=v.required,
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
else:
|
||||
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():
|
||||
if isinstance(v, serializers.ManyRelatedField) and k not in data:
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -95,12 +95,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
variation = ItemVariation.objects.create(**validated_data)
|
||||
|
||||
if require_membership_types:
|
||||
variation.require_membership_types.add(*require_membership_types)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -234,7 +230,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Item
|
||||
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',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'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_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('tax_rule') and data.get('tax_rule').rate > 0:
|
||||
raise ValidationError(
|
||||
|
||||
@@ -118,10 +118,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
{'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'):
|
||||
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
@@ -640,7 +636,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
for fname, field in list(self.fields.items()):
|
||||
if fname in includes:
|
||||
continue
|
||||
elif hasattr(field, 'child'): # Nested list serializers
|
||||
elif hasattr(field, 'child'):
|
||||
found_any = False
|
||||
for childfname, childfield in list(field.child.fields.items()):
|
||||
if f'{fname}.{childfname}' not in includes:
|
||||
@@ -649,15 +645,6 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
found_any = True
|
||||
if not found_any:
|
||||
self.fields.pop(fname)
|
||||
elif isinstance(field, serializers.Serializer): # Nested serializers
|
||||
found_any = False
|
||||
for childfname, childfield in list(field.fields.items()):
|
||||
if f'{fname}.{childfname}' not in includes:
|
||||
field.fields.pop(childfname)
|
||||
else:
|
||||
found_any = True
|
||||
if not found_any:
|
||||
self.fields.pop(fname)
|
||||
else:
|
||||
self.fields.pop(fname)
|
||||
|
||||
@@ -845,10 +832,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
{'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'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
|
||||
@@ -158,14 +158,12 @@ class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
if not answ_data.get('answer'):
|
||||
continue
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
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.answer = 'file://' + a.file.name
|
||||
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
||||
@@ -175,7 +173,7 @@ class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data.get('answer'), File):
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(os.path.basename(an.name), an, save=False)
|
||||
|
||||
@@ -79,13 +79,6 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
||||
validated_data['external_identifier'] = instance.external_identifier
|
||||
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):
|
||||
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
import importlib
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf.urls import re_path
|
||||
from django.urls import include
|
||||
from django.conf.urls import include, re_path
|
||||
from rest_framework import routers
|
||||
|
||||
from pretix.api.views import cart
|
||||
|
||||
@@ -93,10 +93,8 @@ with scopes_disabled():
|
||||
class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CheckinListSerializer
|
||||
queryset = CheckinList.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = CheckinListFilter
|
||||
ordering = ('subevent__date_from', 'name')
|
||||
ordering_fields = ('subevent__date_from', 'id', 'name',)
|
||||
|
||||
def _get_permission_name(self, request):
|
||||
if request.path.endswith('/failed_checkins/'):
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#
|
||||
import datetime
|
||||
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.timezone import now
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
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.token import TeamTokenAuthentication
|
||||
from pretix.base.models import CachedFile
|
||||
from pretix.helpers.images import (
|
||||
IMAGE_TYPES, validate_uploaded_file_for_valid_image,
|
||||
)
|
||||
|
||||
ALLOWED_TYPES = {
|
||||
'image/gif': {'.gif'},
|
||||
@@ -65,13 +61,6 @@ class UploadView(APIView):
|
||||
name=file_obj.name,
|
||||
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(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
|
||||
@@ -42,6 +42,7 @@ from localflavor.fr.forms import FRZipCodeField
|
||||
from localflavor.gb.forms import GBPostcodeField
|
||||
from localflavor.gr.forms import GRPostalCodeField
|
||||
from localflavor.hr.forms import HRPostalCodeField
|
||||
from localflavor.id_.forms import IDPostCodeField
|
||||
from localflavor.ie.forms import EircodeField
|
||||
from localflavor.il.forms import ILPostalCodeField
|
||||
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
|
||||
# world e.g. without zipcodes
|
||||
'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',
|
||||
'NO', 'NZ', 'PK', 'PL', 'PT', 'RO', 'RU', 'SE', 'SG', 'SI', 'SK', 'TR', 'UA', 'US', 'ZA',
|
||||
'GB', 'GR', 'HR', 'ID', 'IE', 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'LT', 'LV', 'MA', 'MT', 'MX',
|
||||
'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,
|
||||
'GR': GRPostalCodeField,
|
||||
'HR': HRPostalCodeField,
|
||||
'ID': IDPostCodeField,
|
||||
'IE': EircodeField,
|
||||
'IL': ILPostalCodeField,
|
||||
'IN': INZipCodeField,
|
||||
|
||||
@@ -381,14 +381,6 @@ def base_placeholders(sender, **kwargs):
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'order_email', ['order'], lambda order: order.email, 'john@example.org'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_number', ['invoice'],
|
||||
lambda invoice: invoice.full_invoice_no,
|
||||
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
||||
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
||||
@@ -520,20 +512,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()
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_remove', ['waiting_list_voucher', 'event'],
|
||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||
'url_remove', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
event, 'presale:event.waitinglist.remove'
|
||||
) + '?voucher=' + waiting_list_voucher.code,
|
||||
) + '?voucher=' + waiting_list_entry.voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.waitinglist.remove',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['waiting_list_voucher', 'event'],
|
||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||
'url', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + waiting_list_voucher.code,
|
||||
) + '?voucher=' + waiting_list_entry.voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.redeem',
|
||||
@@ -588,7 +580,7 @@ def base_placeholders(sender, **kwargs):
|
||||
_('Sample Admission Ticket')
|
||||
),
|
||||
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'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
|
||||
@@ -36,7 +36,7 @@ import io
|
||||
import tempfile
|
||||
from collections import OrderedDict, namedtuple
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Tuple
|
||||
from typing import Tuple
|
||||
|
||||
import pytz
|
||||
from defusedcsv import csv
|
||||
@@ -84,27 +84,6 @@ class BaseExporter:
|
||||
"""
|
||||
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
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -39,7 +39,7 @@ from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
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
|
||||
|
||||
@@ -49,10 +49,7 @@ from ..signals import register_data_exporters
|
||||
|
||||
class AnswerFilesExporter(BaseExporter):
|
||||
identifier = 'answerfiles'
|
||||
verbose_name = _('Question answer file uploads')
|
||||
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.')
|
||||
verbose_name = _('Answers to file upload questions')
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
|
||||
@@ -36,7 +36,7 @@ from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
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
|
||||
|
||||
@@ -48,8 +48,6 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'customerlist'
|
||||
verbose_name = gettext_lazy('Customer accounts')
|
||||
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
|
||||
def additional_form_fields(self):
|
||||
|
||||
@@ -23,24 +23,22 @@ import json
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
import dateutil
|
||||
from django import forms
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
|
||||
from django.utils.translation import gettext, gettext_lazy
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Invoice, OrderPayment
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
|
||||
|
||||
|
||||
class DekodiNREIExporter(BaseExporter):
|
||||
identifier = 'dekodi_nrei'
|
||||
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/
|
||||
|
||||
@@ -115,7 +113,7 @@ class DekodiNREIExporter(BaseExporter):
|
||||
'PTNo14': p.info_data.get('reference') 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)
|
||||
payments.append({
|
||||
'PTID': '81',
|
||||
@@ -194,12 +192,17 @@ class DekodiNREIExporter(BaseExporter):
|
||||
def render(self, form_data):
|
||||
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
|
||||
|
||||
if form_data.get('date_range'):
|
||||
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
|
||||
if d_start:
|
||||
qs = qs.filter(date__gte=d_start)
|
||||
if d_end:
|
||||
qs = qs.filter(date__lte=d_end)
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
qs = qs.filter(date__gte=date_value)
|
||||
|
||||
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 = {
|
||||
'Format': 'NREI',
|
||||
@@ -215,14 +218,22 @@ class DekodiNREIExporter(BaseExporter):
|
||||
def export_form_fields(self):
|
||||
return OrderedDict(
|
||||
[
|
||||
('date_range',
|
||||
DateFrameField(
|
||||
label=gettext_lazy('Date range'),
|
||||
include_future_frames=False,
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=gettext_lazy('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
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.')
|
||||
)),
|
||||
('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.')
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
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 ..exporter import ListExporter
|
||||
@@ -45,8 +45,6 @@ from ..signals import register_multievent_data_exporters
|
||||
class EventDataExporter(ListExporter):
|
||||
identifier = 'eventdata'
|
||||
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
|
||||
def providers(self):
|
||||
|
||||
@@ -38,15 +38,13 @@ from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from zipfile import ZipFile
|
||||
|
||||
import dateutil.parser
|
||||
from django import forms
|
||||
from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import (
|
||||
gettext, gettext_lazy as _, pgettext, pgettext_lazy,
|
||||
)
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext
|
||||
|
||||
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
|
||||
|
||||
@@ -59,24 +57,30 @@ from ..services.invoices import invoice_pdf_task
|
||||
from ..signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
)
|
||||
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
|
||||
|
||||
|
||||
class InvoiceExporterMixin:
|
||||
category = pgettext_lazy('export_category', 'Invoices')
|
||||
|
||||
@property
|
||||
def invoice_exporter_form_fields(self):
|
||||
return OrderedDict(
|
||||
[
|
||||
('date_range',
|
||||
DateFrameField(
|
||||
label=_('Date range'),
|
||||
include_future_frames=False,
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=_('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
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.')
|
||||
)),
|
||||
('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',
|
||||
forms.ChoiceField(
|
||||
label=_('Payment provider'),
|
||||
@@ -108,12 +112,16 @@ class InvoiceExporterMixin:
|
||||
)
|
||||
)
|
||||
qs = qs.filter(has_payment_with_provider=1)
|
||||
if form_data.get('date_range'):
|
||||
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
|
||||
if d_start:
|
||||
qs = qs.filter(date__gte=d_start)
|
||||
if d_end:
|
||||
qs = qs.filter(date__lte=d_end)
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
qs = qs.filter(date__gte=date_value)
|
||||
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
|
||||
|
||||
@@ -121,7 +129,6 @@ class InvoiceExporterMixin:
|
||||
class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
|
||||
identifier = '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):
|
||||
qs = self.invoices_queryset(form_data).filter(shredded=False)
|
||||
@@ -173,10 +180,6 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
|
||||
class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
identifier = 'invoicedata'
|
||||
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
|
||||
def additional_form_fields(self):
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from django.db.models import Prefetch
|
||||
from django.dispatch import receiver
|
||||
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.utils import get_column_letter
|
||||
|
||||
@@ -48,8 +48,6 @@ def _min(a1, a2):
|
||||
class ItemDataExporter(ListExporter):
|
||||
identifier = 'itemdata'
|
||||
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):
|
||||
locales = self.event.settings.locales
|
||||
@@ -75,7 +73,6 @@ class ItemDataExporter(ListExporter):
|
||||
_("Free price input"),
|
||||
_("Sales tax"),
|
||||
_("Is an admission ticket"),
|
||||
_("Personalized ticket"),
|
||||
_("Generate tickets"),
|
||||
_("Waiting list"),
|
||||
_("Available from"),
|
||||
@@ -147,7 +144,6 @@ class ItemDataExporter(ListExporter):
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule 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.allow_waitinglist else "",
|
||||
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 "",
|
||||
str(i.tax_rule) if i.tax_rule 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.allow_waitinglist else "",
|
||||
date_format(i.available_from.astimezone(self.timezone),
|
||||
|
||||
@@ -38,8 +38,6 @@ from decimal import Decimal
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Prefetch
|
||||
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 ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||
@@ -48,10 +46,7 @@ from ..signals import register_data_exporters
|
||||
|
||||
class JSONExporter(BaseExporter):
|
||||
identifier = 'json'
|
||||
verbose_name = lazy(lambda *args: gettext('Order data') + ' (JSON)', str)()
|
||||
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.')
|
||||
verbose_name = 'Order data (JSON)'
|
||||
|
||||
def render(self, form_data):
|
||||
jo = {
|
||||
@@ -83,7 +78,6 @@ class JSONExporter(BaseExporter):
|
||||
'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,
|
||||
'admission': item.admission,
|
||||
'personalized': item.personalized,
|
||||
'active': item.active,
|
||||
'sales_channels': item.sales_channels,
|
||||
'description': str(item.description),
|
||||
|
||||
@@ -36,7 +36,7 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
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
|
||||
|
||||
@@ -50,8 +50,6 @@ from ..signals import (
|
||||
class MailExporter(BaseExporter):
|
||||
identifier = 'mailaddrs'
|
||||
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):
|
||||
qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event')
|
||||
|
||||
@@ -33,8 +33,10 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime, time
|
||||
from decimal import Decimal
|
||||
|
||||
import dateutil
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.db.models import (
|
||||
@@ -44,10 +46,8 @@ from django.db.models import (
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import (
|
||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||
)
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||
|
||||
from pretix.base.models import (
|
||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
@@ -63,24 +63,14 @@ from ...helpers.iter import chunked_iterable
|
||||
from ..exporter import (
|
||||
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||
)
|
||||
from ..forms.widgets import SplitDateTimePickerWidget
|
||||
from ..signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
)
|
||||
from ..timeframes import (
|
||||
DateFrameField,
|
||||
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||
)
|
||||
|
||||
|
||||
class OrderListExporter(MultiSheetListExporter):
|
||||
identifier = 'orderlist'
|
||||
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
|
||||
def providers(self):
|
||||
@@ -115,25 +105,41 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
initial=False,
|
||||
required=False
|
||||
)),
|
||||
('date_range',
|
||||
DateFrameField(
|
||||
label=_('Date range'),
|
||||
include_future_frames=False,
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=_('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
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',
|
||||
DateFrameField(
|
||||
label=_('Event date'),
|
||||
include_future_frames=True,
|
||||
('date_to',
|
||||
forms.DateField(
|
||||
label=_('End date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
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!')
|
||||
)),
|
||||
]
|
||||
d = OrderedDict(d)
|
||||
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
|
||||
|
||||
def _get_all_payment_methods(self, qs):
|
||||
@@ -176,27 +182,45 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
annotations = {}
|
||||
filters = {}
|
||||
|
||||
if form_data.get('date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
|
||||
if dt_start:
|
||||
filters[f'{rel}datetime__gte'] = dt_start
|
||||
if dt_end:
|
||||
filters[f'{rel}datetime__lt'] = dt_end
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if not isinstance(date_value, date):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
|
||||
|
||||
if form_data.get('event_date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['event_date_range'], self.timezone)
|
||||
if dt_start:
|
||||
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'] = dt_start
|
||||
if dt_end:
|
||||
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__lt'] = dt_end
|
||||
filters[f'{rel}datetime__gte'] = datetime_value
|
||||
|
||||
if form_data.get('date_to'):
|
||||
date_value = form_data.get('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)
|
||||
|
||||
filters[f'{rel}datetime__lte'] = datetime_value
|
||||
|
||||
if form_data.get('event_date_from'):
|
||||
date_value = form_data.get('event_date_from')
|
||||
if not isinstance(date_value, date):
|
||||
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:
|
||||
return qs.annotate(**annotations).filter(**filters)
|
||||
@@ -752,10 +776,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
class PaymentListExporter(ListExporter):
|
||||
identifier = 'paymentlist'
|
||||
verbose_name = gettext_lazy('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
|
||||
verbose_name = gettext_lazy('Order payments and refunds')
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -834,8 +855,6 @@ class PaymentListExporter(ListExporter):
|
||||
class QuotaListExporter(ListExporter):
|
||||
identifier = 'quotalist'
|
||||
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):
|
||||
has_subevents = self.event.has_subevents
|
||||
@@ -889,17 +908,21 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'giftcardtransactionlist'
|
||||
verbose_name = gettext_lazy('Gift card transactions')
|
||||
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
|
||||
def additional_form_fields(self):
|
||||
d = [
|
||||
('date_range',
|
||||
DateFrameField(
|
||||
label=_('Date range'),
|
||||
include_future_frames=False,
|
||||
required=False
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=_('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
)),
|
||||
('date_to',
|
||||
forms.DateField(
|
||||
label=_('End date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
)),
|
||||
]
|
||||
d = OrderedDict(d)
|
||||
@@ -910,12 +933,22 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
card__issuer=self.organizer,
|
||||
).order_by('datetime').select_related('card', 'order', 'order__event')
|
||||
|
||||
if form_data.get('date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
|
||||
if dt_start:
|
||||
qs = qs.filter(datetime__gte=dt_start)
|
||||
if dt_end:
|
||||
qs = qs.filter(datetime__lt=dt_end)
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
qs = qs.filter(
|
||||
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 = [
|
||||
_('Gift card code'),
|
||||
@@ -945,8 +978,6 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
class GiftcardRedemptionListExporter(ListExporter):
|
||||
identifier = 'giftcardredemptionlist'
|
||||
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):
|
||||
payments = OrderPayment.objects.filter(
|
||||
@@ -992,18 +1023,14 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'giftcardlist'
|
||||
verbose_name = gettext_lazy('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
|
||||
def additional_form_fields(self):
|
||||
return OrderedDict(
|
||||
[
|
||||
('date', forms.SplitDateTimeField(
|
||||
('date', forms.DateTimeField(
|
||||
label=_('Show value at'),
|
||||
required=False,
|
||||
widget=SplitDateTimePickerWidget(),
|
||||
help_text=_('Defaults to the time of report.')
|
||||
initial=now(),
|
||||
)),
|
||||
('testmode', forms.ChoiceField(
|
||||
label=_('Test mode'),
|
||||
@@ -1031,13 +1058,12 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
)
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
d = form_data.get('date') or now()
|
||||
s = GiftCardTransaction.objects.filter(
|
||||
card=OuterRef('pk'),
|
||||
datetime__lte=d
|
||||
datetime__lte=form_data['date']
|
||||
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
||||
qs = self.organizer.issued_gift_cards.filter(
|
||||
issuance__lte=d
|
||||
issuance__lte=form_data['date']
|
||||
).annotate(
|
||||
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
|
||||
).order_by('issuance').prefetch_related(
|
||||
@@ -1052,11 +1078,11 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
if form_data.get('state') == 'empty':
|
||||
qs = qs.filter(cached_value=0)
|
||||
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':
|
||||
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':
|
||||
qs = qs.filter(expires__lt=d)
|
||||
qs = qs.filter(expires__lt=form_data['date'])
|
||||
|
||||
headers = [
|
||||
_('Gift card code'),
|
||||
|
||||
@@ -39,8 +39,6 @@ from ..signals import (
|
||||
class WaitingListExporter(ListExporter):
|
||||
identifier = 'waitinglist'
|
||||
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
|
||||
status_filters = [
|
||||
|
||||
@@ -531,7 +531,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
code='aspect_ratio_not_3_by_4',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception('Could not parse image')
|
||||
logger.exception('foo')
|
||||
# Pillow doesn't recognize it as an image.
|
||||
if isinstance(exc, ValidationError):
|
||||
raise
|
||||
@@ -575,7 +575,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
|
||||
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(
|
||||
max_length=255,
|
||||
required=event.settings.attendee_names_required and not self.all_optional,
|
||||
@@ -584,7 +584,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=_('Attendee name'),
|
||||
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(
|
||||
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||
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(
|
||||
required=event.settings.attendee_company_required and not self.all_optional,
|
||||
label=_('Company'),
|
||||
@@ -603,7 +603,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
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(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Address'),
|
||||
|
||||
@@ -30,6 +30,7 @@ from django.urls import get_script_prefix
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||
from django.utils.translation.trans_real import (
|
||||
check_for_language, get_supported_language_variant, language_code_re,
|
||||
parse_accept_lang_header,
|
||||
@@ -127,7 +128,12 @@ def get_language_from_user_settings(request: HttpRequest) -> str:
|
||||
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)
|
||||
try:
|
||||
return get_supported_language_variant(lang_code)
|
||||
@@ -181,14 +187,14 @@ def get_language_from_request(request: HttpRequest) -> str:
|
||||
return (
|
||||
get_language_from_user_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_event(request)
|
||||
or get_default_language()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
get_language_from_cookie(request)
|
||||
get_language_from_session_or_cookie(request)
|
||||
or get_language_from_customer_settings(request)
|
||||
or get_language_from_user_settings(request)
|
||||
or get_language_from_browser(request)
|
||||
@@ -218,11 +224,6 @@ def _merge_csp(a, b):
|
||||
if k not in a:
|
||||
a[k] = b[k]
|
||||
|
||||
for k, v in a.items():
|
||||
if "'unsafe-inline'" in v:
|
||||
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
|
||||
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
|
||||
|
||||
|
||||
class SecurityMiddleware(MiddlewareMixin):
|
||||
CSP_EXEMPT = (
|
||||
@@ -300,7 +301,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||
media=mediadomain)
|
||||
for k, v in h.items():
|
||||
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
|
||||
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')
|
||||
resp['Content-Security-Policy'] = _render_csp(h)
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -30,7 +30,6 @@ from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
SubEvent, SubEventMetaValue, generate_invite_token,
|
||||
)
|
||||
from .exports import ScheduledEventExport, ScheduledOrganizerExport
|
||||
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
|
||||
@@ -35,17 +35,14 @@ from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection, models
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.helpers import PostgresWindowFrame
|
||||
|
||||
|
||||
class CheckinList(LoggedModel):
|
||||
@@ -143,54 +140,7 @@ class CheckinList(LoggedModel):
|
||||
|
||||
@property
|
||||
def inside_count(self):
|
||||
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
||||
# 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
|
||||
return self.positions_inside.count()
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
|
||||
@@ -632,7 +632,6 @@ class Event(EventMixin, LoggedModel):
|
||||
return super().presale_has_ended
|
||||
|
||||
def delete_all_orders(self, really=False):
|
||||
from .checkin import Checkin
|
||||
from .orders import (
|
||||
OrderFee, OrderPayment, OrderPosition, OrderRefund, Transaction,
|
||||
)
|
||||
@@ -646,7 +645,6 @@ class Event(EventMixin, LoggedModel):
|
||||
OrderFee.objects.filter(order__event=self).delete()
|
||||
OrderRefund.objects.filter(order__event=self).delete()
|
||||
OrderPayment.objects.filter(order__event=self).delete()
|
||||
Checkin.objects.filter(list__event=self).delete()
|
||||
self.orders.all().delete()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -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)
|
||||
@@ -62,7 +62,6 @@ from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
|
||||
from ...helpers.images import ImageSizeValidator
|
||||
from .event import Event, SubEvent
|
||||
|
||||
|
||||
@@ -311,8 +310,6 @@ class Item(LoggedModel):
|
||||
:type tax_rate: decimal.Decimal
|
||||
:param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise)
|
||||
: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
|
||||
:type picture: File
|
||||
:param available_from: The date this product goes on sale
|
||||
@@ -399,14 +396,8 @@ class Item(LoggedModel):
|
||||
admission = models.BooleanField(
|
||||
verbose_name=_("Is an admission ticket"),
|
||||
help_text=_(
|
||||
'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'
|
||||
'Whether or not buying this product allows a person to enter '
|
||||
'your event'
|
||||
),
|
||||
default=False
|
||||
)
|
||||
@@ -430,8 +421,7 @@ class Item(LoggedModel):
|
||||
picture = models.ImageField(
|
||||
verbose_name=_("Product picture"),
|
||||
null=True, blank=True, max_length=255,
|
||||
upload_to=itempicture_upload_to,
|
||||
validators=[ImageSizeValidator()]
|
||||
upload_to=itempicture_upload_to
|
||||
)
|
||||
available_from = models.DateTimeField(
|
||||
verbose_name=_("Available from"),
|
||||
@@ -588,10 +578,6 @@ class Item(LoggedModel):
|
||||
return self.event.settings.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):
|
||||
price = price if price is not None else self.default_price
|
||||
|
||||
|
||||
@@ -808,7 +808,7 @@ class Order(LockModel, LoggedModel):
|
||||
return True
|
||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||
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 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)
|
||||
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)
|
||||
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
|
||||
|
||||
if self.addon_to_id:
|
||||
|
||||
@@ -23,7 +23,6 @@ import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
@@ -150,15 +149,7 @@ class TaxRule(LoggedModel):
|
||||
rate = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
validators=[
|
||||
MaxValueValidator(
|
||||
limit_value=Decimal("100.00"),
|
||||
),
|
||||
MinValueValidator(
|
||||
limit_value=Decimal("0.00"),
|
||||
),
|
||||
],
|
||||
verbose_name=_("Tax rate"),
|
||||
verbose_name=_("Tax rate")
|
||||
)
|
||||
price_includes_tax = models.BooleanField(
|
||||
verbose_name=_("The configured product prices include the tax amount"),
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
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.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User, Voucher
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from ...helpers.format import format_map
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation
|
||||
@@ -216,74 +213,15 @@ class WaitingListEntry(LoggedModel):
|
||||
self.voucher = v
|
||||
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):
|
||||
recipient = self.email
|
||||
|
||||
try:
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
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,
|
||||
}
|
||||
)
|
||||
mail(
|
||||
self.email,
|
||||
self.event.settings.mail_subject_waiting_list,
|
||||
self.event.settings.mail_text_waiting_list,
|
||||
get_email_context(event=self.event, waiting_list_entry=self),
|
||||
self.event,
|
||||
locale=self.locale
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_itemvar(event, item, variation):
|
||||
|
||||
+10
-10
@@ -325,6 +325,16 @@ class BasePaymentProvider:
|
||||
help_text=_('Users will not be able to choose this payment provider after the given date.'),
|
||||
required=False,
|
||||
)),
|
||||
('_invoice_text',
|
||||
I18nFormField(
|
||||
label=_('Text on invoices'),
|
||||
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
|
||||
'This will only be used if the invoice is generated before the order is paid. If the '
|
||||
'invoice is generated later, it will show a text stating that it has already been paid.'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}}
|
||||
)),
|
||||
('_total_min',
|
||||
forms.DecimalField(
|
||||
label=_('Minimum order total'),
|
||||
@@ -372,16 +382,6 @@ class BasePaymentProvider:
|
||||
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
|
||||
required=False
|
||||
)),
|
||||
('_invoice_text',
|
||||
I18nFormField(
|
||||
label=_('Text on invoices'),
|
||||
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
|
||||
'This will only be used if the invoice is generated before the order is paid. If the '
|
||||
'invoice is generated later, it will show a text stating that it has already been paid.'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}}
|
||||
)),
|
||||
('_restricted_countries',
|
||||
forms.MultipleChoiceField(
|
||||
label=_('Restrict to countries'),
|
||||
|
||||
+2
-25
@@ -35,7 +35,6 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -47,15 +46,12 @@ from collections import OrderedDict
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
import jsonschema
|
||||
from arabic_reshaper import ArabicReshaper
|
||||
from bidi.algorithm import get_display
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max, Min
|
||||
from django.dispatch import receiver
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
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_i18n':
|
||||
text = str(LazyI18nString(o.get('text_i18n', {})))
|
||||
text = str(LazyI18nString(o['text_i18n']))
|
||||
else:
|
||||
text = o.get('text', '')
|
||||
text = o['text']
|
||||
|
||||
def replace(x):
|
||||
if x.group(1).startswith('itemmeta:'):
|
||||
@@ -979,22 +975,3 @@ class Renderer:
|
||||
output.write(outbuffer)
|
||||
outbuffer.seek(0)
|
||||
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))
|
||||
|
||||
@@ -65,14 +65,7 @@ def get_all_plugins(event=None) -> List[type]:
|
||||
)
|
||||
|
||||
|
||||
class PluginConfigMeta(type):
|
||||
def __getattribute__(cls, item):
|
||||
if item == "default" and cls is PluginConfig:
|
||||
return False
|
||||
return super().__getattribute__(item)
|
||||
|
||||
|
||||
class PluginConfig(AppConfig, metaclass=PluginConfigMeta):
|
||||
class PluginConfig(AppConfig):
|
||||
IGNORE = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -59,10 +59,10 @@ class RelativeDateWrapper:
|
||||
def date(self, event) -> datetime.date:
|
||||
from .models import SubEvent
|
||||
|
||||
if isinstance(self.data, datetime.datetime):
|
||||
return self.data.date()
|
||||
elif isinstance(self.data, datetime.date):
|
||||
if isinstance(self.data, datetime.date):
|
||||
return self.data
|
||||
elif isinstance(self.data, datetime.datetime):
|
||||
return self.data.date()
|
||||
else:
|
||||
if self.data.minutes_before is not None:
|
||||
raise ValueError('A minute-based relative datetime can not be used as a date')
|
||||
|
||||
@@ -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 '
|
||||
'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_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.'),
|
||||
'seat_required': _('You need to select a specific seat.'),
|
||||
'seat_invalid': _('Please select a valid seat.'),
|
||||
|
||||
@@ -19,49 +19,31 @@
|
||||
# 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 datetime import timedelta
|
||||
from typing import Any, Dict, Union
|
||||
from typing import Any, Dict
|
||||
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import connection, transaction
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now, override
|
||||
from django.utils.timezone import override
|
||||
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.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
|
||||
User, cachedfile_name,
|
||||
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
|
||||
)
|
||||
from pretix.base.models.exports import ScheduledOrganizerExport
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import (
|
||||
EventTask, OrganizerTask, ProfiledEventTask, ProfiledOrganizerUserTask,
|
||||
ProfiledEventTask, ProfiledOrganizerUserTask,
|
||||
)
|
||||
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.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportError(LazyLocaleException):
|
||||
pass
|
||||
|
||||
|
||||
class ExportEmptyError(ExportError):
|
||||
pass
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(ExportError,), bind=True)
|
||||
def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
||||
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)
|
||||
with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
|
||||
responses = register_data_exporters.send(event)
|
||||
for recv, response in responses:
|
||||
for receiver, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
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
|
||||
region = organizer.settings.region
|
||||
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):
|
||||
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
|
||||
else:
|
||||
events = allowed_events.filter(pk__in=form_data.get('events'), organizer=organizer)
|
||||
events = allowed_events.filter(pk__in=form_data.get('events'))
|
||||
else:
|
||||
events = allowed_events.filter(organizer=organizer)
|
||||
events = allowed_events
|
||||
responses = register_multievent_data_exporters.send(organizer)
|
||||
|
||||
for recv, response in responses:
|
||||
for receiver, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
ex = response(events, organizer, set_progress)
|
||||
@@ -156,210 +138,3 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
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'])
|
||||
|
||||
@@ -452,7 +452,7 @@ def build_preview_invoice_pdf(event):
|
||||
|
||||
if event.tax_rules.exists():
|
||||
for i, tr in enumerate(event.tax_rules.all()):
|
||||
for j in range(5):
|
||||
for j in range(150):
|
||||
tax = tr.tax(Decimal('100.00'), base_price_is='gross')
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||
@@ -460,7 +460,7 @@ def build_preview_invoice_pdf(event):
|
||||
tax_rate=tax.rate
|
||||
)
|
||||
else:
|
||||
for i in range(5):
|
||||
for i in range(150):
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice, description=_("Sample product A"),
|
||||
gross_value=100, tax_value=0, tax_rate=0
|
||||
|
||||
@@ -86,6 +86,7 @@ INVALID_ADDRESS = 'invalid-pretix-mail-address'
|
||||
|
||||
|
||||
class TolerantDict(dict):
|
||||
# kept for backwards compatibility with plugins
|
||||
|
||||
def __missing__(self, key):
|
||||
return key
|
||||
@@ -99,8 +100,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = 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,
|
||||
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):
|
||||
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None):
|
||||
"""
|
||||
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
|
||||
|
||||
@@ -150,21 +150,12 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
|
||||
:param attach_other_files: A list of file paths on our storage to attach.
|
||||
|
||||
:param plain_text_only: If set to ``True``, rendering a HTML version will be skipped.
|
||||
|
||||
:param no_order_links: If set to ``True``, no link to the order confirmation page will be auto-appended. Currently
|
||||
only allowed to use together with ``plain_text_only`` since HTML renderers add their own
|
||||
links.
|
||||
|
||||
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
|
||||
that the email has been sent, just that it has been queued by the email backend.
|
||||
"""
|
||||
if email == INVALID_ADDRESS:
|
||||
return
|
||||
|
||||
if no_order_links and not plain_text_only:
|
||||
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
|
||||
|
||||
headers = headers or {}
|
||||
if auto_email:
|
||||
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
|
||||
@@ -188,7 +179,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
})
|
||||
renderer = ClassicMailRenderer(None, organizer)
|
||||
content_plain = body_plain = render_mail(template, context)
|
||||
subject = str(subject).format_map(TolerantDict(context))
|
||||
subject = format_map(str(subject), context)
|
||||
sender = (
|
||||
sender or
|
||||
(event.settings.get('mail_from') if event else None) or
|
||||
@@ -211,7 +202,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900]
|
||||
signature = ""
|
||||
|
||||
bcc = list(bcc or [])
|
||||
bcc = []
|
||||
|
||||
settings_holder = event or organizer
|
||||
|
||||
@@ -253,7 +244,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if order and order.testmode:
|
||||
subject = "[TESTMODE] " + subject
|
||||
|
||||
if order and position and not no_order_links:
|
||||
if order and position:
|
||||
body_plain += _(
|
||||
"You are receiving this email because someone placed an order for {event} for you."
|
||||
).format(event=event.name)
|
||||
@@ -269,7 +260,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
}
|
||||
)
|
||||
)
|
||||
elif order and not no_order_links:
|
||||
elif order:
|
||||
body_plain += _(
|
||||
"You are receiving this email because you placed an order for {event}."
|
||||
).format(event=event.name)
|
||||
@@ -289,9 +280,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
|
||||
with override(timezone):
|
||||
try:
|
||||
if plain_text_only:
|
||||
body_html = None
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
if 'position' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
@@ -305,7 +294,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
to=[email] if isinstance(email, str) else list(email),
|
||||
cc=cc,
|
||||
bcc=bcc,
|
||||
subject=subject,
|
||||
body=body_plain,
|
||||
@@ -358,11 +346,11 @@ class CustomEmail(EmailMultiAlternatives):
|
||||
|
||||
@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,
|
||||
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,
|
||||
organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None,
|
||||
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:
|
||||
html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
|
||||
html_with_cid, cid_images = replace_images_with_cid_paths(html)
|
||||
|
||||
@@ -1068,8 +1068,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
any_payment_failed = True
|
||||
except Exception:
|
||||
logger.exception('Error during payment attempt')
|
||||
else:
|
||||
order.refresh_from_db()
|
||||
|
||||
pending_sum = order.pending_sum
|
||||
free_order_flow = (
|
||||
@@ -1343,7 +1341,6 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
||||
|
||||
class OrderChangeManager:
|
||||
error_messages = {
|
||||
'order_canceled': _('You cannot change a canceled order.'),
|
||||
'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_missing': _('There is no quota defined that allows this operation.'),
|
||||
@@ -1391,9 +1388,6 @@ class OrderChangeManager:
|
||||
self._invoice_dirty = False
|
||||
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]):
|
||||
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||
raise OrderError(self.error_messages['product_without_variation'])
|
||||
|
||||
@@ -109,31 +109,6 @@ class EventTask(app.Task):
|
||||
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):
|
||||
def __call__(self, *args, **kwargs):
|
||||
organizer_id = kwargs['organizer']
|
||||
|
||||
@@ -206,7 +206,7 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
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': {
|
||||
@@ -229,10 +229,10 @@ DEFAULTS = {
|
||||
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 "
|
||||
"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, "
|
||||
"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': {
|
||||
@@ -242,7 +242,7 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
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 "
|
||||
"required regardless of this setting."),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
|
||||
@@ -2574,7 +2574,7 @@ Your {organizer} team"""))
|
||||
label=_("Attendee data explanation"),
|
||||
widget=I18nTextarea,
|
||||
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.")
|
||||
)
|
||||
},
|
||||
|
||||
@@ -218,10 +218,8 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
o.meta_info = json.dumps(d)
|
||||
o.save(update_fields=['meta_info', 'email', 'customer'])
|
||||
|
||||
for le in self.event.logentry_set.filter(
|
||||
Q(action_type__contains="order.email") | Q(action_type__contains="position.email"),
|
||||
):
|
||||
shred_log_fields(le, banlist=['recipient', 'message', 'subject', 'full_mail'])
|
||||
for le in self.event.logentry_set.filter(action_type__contains="order.email"):
|
||||
shred_log_fields(le, banlist=['recipient', 'message', 'subject'])
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"):
|
||||
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 %}
|
||||
@@ -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
|
||||
@@ -19,12 +19,6 @@
|
||||
# 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 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
|
||||
# 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
|
||||
# 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:
|
||||
|
||||
@@ -102,18 +101,3 @@ class EmailBanlistValidator(BanlistValidator):
|
||||
banlist = [
|
||||
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.")
|
||||
|
||||
@@ -78,7 +78,7 @@ class BaseQuestionsViewMixin:
|
||||
form.pos = cartpos or orderpos
|
||||
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
|
||||
(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_emails_asked or
|
||||
self.request.event.settings.attendee_company_asked or
|
||||
|
||||
@@ -28,7 +28,7 @@ from django.urls import reverse
|
||||
|
||||
|
||||
def _is_samesite_referer(request):
|
||||
referer = request.headers.get('referer')
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
if referer is None:
|
||||
return False
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from celery import states
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
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.shortcuts import redirect, render
|
||||
from django.test import RequestFactory
|
||||
@@ -149,8 +149,6 @@ class AsyncMixin:
|
||||
return redirect(self.get_success_url(value))
|
||||
|
||||
def error(self, exception):
|
||||
if isinstance(exception, PermissionDenied):
|
||||
raise exception
|
||||
messages.error(self.request, self.get_error_message(exception))
|
||||
if "ajax" in self.request.POST or "ajax" in self.request.GET:
|
||||
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
|
||||
not supported.
|
||||
"""
|
||||
known_errortypes = ['ValidationError', 'PermissionDenied']
|
||||
expected_exceptions = (ValidationError, PermissionDenied)
|
||||
known_errortypes = ['ValidationError']
|
||||
expected_exceptions = (ValidationError,)
|
||||
task_base = ProfiledEventTask
|
||||
|
||||
def async_set_progress(self, percentage):
|
||||
|
||||
@@ -51,9 +51,6 @@ from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
|
||||
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
|
||||
from ...base.forms.widgets import ( # noqa
|
||||
@@ -217,16 +214,12 @@ class ExtValidationMixin:
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
data = super().clean(*args, **kwargs)
|
||||
if isinstance(data, UploadedFile):
|
||||
if isinstance(data, File):
|
||||
filename = data.name
|
||||
ext = os.path.splitext(filename)[1]
|
||||
ext = ext.lower()
|
||||
if ext not in self.ext_whitelist:
|
||||
raise forms.ValidationError(_("Filetype not allowed!"))
|
||||
|
||||
if ext in IMAGE_EXTS:
|
||||
validate_uploaded_file_for_valid_image(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -34,13 +34,12 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
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.forms import (
|
||||
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
|
||||
@@ -66,7 +65,6 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||
)
|
||||
from pretix.base.validators import multimail_validate
|
||||
from pretix.control.forms import (
|
||||
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
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 "
|
||||
"here in percent. If you have a more complicated tax situation, you can add more tax rates and "
|
||||
"detailed configuration later."),
|
||||
max_value=Decimal("100.00"),
|
||||
min_value=Decimal("0.00"),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -865,6 +861,13 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
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):
|
||||
if "web" not in val:
|
||||
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_subject_resend_link': ['event', 'order'],
|
||||
'mail_subject_resend_link_attendee': ['event', 'order'],
|
||||
'mail_text_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
|
||||
'mail_subject_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'],
|
||||
'mail_text_resend_all_links': ['event', 'orders'],
|
||||
'mail_subject_resend_all_links': ['event', 'orders'],
|
||||
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
||||
|
||||
@@ -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(' ', '')
|
||||
@@ -77,31 +77,6 @@ def get_all_payment_providers():
|
||||
if PAYMENT_PROVIDERS:
|
||||
return PAYMENT_PROVIDERS
|
||||
|
||||
class FakeSettings:
|
||||
def __init__(self, orig_settings):
|
||||
self.orig_settings = orig_settings
|
||||
|
||||
def set(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.orig_settings, item)
|
||||
|
||||
class FakeEvent:
|
||||
def __init__(self, orig_event):
|
||||
self.orig_event = orig_event
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return FakeSettings(self.orig_event.settings)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.orig_event, item)
|
||||
|
||||
@property
|
||||
def __class__(self): # hackhack
|
||||
return Event
|
||||
|
||||
with rolledback_transaction():
|
||||
event = Event.objects.create(
|
||||
plugins=",".join([app.name for app in apps.get_app_configs()]),
|
||||
@@ -109,7 +84,6 @@ def get_all_payment_providers():
|
||||
date_from=now(),
|
||||
organizer=Organizer.objects.create(name="INTERNAL")
|
||||
)
|
||||
event = FakeEvent(event)
|
||||
provs = register_payment_providers.send(
|
||||
sender=event
|
||||
)
|
||||
@@ -792,12 +766,6 @@ class OrderSearchFilterForm(OrderFilterForm):
|
||||
)
|
||||
)
|
||||
|
||||
def use_query_hack(self):
|
||||
return (
|
||||
self.cleaned_data.get('query') or
|
||||
self.cleaned_data.get('status') in ('overpaid', 'partially_paid', 'underpaid', 'pendingpaid')
|
||||
)
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
qs = super().filter_qs(qs)
|
||||
@@ -2264,7 +2232,7 @@ class DeviceFilterForm(FilterForm):
|
||||
state = forms.ChoiceField(
|
||||
label=_('Device status'),
|
||||
choices=[
|
||||
('all', _('All devices')),
|
||||
('', _('All devices')),
|
||||
('active', _('Active devices')),
|
||||
('revoked', _('Revoked devices'))
|
||||
],
|
||||
|
||||
@@ -295,7 +295,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
self.user = kwargs.pop('user')
|
||||
kwargs.setdefault('initial', {})
|
||||
kwargs['initial'].setdefault('admission', True)
|
||||
kwargs['initial'].setdefault('personalized', True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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.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)
|
||||
|
||||
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):
|
||||
@@ -497,7 +494,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
'internal_name',
|
||||
'category',
|
||||
'admission',
|
||||
'personalized',
|
||||
'default_price',
|
||||
'tax_rule',
|
||||
]
|
||||
@@ -592,33 +588,13 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'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.")
|
||||
)
|
||||
if d.get('admission'):
|
||||
if d['admission']:
|
||||
self.add_error(
|
||||
'admission',
|
||||
_(
|
||||
"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
|
||||
|
||||
def clean_picture(self):
|
||||
@@ -639,7 +615,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'active',
|
||||
'sales_channels',
|
||||
'admission',
|
||||
'personalized',
|
||||
'description',
|
||||
'picture',
|
||||
'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):
|
||||
instance = super().save(commit)
|
||||
self.meta_fields = []
|
||||
|
||||
@@ -227,10 +227,6 @@ class ExporterForm(forms.Form):
|
||||
elif isinstance(v, models.QuerySet):
|
||||
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
|
||||
|
||||
|
||||
@@ -270,7 +266,7 @@ class OtherOperationsForm(forms.Form):
|
||||
notify = forms.BooleanField(
|
||||
label=_('Notify user'),
|
||||
required=False,
|
||||
initial=False,
|
||||
initial=True,
|
||||
help_text=_(
|
||||
'Send an email to the customer notifying that their order has been changed.'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -19,15 +19,17 @@
|
||||
# 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
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django import forms
|
||||
from django.forms import formset_factory
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.urls import reverse
|
||||
from django.utils.dates import MONTHS, WEEKDAYS
|
||||
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 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.templatetags.money import money_filter
|
||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
|
||||
|
||||
@@ -439,15 +440,166 @@ class CheckinListFormSet(I18nInlineFormSet):
|
||||
return form
|
||||
|
||||
|
||||
class RRuleFormSetForm(RRuleForm):
|
||||
class RRuleForm(forms.Form):
|
||||
# TODO: calendar.setfirstweekday
|
||||
exclude = forms.BooleanField(
|
||||
label=_('Exclude these dates instead of adding them.'),
|
||||
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(
|
||||
RRuleFormSetForm,
|
||||
RRuleForm,
|
||||
can_order=False, can_delete=True, extra=1
|
||||
)
|
||||
|
||||
|
||||
@@ -315,11 +315,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.organizer.changed': _('The organizer has been changed.'),
|
||||
'pretix.organizer.settings': _('The organizer settings 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.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||
'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.canceled': _('Refund {local_id} has been canceled.'),
|
||||
'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.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
|
||||
'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/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/rrule.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/variations.js" %}"></script>
|
||||
|
||||
@@ -82,10 +82,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<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" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
{% endif %}
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
<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>
|
||||
@@ -127,7 +125,7 @@
|
||||
{% for e in entries %}
|
||||
<tr>
|
||||
<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 }}"/>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -200,7 +198,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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">
|
||||
<span class="fa fa-sign-in" aria-hidden="true"></span>
|
||||
{% trans "Check-In selected attendees" %}
|
||||
@@ -209,8 +207,6 @@
|
||||
<span class="fa fa-sign-out" aria-hidden="true"></span>
|
||||
{% trans "Check-Out selected attendees" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
<button type="submit" class="btn btn-danger btn-save" name="revert" value="true">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
{% trans "Delete all check-ins of selected attendees" %}
|
||||
|
||||
@@ -36,16 +36,12 @@
|
||||
</div>
|
||||
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
|
||||
{% if request.event %}
|
||||
{% for l, n in settings.LANGUAGES %}
|
||||
{% if l in request.event.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endif %}
|
||||
{% for l in request.event.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for l, n in settings.LANGUAGES %}
|
||||
{% if l in request.organizer.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endif %}
|
||||
{% for l in request.organizer.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</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_emails_asked_required layout="control" %}
|
||||
|
||||
@@ -20,19 +20,13 @@
|
||||
<div class="col-md-9">
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on">
|
||||
<span class="fa fa-fw fa-user"></span>
|
||||
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
|
||||
<span class="fa fa-user"></span>
|
||||
<strong>{% trans "Admission product" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
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.
|
||||
{% 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.
|
||||
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="help-block">
|
||||
@@ -46,12 +40,12 @@
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<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>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
A product that does not represent a person. By default, we will not offer ticket downloads
|
||||
(but you can still enable ticket downloads in event settings or product settings).
|
||||
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
|
||||
ticket downloads.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="help-block">
|
||||
@@ -64,47 +58,6 @@
|
||||
</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" %}
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -27,19 +27,13 @@
|
||||
{% endfor %}
|
||||
<div class="big-radio radio">
|
||||
<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>
|
||||
<strong>{% trans "Admission product" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
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.
|
||||
{% 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.
|
||||
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="help-block">
|
||||
@@ -57,8 +51,8 @@
|
||||
<strong>{% trans "Non-admission product" %}</strong>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
A product that does not represent a person. By default, we will not offer ticket downloads
|
||||
(but you can still enable ticket downloads in event settings or product settings).
|
||||
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
|
||||
ticket downloads.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="help-block">
|
||||
@@ -71,52 +65,6 @@
|
||||
</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.picture layout="control" %}
|
||||
{% bootstrap_field form.require_approval layout="control" %}
|
||||
|
||||
@@ -81,11 +81,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if i.admission %}
|
||||
{% if i.personalized %}
|
||||
<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 %}
|
||||
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket" %}"></span>
|
||||
{% elif i.issue_giftcard %}
|
||||
<span class="fa fa-gift fa-fw text-muted" data-toggle="tooltip" title="{% trans "Gift card" %}"></span>
|
||||
{% endif %}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<small>{% trans "All personalized products" %}</small>
|
||||
<small>{% trans "All admission products" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="dnd-container">
|
||||
|
||||
@@ -184,11 +184,9 @@
|
||||
<dt>{% trans "Order locale" %}</dt>
|
||||
<dd>
|
||||
{{ 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">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<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>
|
||||
</a>
|
||||
</dd>
|
||||
{% if order.status == "n" %}
|
||||
<dt>{% trans "Expiry date" %}</dt>
|
||||
@@ -208,11 +206,9 @@
|
||||
{{ order.customer.identifier }} – {{ order.customer.email }}
|
||||
</a>
|
||||
{% 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">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<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>
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Contact email" %}</dt>
|
||||
@@ -221,23 +217,21 @@
|
||||
{% 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>
|
||||
{% 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">
|
||||
<span class="fa fa-edit"></span>
|
||||
<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>
|
||||
</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>
|
||||
{% 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>
|
||||
{% if order.status != "c" %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if order.status != "c" %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
@@ -245,11 +239,9 @@
|
||||
<dt>{% trans "Phone number" %}</dt>
|
||||
<dd>
|
||||
{{ 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">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<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>
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if invoices %}
|
||||
@@ -314,7 +306,7 @@
|
||||
{% trans "Email invoices" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_generate_invoice and 'can_change_orders' in request.eventpermset %}
|
||||
{% if can_generate_invoice %}
|
||||
<br/>
|
||||
<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 %}">
|
||||
@@ -325,7 +317,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% elif can_generate_invoice and 'can_change_orders' in request.eventpermset %}
|
||||
{% elif can_generate_invoice %}
|
||||
<dt>{% trans "Invoices" %}</dt>
|
||||
<dd>
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
@@ -343,11 +335,11 @@
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<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 %}
|
||||
<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>
|
||||
· <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>
|
||||
{% trans "Change products" %}
|
||||
@@ -468,12 +460,12 @@
|
||||
{% endif %}
|
||||
{% if line.has_questions %}
|
||||
<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>
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
|
||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
{% 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>
|
||||
<dd>
|
||||
{% if line.attendee_email %}
|
||||
@@ -496,7 +488,7 @@
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
|
||||
{% if line.item.admission and event.settings.attendee_company_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee company" %}
|
||||
</dt>
|
||||
@@ -504,7 +496,7 @@
|
||||
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
|
||||
{% if line.item.admission and event.settings.attendee_addresses_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee address" %}
|
||||
</dt>
|
||||
@@ -762,7 +754,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</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">
|
||||
{% trans "Create a refund" %}
|
||||
</a>
|
||||
@@ -950,11 +942,9 @@
|
||||
{% bootstrap_field comment_form.comment show_help=True show_label=False %}
|
||||
{% bootstrap_field comment_form.custom_followup_at %}
|
||||
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,113 +4,35 @@
|
||||
{% load order_overview %}
|
||||
{% block title %}{% trans "Data export" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
<h1>
|
||||
{% trans "Data export" %}
|
||||
{% if "identifier" in request.GET %}
|
||||
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if scheduled %}
|
||||
<h2>{% trans "Scheduled exports" %}</h2>
|
||||
<ul class="list-group">
|
||||
{% for s in scheduled %}
|
||||
<li class="list-group-item logentry">
|
||||
<div class="row">
|
||||
<div class="col-lg-5 col-md-4 col-xs-12">
|
||||
<span class="fa fa-fw fa-folder"></span>
|
||||
{{ s.export_verbose_name }}
|
||||
<br>
|
||||
<span class="text-muted">
|
||||
<span class="fa fa-fw fa-user"></span>
|
||||
{{ s.owner.fullname|default:s.owner.email }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-lg-5 col-md-6 col-xs-12">
|
||||
{% if s.schedule_next_run %}
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
{% trans "Next run:" %}
|
||||
{{ s.schedule_next_run|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
{% trans "No next run scheduled" %}
|
||||
{% 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: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>
|
||||
{% for e in exporters %}
|
||||
<details class="panel panel-default"
|
||||
{% if request.GET.identifier == e.identifier or request.POST.exporter == e.identifier %}open{% endif %}>
|
||||
<summary class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{ e.verbose_name }}
|
||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||
</h3>
|
||||
</summary>
|
||||
<div id="{{ e.identifier }}">
|
||||
<div class="panel-body">
|
||||
<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="{{ e.identifier }}" />
|
||||
{% bootstrap_form e.form layout='control' %}
|
||||
<button class="btn btn-primary pull-right flip" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start export" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
@@ -22,72 +22,54 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="" method="post" class="row">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<dl class="dl-horizontal col-lg-6 col-sm-12">
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>#{{ customer.identifier }}</dd>
|
||||
{% if customer.provider %}
|
||||
<dt>{% trans "SSO provider" %}</dt>
|
||||
<dd>{{ customer.provider.name }}</dd>
|
||||
{% endif %}
|
||||
{% if customer.external_identifier %}
|
||||
<dt>{% trans "External identifier" %}</dt>
|
||||
<dd>{{ customer.external_identifier }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not customer.is_active %}
|
||||
{% trans "disabled" %}
|
||||
{% elif not customer.is_verified %}
|
||||
{% trans "not yet activated" %}
|
||||
{% else %}
|
||||
{% trans "active" %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "E-mail" %}</dt>
|
||||
<dd>
|
||||
{{ customer.email|default_if_none:"" }}
|
||||
{% if customer.email and not customer.provider %}
|
||||
<button type="submit" name="action" value="pwreset" class="btn btn-xs btn-default">
|
||||
{% trans "Send password reset link" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ customer.name }}</dd>
|
||||
{% if customer.phone %}
|
||||
<dt>{% trans "Phone" %}</dt>
|
||||
<dd>{{ customer.phone }}</dd>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>#{{ customer.identifier }}</dd>
|
||||
{% if customer.provider %}
|
||||
<dt>{% trans "SSO provider" %}</dt>
|
||||
<dd>{{ customer.provider.name }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Locale" %}</dt>
|
||||
<dd>{{ display_locale }}</dd>
|
||||
<dt>{% trans "Registration date" %}</dt>
|
||||
<dd>{{ customer.date_joined|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Last login" %}</dt>
|
||||
<dd>{% if customer.last_login %}{{ customer.last_login|date:"SHORT_DATETIME_FORMAT" }}{% else %}
|
||||
–{% endif %}</dd>
|
||||
{% if customer.notes %}
|
||||
<dt>{% trans "Notes" %}</dt>
|
||||
<dd>{{ customer.notes|linebreaks }}</dd>
|
||||
{% if customer.external_identifier %}
|
||||
<dt>{% trans "External identifier" %}</dt>
|
||||
<dd>{{ customer.external_identifier }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
<dl class="col-lg-6 col-sm-12 text-right">
|
||||
<dt class="text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "This includes all paid orders by this customer across all your events." %}">
|
||||
{% trans "Lifetime spending" %}
|
||||
</dt>
|
||||
{% if lifetime_spending %}
|
||||
{% for s in lifetime_spending %}
|
||||
{% if s.spending >= 0 %}
|
||||
<dd class="text-success text-h3">{{ s.spending|money:s.currency }}</dd>
|
||||
{% elif s.spending < 0 %}
|
||||
<dd class="text-error text-h3">{{ s.spending|money:s.currency }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not customer.is_active %}
|
||||
{% trans "disabled" %}
|
||||
{% elif not customer.is_verified %}
|
||||
{% trans "not yet activated" %}
|
||||
{% else %}
|
||||
{% trans "active" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<dd class="text-muted text-h3">{{ 0|floatformat:2 }}</dd>
|
||||
</dd>
|
||||
<dt>{% trans "E-mail" %}</dt>
|
||||
<dd>
|
||||
{{ customer.email|default_if_none:"" }}
|
||||
{% if customer.email and not customer.provider %}
|
||||
<button type="submit" name="action" value="pwreset" class="btn btn-xs btn-default">
|
||||
{% trans "Send password reset link" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ customer.name }}</dd>
|
||||
{% if customer.phone %}
|
||||
<dt>{% trans "Phone" %}</dt>
|
||||
<dd>{{ customer.phone }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Locale" %}</dt>
|
||||
<dd>{{ display_locale }}</dd>
|
||||
<dt>{% trans "Registration date" %}</dt>
|
||||
<dd>{{ customer.date_joined|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Last login" %}</dt>
|
||||
<dd>{% if customer.last_login %}{{ customer.last_login|date:"SHORT_DATETIME_FORMAT" }}{% else %}
|
||||
–{% endif %}</dd>
|
||||
{% if customer.notes %}
|
||||
<dt>{% trans "Notes" %}</dt>
|
||||
<dd>{{ customer.notes|linebreaks }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</form>
|
||||
|
||||
@@ -1,110 +1,37 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load order_overview %}
|
||||
{% block title %}{% trans "Data export" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
<h1>
|
||||
{% trans "Data export" %}
|
||||
{% if "identifier" in request.GET %}
|
||||
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if scheduled %}
|
||||
<h2>{% trans "Scheduled exports" %}</h2>
|
||||
<ul class="list-group">
|
||||
{% for s in scheduled %}
|
||||
<li class="list-group-item logentry">
|
||||
<div class="row">
|
||||
<div class="col-lg-5 col-md-4 col-xs-12">
|
||||
<span class="fa fa-fw fa-folder"></span>
|
||||
{{ s.export_verbose_name }}
|
||||
<br>
|
||||
<span class="text-muted">
|
||||
<span class="fa fa-fw fa-user"></span>
|
||||
{{ s.owner.fullname|default:s.owner.email }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-lg-5 col-md-6 col-xs-12">
|
||||
{% if s.schedule_next_run %}
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
{% trans "Next run:" %}
|
||||
{{ s.schedule_next_run|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
{% trans "No next run scheduled" %}
|
||||
{% 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>
|
||||
{% for e in exporters %}
|
||||
<details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
|
||||
<summary class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{ e.verbose_name }}
|
||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||
</h3>
|
||||
</summary>
|
||||
<div id="{{ e.identifier }}">
|
||||
<div class="panel-body">
|
||||
<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="{{ e.identifier }}" />
|
||||
{% bootstrap_form e.form layout='control' %}
|
||||
<button class="btn btn-primary pull-right flip" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start export" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
</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 "ajv/ajv2020.bundle.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>
|
||||
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django.conf.urls import re_path
|
||||
from django.urls import include
|
||||
from django.conf.urls import include, re_path
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from pretix.control.views import (
|
||||
@@ -207,10 +206,6 @@ urlpatterns = [
|
||||
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/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'^events/$', main.EventList.as_view(), name='events'),
|
||||
re_path(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
|
||||
@@ -390,8 +385,6 @@ urlpatterns = [
|
||||
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/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/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||
re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
||||
|
||||
@@ -353,12 +353,6 @@ class Recover(TemplateView):
|
||||
user.save()
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
has_redis = settings.HAS_REDIS
|
||||
if has_redis:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
rc.delete('pretix_pwreset_%s' % user.id)
|
||||
return redirect('control:auth.login')
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
import dateutil.parser
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import Exists, Max, OuterRef, Prefetch, Subquery
|
||||
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.timezone import is_aware, make_aware, now
|
||||
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 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.views import CreateView, PaginationMixin, UpdateView
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
|
||||
@@ -173,7 +171,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
||||
|
||||
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
|
||||
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
||||
permission = ('can_change_orders', 'can_checkin_orders')
|
||||
permission = 'can_change_orders'
|
||||
context_object_name = 'device'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@@ -183,16 +181,11 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(None).order_by()
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_success_url(None)
|
||||
|
||||
@transaction.atomic()
|
||||
def async_post(self, request, *args, **kwargs):
|
||||
self.list = get_object_or_404(request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||
positions = self.get_queryset()
|
||||
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:
|
||||
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()
|
||||
@@ -395,7 +388,7 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class CheckinListDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = CheckinList
|
||||
template_name = 'pretixcontrol/checkin/list_delete.html'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@@ -51,7 +51,7 @@ from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, ngettext, pgettext
|
||||
from django.utils.translation import gettext_lazy as _, pgettext, ungettext
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
@@ -555,7 +555,7 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
}),
|
||||
orders_text=ngettext('{num} order', '{num} orders', event.order_count or 0).format(
|
||||
orders_text=ungettext('{num} order', '{num} orders', event.order_count or 0).format(
|
||||
num=event.order_count or 0
|
||||
)
|
||||
) if user.has_active_staff_session(request.session.session_key) or event.pk in events_with_orders else ''
|
||||
|
||||
@@ -35,6 +35,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import DeleteView
|
||||
|
||||
from pretix.base.models import CartPosition, Discount
|
||||
from pretix.control.forms.discounts import DiscountForm
|
||||
@@ -44,11 +45,10 @@ from pretix.control.permissions import (
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...base.channels import get_all_sales_channels
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
class DiscountDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class DiscountDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Discount
|
||||
template_name = 'pretixcontrol/items/discount_delete.html'
|
||||
permission = 'can_change_items'
|
||||
|
||||
@@ -58,7 +58,7 @@ from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
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.detail import SingleObjectMixin
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -94,7 +94,6 @@ from ...base.models.items import (
|
||||
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
||||
)
|
||||
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from ...helpers.format import format_map
|
||||
from ..logdisplay import OVERVIEW_BANLIST
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
@@ -1244,7 +1243,7 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, DeleteView):
|
||||
model = TaxRule
|
||||
template_name = 'pretixcontrol/event/tax_delete.html'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@@ -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.generic import ListView
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
from django.views.generic.edit import DeleteView
|
||||
from django_countries.fields import Country
|
||||
|
||||
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 ...base.channels import get_all_sales_channels
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
@@ -189,8 +189,9 @@ def reorder_items(request, organizer, event):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
class CategoryDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = ItemCategory
|
||||
form_class = CategoryForm
|
||||
template_name = 'pretixcontrol/items/category_delete.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'category'
|
||||
@@ -522,7 +523,7 @@ def reorder_questions(request, organizer, event):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Question
|
||||
template_name = 'pretixcontrol/items/question_delete.html'
|
||||
permission = 'can_change_items'
|
||||
@@ -1089,7 +1090,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class QuotaDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Quota
|
||||
template_name = 'pretixcontrol/items/quota_delete.html'
|
||||
permission = 'can_change_items'
|
||||
@@ -1203,7 +1204,7 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
initial['tax_rule'] = trs[0]
|
||||
|
||||
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:
|
||||
initial[f] = getattr(self.copy_from, f)
|
||||
initial['copy_from'] = self.copy_from
|
||||
@@ -1463,7 +1464,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
return f
|
||||
|
||||
|
||||
class ItemDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Item
|
||||
template_name = 'pretixcontrol/item/delete.html'
|
||||
permission = 'can_change_items'
|
||||
|
||||
@@ -127,9 +127,6 @@ class OAuthApplicationDeleteView(ApplicationDelete):
|
||||
self.object.save()
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
def form_valid(self, form):
|
||||
return self.delete(self.request, self.args, self.kwargs)
|
||||
|
||||
|
||||
class AuthorizationListView(ListView):
|
||||
template_name = 'pretixcontrol/oauth/authorized.html'
|
||||
|
||||
@@ -62,7 +62,7 @@ from django.urls import reverse
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format, get_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext, gettext_lazy as _, ngettext
|
||||
from django.views.generic import (
|
||||
@@ -77,7 +77,7 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedFile, CachedTicket, Checkin, Invoice,
|
||||
InvoiceAddress, Item, ItemVariation, LogEntry, Order, QuestionAnswer,
|
||||
Quota, ScheduledEventExport, generate_secret,
|
||||
Quota, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
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.services import tickets
|
||||
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 (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
|
||||
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.views.mixins import OrderQuestionsViewMixin
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.exports import ScheduledEventExportForm
|
||||
from pretix.control.forms.filter import (
|
||||
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
||||
RefundFilterForm,
|
||||
@@ -123,11 +122,9 @@ from pretix.control.forms.orders import (
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import order_search_forms
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.safedownload import check_token
|
||||
from pretix.presale.signals import question_form_fields
|
||||
@@ -386,8 +383,8 @@ class OrderDetail(OrderView):
|
||||
|
||||
p.has_questions = (
|
||||
p.additional_fields or
|
||||
(p.item.ask_attendee_data 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_names_asked) or
|
||||
(p.item.admission and self.request.event.settings.attendee_emails_asked) or
|
||||
p.item.questions.all()
|
||||
)
|
||||
p.cache_answers()
|
||||
@@ -683,7 +680,7 @@ class OrderRefundCancel(OrderView):
|
||||
messages.success(self.request, _('The refund has been canceled.'))
|
||||
else:
|
||||
messages.error(self.request, _('This refund can not be canceled at the moment.'))
|
||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect(self.request.GET.get("next"))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
@@ -719,7 +716,7 @@ class OrderRefundProcess(OrderView):
|
||||
messages.success(self.request, _('The refund has been processed.'))
|
||||
else:
|
||||
messages.error(self.request, _('This refund can not be processed at the moment.'))
|
||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect(self.request.GET.get("next"))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
@@ -745,7 +742,7 @@ class OrderRefundDone(OrderView):
|
||||
messages.success(self.request, _('The refund has been marked as done.'))
|
||||
else:
|
||||
messages.error(self.request, _('This refund can not be processed at the moment.'))
|
||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect(self.request.GET.get("next"))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
@@ -2240,40 +2237,20 @@ class OrderGo(EventPermissionRequiredMixin, View):
|
||||
class ExportMixin:
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
exporters = []
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
return sorted(
|
||||
[response(self.request.event, self.request.organizer) for r, response in responses if response],
|
||||
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
)
|
||||
|
||||
@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:
|
||||
id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
|
||||
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)):
|
||||
if id and ex.identifier != id:
|
||||
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
|
||||
}
|
||||
# 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),
|
||||
@@ -2281,27 +2258,12 @@ class ExportMixin:
|
||||
initial=initial
|
||||
)
|
||||
ex.form.fields = ex.export_form_fields
|
||||
return ex
|
||||
|
||||
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"))
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['exporters'] = self.exporters
|
||||
ctx['exporter'] = self.exporter
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -2309,7 +2271,7 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
|
||||
permission = 'can_view_orders'
|
||||
known_errortypes = ['ExportError']
|
||||
task = export
|
||||
template_name = 'pretixcontrol/orders/export_form.html'
|
||||
template_name = 'pretixcontrol/orders/export.html'
|
||||
|
||||
def get_success_message(self, value):
|
||||
return None
|
||||
@@ -2326,6 +2288,16 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
|
||||
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 '')
|
||||
|
||||
@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):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
@@ -2339,165 +2311,20 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}))
|
||||
|
||||
if self.scheduled:
|
||||
data = self.scheduled.export_form_data
|
||||
else:
|
||||
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
|
||||
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)
|
||||
|
||||
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
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'
|
||||
paginate_by = 25
|
||||
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
|
||||
}))
|
||||
template_name = 'pretixcontrol/orders/export.html'
|
||||
|
||||
|
||||
class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user