forked from CGM_Public/pretix_original
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
756c004d66 | ||
|
|
61243e4a5a | ||
|
|
c10a8575ad | ||
|
|
202f34ad5b | ||
|
|
6f0f4755ef | ||
|
|
910a35dedc | ||
|
|
e694bd8c21 | ||
|
|
29cf384c28 | ||
|
|
492288f437 | ||
|
|
34e4f7e0fc | ||
|
|
f6f3bbcce6 | ||
|
|
16054893ed | ||
|
|
f6038d2c39 | ||
|
|
8d13b51271 | ||
|
|
83e1f365c2 | ||
|
|
146e1aeb67 | ||
|
|
f9b2920984 | ||
|
|
2c01b214a7 | ||
|
|
fdab45e5ce | ||
|
|
9d2cf18543 | ||
|
|
2206ab1d35 | ||
|
|
ecd2c80dce | ||
|
|
3387df491a | ||
|
|
b6974e0c77 | ||
|
|
31751cbd79 | ||
|
|
993da5a392 | ||
|
|
72455209bb | ||
|
|
803aa0b70d | ||
|
|
954d86337c | ||
|
|
38a58d62f3 | ||
|
|
e67b39a57b | ||
|
|
148b67ac3f | ||
|
|
d261cb3b6b | ||
|
|
169a6c51b4 | ||
|
|
245ad644ff | ||
|
|
4fdce0d126 | ||
|
|
a542bc7a5a | ||
|
|
3164919923 | ||
|
|
8085311eb6 | ||
|
|
3887a65961 | ||
|
|
b229c6156a | ||
|
|
c45298544e | ||
|
|
7bb9d3fc3d | ||
|
|
8607df5a9c | ||
|
|
c4150473fc | ||
|
|
172b2f74e0 | ||
|
|
9586f71dc2 | ||
|
|
25692d180f | ||
|
|
ae047037dc | ||
|
|
265106034b | ||
|
|
dd0a4df914 | ||
|
|
b0ae40c264 | ||
|
|
ad95815043 | ||
|
|
f68522ec0d | ||
|
|
b831e57351 | ||
|
|
51166786ee | ||
|
|
909e7906ff | ||
|
|
e185d5f0e7 | ||
|
|
ce8edf621b | ||
|
|
e58b512876 | ||
|
|
d1754f6d1b | ||
|
|
ff2f1b7424 | ||
|
|
fb1838a2f0 | ||
|
|
d7b05063a4 | ||
|
|
f64a42d61a | ||
|
|
c1994e89a5 | ||
|
|
f37de1ad2f | ||
|
|
e1ff6f8590 | ||
|
|
a5dd22eb4d | ||
|
|
19cde63505 | ||
|
|
754d4f4f62 | ||
|
|
e433230573 | ||
|
|
f8927396d3 | ||
|
|
60be99fbb2 | ||
|
|
0c508c5ba4 | ||
|
|
ea6067ab3f | ||
|
|
9d0fa84277 | ||
|
|
a6835d3b14 | ||
|
|
9ff565f772 | ||
|
|
5d41b20bae | ||
|
|
03de0d5d2e | ||
|
|
2937acdc66 | ||
|
|
6fd09e99e2 | ||
|
|
290e14689d | ||
|
|
89c937089b | ||
|
|
0e02febe76 | ||
|
|
771f822e5f |
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/src"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/pretix/static/npm_dir"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
@@ -26,6 +26,13 @@ In addition to these standard update steps, the following list issues steps that
|
||||
to specific versions for pretix. If you're skipping versions, please read the instructions for every version in
|
||||
between as well.
|
||||
|
||||
Upgrade to 3.17.0 or newer
|
||||
""""""""""""""""""""""""""
|
||||
|
||||
pretix 3.17 introduces a dependency on ``nodejs``, so you should install it on your system::
|
||||
|
||||
# apt install nodejs npm
|
||||
|
||||
Upgrade to 4.4.0 or newer
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
|
||||
@@ -31,5 +31,6 @@ Resources and endpoints
|
||||
webhooks
|
||||
seatingplans
|
||||
exporters
|
||||
sendmail_rules
|
||||
billing_invoices
|
||||
billing_var
|
||||
|
||||
@@ -78,6 +78,12 @@ lines list of objects The actual invo
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no end date set.
|
||||
├ event_location string Location of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no location set.
|
||||
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
|
||||
name was set or if names are configured to not be added to invoices.
|
||||
├ gross_value money (string) Price including taxes
|
||||
@@ -110,6 +116,10 @@ internal_reference string Customer's refe
|
||||
|
||||
The attributes ``fee_type`` and ``fee_internal_type`` have been added.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The attribute ``lines.event_location`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -179,6 +189,7 @@ Endpoints
|
||||
"fee_internal_type": null,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"event_location": "Heidelberg",
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -267,6 +278,7 @@ Endpoints
|
||||
"fee_internal_type": null,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"event_location": "Heidelberg",
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
|
||||
281
doc/api/resources/sendmail_rules.rst
Normal file
281
doc/api/resources/sendmail_rules.rst
Normal file
@@ -0,0 +1,281 @@
|
||||
Automated email rules
|
||||
=====================
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Automated email rules that specify emails that the system will send automatically at a specific point in time, e.g.
|
||||
the day of the event.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the rule
|
||||
enabled boolean If ``false``, the rule is ignored
|
||||
subject multi-lingual string The subject of the email
|
||||
template multi-lingual string The body of the email
|
||||
all_products boolean If ``true``, the email is sent to buyers of all products
|
||||
limit_products list of integers List of product IDs, if ``all_products`` is not set
|
||||
include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
|
||||
only paid orders are considered.
|
||||
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
|
||||
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
|
||||
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
|
||||
before/after the email is sent.
|
||||
send_offset_time time If ``date_is_absolute`` is not set, this is the time of day the
|
||||
email is sent on the day specified by ``send_offset_days``.
|
||||
offset_to_event_end boolean If ``true``, ``send_offset_days`` is relative to the event end
|
||||
date. Otherwise it is relative to the event start date.
|
||||
offset_is_after boolean If ``true``, ``send_offset_days`` is the number of days **after**
|
||||
the event start or end date. Otherwise it is the number of days
|
||||
**before**.
|
||||
send_to string Can be ``"orders"`` if the email should be sent to customers
|
||||
(one email per order),
|
||||
``"attendees"`` if the email should be sent to every attendee,
|
||||
or ``"both"``.
|
||||
date. Otherwise it is relative to the event start date.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
|
||||
|
||||
Returns a list of all rules configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
: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:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
|
||||
|
||||
Returns information on one rule, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the rule to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
|
||||
|
||||
Create a new rule.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"enabled": true,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a rule for
|
||||
:param event: The ``slug`` field of the event to create a rule for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The rule 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 rules.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
|
||||
|
||||
Update a rule. 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/sendmail_rules/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"enabled": false,
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"enabled": false,
|
||||
"subject": {"en": "See you tomorrow!"},
|
||||
"template": {"en": "Don't forget your tickets, download them at {url}"},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
"date_is_absolute": false,
|
||||
"offset_to_event_end": false,
|
||||
"offset_is_after": false,
|
||||
"send_to": "orders"
|
||||
}
|
||||
|
||||
: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 rule to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The rule could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
|
||||
|
||||
Delete a rule.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/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 rule to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.
|
||||
@@ -1,6 +1,11 @@
|
||||
.. spelling:: Rebase rebasing
|
||||
|
||||
Coding style and quality
|
||||
========================
|
||||
|
||||
Code
|
||||
----
|
||||
|
||||
* Basically, we want all python code to follow the `PEP 8`_ standard. There are a few exceptions where
|
||||
we see things differently or just aren't that strict. The ``setup.cfg`` file in the project's source
|
||||
folder contains definitions that allow `flake8`_ to check for violations automatically. See :ref:`checksandtests`
|
||||
@@ -20,8 +25,62 @@ Coding style and quality
|
||||
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
|
||||
but please use ``pytest`` style for any new test files.
|
||||
|
||||
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
|
||||
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
|
||||
Commits and Pull Requests
|
||||
-------------------------
|
||||
|
||||
|
||||
|
||||
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
|
||||
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
|
||||
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
|
||||
individual commits, we use "Rebase and merge" instead. Merge commits should be avoided.
|
||||
|
||||
* The commit message should start with a single subject line and can optionally be followed by a commit message body.
|
||||
|
||||
* The subject line should be the shortest possible representation of what the commit changes. Someone who reviewed
|
||||
the commit should able to immediately remember the commit in a couple of weeks based on the subject line and tell
|
||||
it apart from other commits.
|
||||
|
||||
* If there's additional useful information that we should keep, such as reasoning behind the commit, you can
|
||||
add a longer body, separated from the first line by a blank line.
|
||||
|
||||
* The body should explain **what** you changed and more importantly **why** you changed it. There's no need to iterate
|
||||
**how** you changed something.
|
||||
|
||||
* The subject line should be capitalized ("Add new feature" instead of "add new feature") and should not end with a period
|
||||
("Add new feature" instead of "Add new feature.")
|
||||
|
||||
* The subject line should be written in imperative mood, as if you were giving a command what the computer should do if the
|
||||
commit is applied. This is how generated commit messages by git itself are already written ("Merge branch …", "Revert …")
|
||||
and makes for short and consistent messages.
|
||||
|
||||
* Good: "Fix typo in template"
|
||||
* Good: "Add Chinese translation"
|
||||
* Good: "Remove deprecated method"
|
||||
* Good: "Bump version to 4.4.0"
|
||||
* Bad: "Fixed bug with …"
|
||||
* Bad: "Fixes bug with …"
|
||||
* Bad: "Fixing bug …"
|
||||
|
||||
* If all changes in your commit are in context of a single feature or e.g. a bundled plugin, it makes sense to prefix the
|
||||
subject line with the name of that feature. Examples:
|
||||
|
||||
* "API: Add support for PATCH on customers"
|
||||
* "Docs: Add chapter on alpaca feeding"
|
||||
* "Stripe: Fix duplicate payments"
|
||||
* "Order change form: Fix incorrect validation"
|
||||
|
||||
* If your commit references a GitHub issue that is fully resolved by your commit, start your subject line with the issue
|
||||
ID in the form of "Fix #1234 -- Crash in order list". In this case, you can omit the verb "Fix" at the beginning of the
|
||||
second part of the message to avoid repetition of the word "fix". If your commit only partially resolves the issue, use
|
||||
"Refs #1234 -- Crash in order list" instead.
|
||||
|
||||
* Applies to pretix employees only: If your commit references a sentry issue, please put it in parentheses at the end
|
||||
of the subject line or inside the body ("Fix crash in order list (PRETIXEU-ABC)"). If your commit references a support
|
||||
ticket, please put it in parentheses at the end of the subject line with a "Z#" prefix ("Fix crash in order list (Z#12345)").
|
||||
|
||||
* If your PR was open for a while and might cause conflicts on merge, please prefer rebasing it (``git rebase -i master``)
|
||||
over merging ``master`` into your branch unless it is prohibitively complicated.
|
||||
|
||||
|
||||
.. _PEP 8: https://legacy.python.org/dev/peps/pep-0008/
|
||||
|
||||
@@ -26,7 +26,7 @@ Your should install the following on your system:
|
||||
* ``libssl`` (Debian package: ``libssl-dev``)
|
||||
* ``libxml2`` (Debian package ``libxml2-dev``)
|
||||
* ``libxslt`` (Debian package ``libxslt1-dev``)
|
||||
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
|
||||
* ``libenchant-2-2`` (Debian package ``libenchant-2-2``)
|
||||
* ``msgfmt`` (Debian package ``gettext``)
|
||||
* ``git``
|
||||
|
||||
@@ -51,7 +51,12 @@ the dependencies might fail::
|
||||
|
||||
Working with the code
|
||||
---------------------
|
||||
The first thing you need are all the main application's dependencies::
|
||||
If you do not have a recent installation of ``nodejs``, install it now::
|
||||
|
||||
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
|
||||
sudo apt install nodejs
|
||||
|
||||
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||
|
||||
cd src/
|
||||
pip3 install -e ".[dev]"
|
||||
|
||||
@@ -34,5 +34,7 @@ git push
|
||||
# Unlock Weblate
|
||||
for c in $COMPONENTS; do
|
||||
wlc unlock $c;
|
||||
done
|
||||
for c in $COMPONENTS; do
|
||||
wlc pull $c;
|
||||
done
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "4.4.1"
|
||||
__version__ = "4.5.0.dev0"
|
||||
|
||||
@@ -734,6 +734,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_numbers_prefix_cancellations',
|
||||
'invoice_numbers_counter_length',
|
||||
'invoice_attendee_name',
|
||||
'invoice_event_location',
|
||||
'invoice_include_expire_date',
|
||||
'invoice_address_explanation_text',
|
||||
'invoice_email_attachment',
|
||||
@@ -763,6 +764,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_price',
|
||||
'primary_color',
|
||||
|
||||
@@ -1428,7 +1428,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
|
||||
'fee_internal_type')
|
||||
'fee_internal_type', 'event_location')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -296,7 +296,14 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'organizer_logo_image_inherit',
|
||||
'organizer_logo_image'
|
||||
'organizer_logo_image',
|
||||
'privacy_url',
|
||||
'cookie_consent',
|
||||
'cookie_consent_dialog_title',
|
||||
'cookie_consent_dialog_text',
|
||||
'cookie_consent_dialog_text_secondary',
|
||||
'cookie_consent_dialog_button_yes',
|
||||
'cookie_consent_dialog_button_no',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -69,7 +69,7 @@ class ExportersMixin:
|
||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||
if cf.file:
|
||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||
return resp
|
||||
elif not settings.HAS_CELERY:
|
||||
return Response(
|
||||
|
||||
@@ -324,7 +324,6 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
_('Tax rate'),
|
||||
_('Tax name'),
|
||||
_('Event start date'),
|
||||
|
||||
_('Date'),
|
||||
_('Order code'),
|
||||
_('E-mail address'),
|
||||
@@ -348,6 +347,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
||||
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
||||
_('Payment providers'),
|
||||
_('Event end date'),
|
||||
_('Location'),
|
||||
]
|
||||
|
||||
p_providers = OrderPayment.objects.filter(
|
||||
@@ -406,7 +407,9 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((l.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
])
|
||||
]),
|
||||
date_format(l.event_date_to, "SHORT_DATE_FORMAT") if l.event_date_to else "",
|
||||
l.event_location or "",
|
||||
]
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -55,6 +55,7 @@ class UserSettingsForm(forms.ModelForm):
|
||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
'pw_equal': _("Please choose a password different to your current one.")
|
||||
}
|
||||
|
||||
old_pw = forms.CharField(max_length=255,
|
||||
@@ -158,6 +159,12 @@ class UserSettingsForm(forms.ModelForm):
|
||||
code='pw_current'
|
||||
)
|
||||
|
||||
if password1 and password1 == old_pw:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_equal'],
|
||||
code='pw_equal'
|
||||
)
|
||||
|
||||
if password1:
|
||||
self.instance.set_password(password1)
|
||||
|
||||
|
||||
@@ -395,7 +395,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
return txt
|
||||
|
||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
|
||||
tz = self.invoice.event.timezone
|
||||
show_end_date = (
|
||||
self.invoice.event.settings.show_date_to and
|
||||
self.invoice.event.date_to and
|
||||
self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
|
||||
)
|
||||
if show_end_date:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' +
|
||||
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
@@ -550,7 +556,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
for line in self.invoice.lines.all():
|
||||
if has_taxes:
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
Paragraph(
|
||||
bleach.clean(line.description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
|
||||
self.stylesheet['Normal']
|
||||
),
|
||||
"1",
|
||||
localize(line.tax_rate) + " %",
|
||||
money_filter(line.net_value, self.invoice.event.currency),
|
||||
@@ -558,7 +567,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
))
|
||||
else:
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
Paragraph(
|
||||
bleach.clean(line.description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
|
||||
self.stylesheet['Normal']
|
||||
),
|
||||
"1",
|
||||
money_filter(line.gross_value, self.invoice.event.currency),
|
||||
))
|
||||
|
||||
@@ -208,7 +208,7 @@ def _parse_csp(header):
|
||||
|
||||
|
||||
def _render_csp(h):
|
||||
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items())
|
||||
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
|
||||
|
||||
|
||||
def _merge_csp(a, b):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-11-03 09:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0200_transaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='event_location',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.9 on 2021-11-04 13:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0201_invoiceline_event_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='needs_password_change',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
17
src/pretix/base/migrations/0203_orderposition_is_bundled.py
Normal file
17
src/pretix/base/migrations/0203_orderposition_is_bundled.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.2 on 2021-11-08 07:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0202_user_needs_password_change'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='is_bundled',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.2.2 on 2021-11-08 07:51
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Count, OuterRef, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
|
||||
def fill_is_bundled(apps, schema_editor):
|
||||
# We cannot really know if a position was bundled or an add-on, but we can at least guess
|
||||
ItemBundle = apps.get_model("pretixbase", "ItemBundle")
|
||||
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
|
||||
|
||||
for ib in ItemBundle.objects.iterator():
|
||||
OrderPosition.all.alias(
|
||||
pos_earlier=Coalesce(Subquery(
|
||||
OrderPosition.all.filter(
|
||||
canceled=False,
|
||||
addon_to=OuterRef('addon_to'),
|
||||
item=ib.bundled_item,
|
||||
variation=ib.bundled_variation,
|
||||
positionid__lt=OuterRef('positionid'),
|
||||
).values('addon_to').order_by().annotate(c=Count('*')).values('c'),
|
||||
output_field=models.IntegerField()
|
||||
), 0)
|
||||
).filter(
|
||||
canceled=False,
|
||||
addon_to__item=ib.base_item,
|
||||
item=ib.bundled_item,
|
||||
variation=ib.bundled_variation,
|
||||
pos_earlier__lt=ib.count,
|
||||
).update(
|
||||
is_bundled=True
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0203_orderposition_is_bundled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
fill_is_bundled,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -113,6 +113,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
:type date_joined: datetime
|
||||
:param locale: The user's preferred locale code.
|
||||
:type locale: str
|
||||
:param needs_password_change: Whether this user's password needs to be changed.
|
||||
:type needs_password_change: bool
|
||||
:param timezone: The user's preferred timezone.
|
||||
:type timezone: str
|
||||
"""
|
||||
@@ -130,6 +132,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
verbose_name=_('Is site admin'))
|
||||
date_joined = models.DateTimeField(auto_now_add=True,
|
||||
verbose_name=_('Date joined'))
|
||||
needs_password_change = models.BooleanField(default=False,
|
||||
verbose_name=_('Force user to select a new password'))
|
||||
locale = models.CharField(max_length=50,
|
||||
choices=settings.LANGUAGES,
|
||||
default=settings.LANGUAGE_CODE,
|
||||
|
||||
@@ -565,6 +565,8 @@ class Event(EventMixin, LoggedModel):
|
||||
self.settings.ticketoutput_pdf__enabled = True
|
||||
self.settings.ticketoutput_passbook__enabled = True
|
||||
self.settings.event_list_type = 'calendar'
|
||||
self.settings.invoice_email_attachment = True
|
||||
self.settings.name_scheme = 'given_family'
|
||||
|
||||
@property
|
||||
def social_image(self):
|
||||
|
||||
@@ -264,6 +264,7 @@ class Invoice(models.Model):
|
||||
self.invoice_no = self._get_invoice_number_from_order()
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.full_invoice_no = self.prefix + self.invoice_no
|
||||
return super().save(*args, **kwargs)
|
||||
except DatabaseError:
|
||||
# Suppress duplicate key errors and try again
|
||||
@@ -328,6 +329,8 @@ class InvoiceLine(models.Model):
|
||||
:type event_date_from: datetime
|
||||
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
|
||||
:type event_date_to: datetime
|
||||
:param event_location: Event location of the (sub)event at the time the invoice was created
|
||||
:type event_location: str
|
||||
:param item: The item this line refers to
|
||||
:type item: Item
|
||||
:param variation: The variation this line refers to
|
||||
@@ -345,6 +348,7 @@ class InvoiceLine(models.Model):
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event_date_from = models.DateTimeField(null=True)
|
||||
event_date_to = models.DateTimeField(null=True)
|
||||
event_location = models.TextField(null=True, blank=True)
|
||||
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
|
||||
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
|
||||
attendee_name = models.TextField(null=True, blank=True)
|
||||
|
||||
@@ -75,7 +75,7 @@ from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Customer, User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import order_gracefully_delete
|
||||
|
||||
@@ -581,6 +581,7 @@ class Order(LockModel, LoggedModel):
|
||||
Returns whether or not this order can be canceled by the user.
|
||||
"""
|
||||
from .checkin import Checkin
|
||||
from .items import ItemAddOn
|
||||
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID) or not self.count_positions:
|
||||
return False
|
||||
@@ -606,7 +607,10 @@ class Order(LockModel, LoggedModel):
|
||||
if self.user_change_deadline and now() > self.user_change_deadline:
|
||||
return False
|
||||
|
||||
return self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])
|
||||
return (
|
||||
(self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
|
||||
(self.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
|
||||
)
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
@@ -1306,6 +1310,7 @@ class AbstractPosition(models.Model):
|
||||
seat = models.ForeignKey(
|
||||
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
||||
)
|
||||
is_bundled = models.BooleanField(default=False)
|
||||
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
|
||||
@@ -1542,7 +1547,7 @@ class OrderPayment(models.Model):
|
||||
return self.order.event.get_payment_providers(cached=True).get(self.provider)
|
||||
|
||||
@transaction.atomic()
|
||||
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
|
||||
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
|
||||
from pretix.base.signals import order_paid
|
||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
|
||||
if can_be_paid is not True:
|
||||
@@ -1618,10 +1623,6 @@ class OrderPayment(models.Model):
|
||||
:type mail_text: str
|
||||
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
|
||||
"""
|
||||
from pretix.base.services.invoices import (
|
||||
generate_invoice, invoice_qualified,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
|
||||
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
|
||||
@@ -1665,7 +1666,15 @@ class OrderPayment(models.Model):
|
||||
))
|
||||
return
|
||||
|
||||
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
|
||||
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum)
|
||||
|
||||
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
||||
ignore_date=False, lock=True, payment_refund_sum=0):
|
||||
from pretix.base.services.invoices import (
|
||||
generate_invoice, invoice_qualified,
|
||||
)
|
||||
|
||||
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TIMEOUT * 2)) or not lock:
|
||||
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
|
||||
# database transaction is more than enough.
|
||||
lockfn = NoLockManager
|
||||
@@ -1673,8 +1682,8 @@ class OrderPayment(models.Model):
|
||||
lockfn = self.order.event.lock
|
||||
|
||||
with lockfn():
|
||||
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
|
||||
ignore_date=ignore_date)
|
||||
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
|
||||
ignore_date=ignore_date)
|
||||
|
||||
invoice = None
|
||||
if invoice_qualified(self.order):
|
||||
@@ -2562,7 +2571,6 @@ class CartPosition(AbstractPosition):
|
||||
max_digits=10, decimal_places=2,
|
||||
null=True, blank=True
|
||||
)
|
||||
is_bundled = models.BooleanField(default=False)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
|
||||
@@ -97,10 +97,21 @@ class Organizer(LoggedModel):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.pk
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.get_cache().clear()
|
||||
if is_new:
|
||||
self.set_defaults()
|
||||
else:
|
||||
self.get_cache().clear()
|
||||
return obj
|
||||
|
||||
def set_defaults(self):
|
||||
"""
|
||||
This will be called after organizer creation.
|
||||
This way, we can use this to introduce new default settings to pretix that do not affect existing organizers.
|
||||
"""
|
||||
self.settings.cookie_consent = True
|
||||
|
||||
def get_cache(self):
|
||||
"""
|
||||
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
|
||||
|
||||
@@ -736,7 +736,11 @@ def process_exit_all(sender, **kwargs):
|
||||
exit_all_at__isnull=False
|
||||
).select_related('event', 'event__organizer')
|
||||
for cl in qs:
|
||||
for p in cl.positions_inside:
|
||||
positions = cl.positions_inside.filter(
|
||||
Q(last_exit__isnull=True) | Q(last_exit__lte=cl.exit_all_at),
|
||||
last_entry__lte=cl.exit_all_at,
|
||||
)
|
||||
for p in positions:
|
||||
with scope(organizer=cl.event.organizer):
|
||||
ci = Checkin.objects.create(
|
||||
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
|
||||
@@ -748,6 +752,9 @@ def process_exit_all(sender, **kwargs):
|
||||
cl.event.settings.delete(f'autocheckin_dst_hack_{cl.pk}')
|
||||
try:
|
||||
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
|
||||
except pytz.exceptions.AmbiguousTimeError:
|
||||
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone,
|
||||
is_dst=False)
|
||||
except pytz.exceptions.NonExistentTimeError:
|
||||
cl.event.settings.set(f'autocheckin_dst_hack_{cl.pk}', True)
|
||||
d += timedelta(hours=1)
|
||||
|
||||
@@ -69,6 +69,10 @@ from pretix.helpers.models import modelcopy
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _location_oneliner(loc):
|
||||
return ', '.join([l.strip() for l in loc.splitlines() if l and l.strip()])
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice.locale = invoice.event.settings.get('invoice_language', invoice.event.settings.locale)
|
||||
@@ -176,19 +180,38 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
reverse_charge = False
|
||||
|
||||
positions.sort(key=lambda p: p.sort_key)
|
||||
fees = list(invoice.order.fees.all())
|
||||
|
||||
locations = {
|
||||
str((p.subevent or invoice.event).location) if (p.subevent or invoice.event).location else None
|
||||
for p in positions
|
||||
}
|
||||
if fees and invoice.event.has_subevents:
|
||||
locations.add(None)
|
||||
|
||||
tax_texts = []
|
||||
|
||||
if invoice.event.settings.invoice_event_location and len(locations) == 1 and list(locations)[0] is not None:
|
||||
tax_texts.append(pgettext("invoice", "Event location: {location}").format(
|
||||
location=_location_oneliner(str(list(locations)[0]))
|
||||
))
|
||||
|
||||
for i, p in enumerate(positions):
|
||||
if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
|
||||
continue
|
||||
|
||||
location = str((p.subevent or invoice.event).location) if (p.subevent or invoice.event).location else None
|
||||
|
||||
desc = str(p.item.name)
|
||||
if p.variation:
|
||||
desc += " - " + str(p.variation.value)
|
||||
if p.addon_to_id:
|
||||
desc = " + " + desc
|
||||
if invoice.event.settings.invoice_attendee_name and p.attendee_name:
|
||||
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
|
||||
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(
|
||||
name=p.attendee_name
|
||||
)
|
||||
|
||||
for recv, resp in invoice_line_text.send(sender=invoice.event, position=p):
|
||||
if resp:
|
||||
desc += "<br/>" + resp
|
||||
@@ -204,6 +227,12 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
|
||||
if invoice.event.has_subevents:
|
||||
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
|
||||
|
||||
if invoice.event.settings.invoice_event_location and location and len(locations) > 1:
|
||||
desc += "<br />" + pgettext("invoice", "Event location: {location}").format(
|
||||
location=_location_oneliner(location)
|
||||
)
|
||||
|
||||
InvoiceLine.objects.create(
|
||||
position=i,
|
||||
invoice=invoice,
|
||||
@@ -216,6 +245,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
|
||||
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
||||
event_location=location if invoice.event.settings.invoice_event_location else None,
|
||||
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
|
||||
)
|
||||
|
||||
@@ -228,7 +258,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
tax_texts.append(tax_text)
|
||||
|
||||
offset = len(positions)
|
||||
for i, fee in enumerate(invoice.order.fees.all()):
|
||||
for i, fee in enumerate(fees):
|
||||
if fee.fee_type == OrderFee.FEE_TYPE_OTHER and fee.description:
|
||||
fee_title = fee.description
|
||||
else:
|
||||
@@ -242,6 +272,12 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
gross_value=fee.value,
|
||||
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
|
||||
event_location=(
|
||||
None if invoice.event.has_subevents
|
||||
else (str(invoice.event.location)
|
||||
if invoice.event.settings.invoice_event_location and invoice.event.location
|
||||
else None)
|
||||
),
|
||||
tax_value=fee.tax_value,
|
||||
tax_rate=fee.tax_rate,
|
||||
tax_name=fee.tax_rule.name if fee.tax_rule else '',
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import Counter, namedtuple
|
||||
from collections import Counter, defaultdict, namedtuple
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional
|
||||
@@ -46,7 +46,7 @@ from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.transaction import get_connection
|
||||
@@ -73,7 +73,7 @@ from pretix.base.models.orders import (
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
@@ -122,8 +122,7 @@ error_messages = {
|
||||
'from your cart.'),
|
||||
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
|
||||
'removed this item from your cart.'),
|
||||
'voucher_required': _('You need a valid voucher code to order one of the products in your cart. We removed this '
|
||||
'item from your cart.'),
|
||||
'voucher_required': _('You need a valid voucher code to order one of the products.'),
|
||||
'some_subevent_not_started': _('The presale period for one of the events in your cart has not yet started. The '
|
||||
'affected positions have been removed from your cart.'),
|
||||
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
|
||||
@@ -131,6 +130,13 @@ error_messages = {
|
||||
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
|
||||
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
|
||||
'country_blocked': _('One of the selected products is not available in the selected country.'),
|
||||
'not_for_sale': _('You selected a product which is not available for sale.'),
|
||||
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
|
||||
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
|
||||
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
|
||||
'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.'),
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1261,15 +1267,15 @@ class OrderChangeManager:
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||
SeatOperation = namedtuple('SubeventOperation', ('position', 'seat'))
|
||||
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
|
||||
PriceOperation = namedtuple('PriceOperation', ('position', 'price', 'price_diff'))
|
||||
TaxRuleOperation = namedtuple('TaxRuleOperation', ('position', 'tax_rule'))
|
||||
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position',))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership'))
|
||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value'))
|
||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee',))
|
||||
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',))
|
||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
|
||||
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
|
||||
|
||||
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
|
||||
@@ -1386,7 +1392,7 @@ class OrderChangeManager:
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._operations.append(self.PriceOperation(position, price))
|
||||
self._operations.append(self.PriceOperation(position, price, price.gross - position.price))
|
||||
|
||||
def change_tax_rule(self, position_or_fee, tax_rule: TaxRule):
|
||||
self._operations.append(self.TaxRuleOperation(position_or_fee, tax_rule))
|
||||
@@ -1426,28 +1432,28 @@ class OrderChangeManager:
|
||||
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
|
||||
override_tax_rate=new_rate)
|
||||
self._totaldiff += new_tax.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, new_tax))
|
||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||
|
||||
def cancel_fee(self, fee: OrderFee):
|
||||
self._totaldiff -= fee.value
|
||||
self._operations.append(self.CancelFeeOperation(fee))
|
||||
self._operations.append(self.CancelFeeOperation(fee, -fee.value))
|
||||
self._invoice_dirty = True
|
||||
|
||||
def add_fee(self, fee: OrderFee):
|
||||
self._totaldiff += fee.value
|
||||
self._invoice_dirty = True
|
||||
self._operations.append(self.AddFeeOperation(fee))
|
||||
self._operations.append(self.AddFeeOperation(fee, fee.value))
|
||||
|
||||
def change_fee(self, fee: OrderFee, value: Decimal):
|
||||
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
|
||||
self._totaldiff += value.gross - fee.value
|
||||
self._invoice_dirty = True
|
||||
self._operations.append(self.FeeValueOperation(fee, value))
|
||||
self._operations.append(self.FeeValueOperation(fee, value, value.gross - fee.value))
|
||||
|
||||
def cancel(self, position: OrderPosition):
|
||||
self._totaldiff -= position.price
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.CancelOperation(position))
|
||||
self._operations.append(self.CancelOperation(position, -position.price))
|
||||
if position.seat:
|
||||
self._seatdiff.subtract([position.seat])
|
||||
|
||||
@@ -1472,7 +1478,7 @@ class OrderChangeManager:
|
||||
try:
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
elif not isinstance(price, TaxedPrice):
|
||||
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||
@@ -1515,6 +1521,190 @@ class OrderChangeManager:
|
||||
|
||||
self._operations.append(self.SplitOperation(position))
|
||||
|
||||
def set_addons(self, addons):
|
||||
if self._operations:
|
||||
raise ValueError("Setting addons should be the first/only operation")
|
||||
|
||||
# Prepare various containers to hold data later
|
||||
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
|
||||
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
|
||||
selected_addons = defaultdict(Counter) # OrderPos, ItemAddOn -> final desired set of add-ons
|
||||
opcache = {} # OrderPos.pk -> OrderPos
|
||||
quota_diff = Counter() # Quota -> Number of usages
|
||||
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
|
||||
price_included = defaultdict(dict) # OrderPos -> CategoryID -> bool(price is included)
|
||||
toplevel_op = self.order.positions.filter(
|
||||
addon_to__isnull=True
|
||||
).prefetch_related(
|
||||
'addons', 'item__addons', 'item__addons__addon_category'
|
||||
).select_related('item', 'variation')
|
||||
|
||||
_items_cache = {
|
||||
i.pk: i
|
||||
for i in self.event.items.select_related('category').prefetch_related(
|
||||
'addons', 'bundles', 'addons__addon_category', 'quotas'
|
||||
).annotate(
|
||||
has_variations=Count('variations'),
|
||||
).filter(
|
||||
id__in=[a['item'] for a in addons]
|
||||
).order_by()
|
||||
}
|
||||
_variations_cache = {
|
||||
v.pk: v
|
||||
for v in ItemVariation.objects.filter(item__event=self.event).prefetch_related(
|
||||
'quotas'
|
||||
).select_related('item', 'item__event').filter(
|
||||
id__in=[a['variation'] for a in addons if a.get('variation')]
|
||||
).order_by()
|
||||
}
|
||||
|
||||
# Prefill some of the cache containers
|
||||
for op in toplevel_op:
|
||||
if op.canceled:
|
||||
continue
|
||||
available_categories[op.pk] = {iao.addon_category_id for iao in op.item.addons.all()}
|
||||
price_included[op.pk] = {iao.addon_category_id: iao.price_included for iao in op.item.addons.all()}
|
||||
opcache[op.pk] = op
|
||||
for a in op.addons.all():
|
||||
if a.canceled:
|
||||
continue
|
||||
|
||||
if not a.is_bundled:
|
||||
current_addons[op][a.item_id, a.variation_id].append(a)
|
||||
|
||||
# Create operations, perform various checks
|
||||
for a in addons:
|
||||
# Check whether the specified items are part of what we just fetched from the database
|
||||
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||
# a different event
|
||||
if a['item'] not in _items_cache or (a['variation'] and a['variation'] not in _variations_cache):
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
|
||||
# Only attach addons to things that are actually in this user's cart
|
||||
if a['addon_to'] not in opcache:
|
||||
raise OrderError(error_messages['addon_invalid_base'])
|
||||
|
||||
op = opcache[a['addon_to']]
|
||||
item = _items_cache[a['item']]
|
||||
variation = _variations_cache[a['variation']] if a['variation'] is not None else None
|
||||
|
||||
if item.category_id not in available_categories[op.pk]:
|
||||
raise OrderError(error_messages['addon_invalid_base'])
|
||||
|
||||
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
||||
quotas = list(item.quotas.filter(subevent=op.subevent)
|
||||
if variation is None else variation.quotas.filter(subevent=op.subevent))
|
||||
if not quotas:
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if (a['item'], a['variation']) in input_addons[op.id]:
|
||||
raise OrderError(error_messages['addon_duplicate_item'])
|
||||
|
||||
if item.require_voucher or op.item.hide_without_voucher or (op.variation and op.variation.hide_without_voucher):
|
||||
raise OrderError(error_messages['voucher_required'])
|
||||
|
||||
if not item.is_available() or (variation and not variation.is_available()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if self.order.sales_channel not in item.sales_channels or (
|
||||
variation and self.order.sales_channel not in variation.sales_channels):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if op.subevent and item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
|
||||
if op.subevent and variation and variation.pk in op.subevent.var_overrides and \
|
||||
not op.subevent.var_overrides[variation.pk].is_available():
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
|
||||
if item.has_variations and not variation:
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
|
||||
if variation and variation.item_id != item.pk:
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
|
||||
if op.subevent and op.subevent.presale_start and now() < op.subevent.presale_start:
|
||||
raise OrderError(error_messages['not_started'])
|
||||
|
||||
if (op.subevent and op.subevent.presale_has_ended) or self.event.presale_has_ended:
|
||||
raise OrderError(error_messages['ended'])
|
||||
|
||||
if item.require_bundling:
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
|
||||
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
|
||||
|
||||
if price_included[op.pk].get(item.category_id):
|
||||
price = TAXED_ZERO
|
||||
else:
|
||||
price = get_price(
|
||||
item, variation, voucher=None, custom_price=a.get('price'), subevent=op.subevent,
|
||||
custom_price_is_net=self.event.settings.display_net_prices,
|
||||
invoice_address=self._invoice_address,
|
||||
)
|
||||
|
||||
if a.get('count', 1) > len(current_addons[op][a['item'], a['variation']]):
|
||||
# This add-on is new, add it to the cart
|
||||
for quota in quotas:
|
||||
quota_diff[quota] += a.get('count', 1) - len(current_addons[op][a['item'], a['variation']])
|
||||
|
||||
for i in range(a.get('count', 1) - len(current_addons[op][a['item'], a['variation']])):
|
||||
self.add_position(
|
||||
item=item, variation=variation, price=price,
|
||||
addon_to=op, subevent=op.subevent, seat=None,
|
||||
)
|
||||
|
||||
# Check constraints on the add-on combinations
|
||||
for op in toplevel_op:
|
||||
item = op.item
|
||||
for iao in item.addons.all():
|
||||
selected = selected_addons[op.id, iao.addon_category_id]
|
||||
n_per_i = Counter()
|
||||
for (i, v), c in selected.items():
|
||||
n_per_i[i] += c
|
||||
if sum(selected.values()) > iao.max_count:
|
||||
# TODO: Proper i18n
|
||||
# TODO: Proper pluralization
|
||||
raise OrderError(
|
||||
error_messages['addon_max_count'],
|
||||
{
|
||||
'base': str(item.name),
|
||||
'max': iao.max_count,
|
||||
'cat': str(iao.addon_category.name),
|
||||
}
|
||||
)
|
||||
elif sum(selected.values()) < iao.min_count:
|
||||
# TODO: Proper i18n
|
||||
# TODO: Proper pluralization
|
||||
raise OrderError(
|
||||
error_messages['addon_min_count'],
|
||||
{
|
||||
'base': str(item.name),
|
||||
'min': iao.min_count,
|
||||
'cat': str(iao.addon_category.name),
|
||||
}
|
||||
)
|
||||
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
|
||||
raise OrderError(
|
||||
error_messages['addon_no_multi'],
|
||||
{
|
||||
'base': str(item.name),
|
||||
'cat': str(iao.addon_category.name),
|
||||
}
|
||||
)
|
||||
|
||||
# Detect removed add-ons and create RemoveOperations
|
||||
for cp, al in list(current_addons.items()):
|
||||
for k, v in al.items():
|
||||
input_num = input_addons[cp.id].get(k, 0)
|
||||
current_num = len(current_addons[cp].get(k, []))
|
||||
if input_num < current_num:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
self.cancel(a)
|
||||
|
||||
def _check_seats(self):
|
||||
for seat, diff in self._seatdiff.items():
|
||||
if diff <= 0:
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from zeep import Client, Transport
|
||||
from zeep.cache import SqliteCache
|
||||
@@ -83,14 +85,12 @@ def _validate_vat_id_CH(vat_id, country_code):
|
||||
|
||||
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
|
||||
try:
|
||||
transport = Transport(cache=SqliteCache())
|
||||
transport = Transport(cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")))
|
||||
client = Client(
|
||||
'https://www.uid-wse-a.admin.ch/V5.0/PublicServices.svc?wsdl',
|
||||
'https://www.uid-wse.admin.ch/V5.0/PublicServices.svc?wsdl',
|
||||
transport=transport
|
||||
)
|
||||
if not client.service.ValidateUID(uid=vat_id):
|
||||
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
|
||||
return vat_id
|
||||
result = client.service.ValidateUID(uid=vat_id)
|
||||
except Fault as e:
|
||||
if e.message == 'Data_validation_failed':
|
||||
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
|
||||
@@ -118,6 +118,10 @@ def _validate_vat_id_CH(vat_id, country_code):
|
||||
'need to charge VAT on your invoice. You can get the tax amount '
|
||||
'back via the VAT reimbursement process.'
|
||||
))
|
||||
else:
|
||||
if not result:
|
||||
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
|
||||
return vat_id
|
||||
|
||||
|
||||
def validate_vat_id(vat_id, country_code):
|
||||
|
||||
@@ -310,6 +310,17 @@ DEFAULTS = {
|
||||
label=_("Show attendee names on invoices"),
|
||||
)
|
||||
},
|
||||
'invoice_event_location': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Show event location on invoices"),
|
||||
help_text=_("The event location will be shown below the list of products if it is the same for all "
|
||||
"lines. It will be shown on every line if there are different locations.")
|
||||
)
|
||||
},
|
||||
'invoice_eu_currencies': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
@@ -415,7 +426,7 @@ DEFAULTS = {
|
||||
)
|
||||
},
|
||||
'invoice_include_expire_date': {
|
||||
'default': 'False',
|
||||
'default': 'False', # default for new events is True
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
@@ -471,7 +482,7 @@ DEFAULTS = {
|
||||
)
|
||||
},
|
||||
'invoice_renderer': {
|
||||
'default': 'classic',
|
||||
'default': 'classic', # default for new events is 'modern1'
|
||||
'type': str,
|
||||
},
|
||||
'ticket_secret_generator': {
|
||||
@@ -897,7 +908,7 @@ DEFAULTS = {
|
||||
'type': str
|
||||
},
|
||||
'invoice_email_attachment': {
|
||||
'default': 'False',
|
||||
'default': 'False', # default for new events is True
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
@@ -1230,7 +1241,7 @@ DEFAULTS = {
|
||||
)
|
||||
},
|
||||
'event_list_type': {
|
||||
'default': 'list',
|
||||
'default': 'list', # default for new events is 'calendar'
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
@@ -1290,6 +1301,15 @@ DEFAULTS = {
|
||||
label=_("Customers can change the variation of the products they purchased"),
|
||||
)
|
||||
},
|
||||
'change_allow_user_addons': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Customers can change their selected add-on products"),
|
||||
)
|
||||
},
|
||||
'change_allow_user_price': {
|
||||
'default': 'gte',
|
||||
'type': str,
|
||||
@@ -1492,6 +1512,17 @@ DEFAULTS = {
|
||||
),
|
||||
'serializer_class': serializers.URLField,
|
||||
},
|
||||
'privacy_url': {
|
||||
'default': None,
|
||||
'type': str,
|
||||
'form_class': forms.URLField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Privacy Policy URL"),
|
||||
help_text=_("This should point e.g. to a part of your website that explains how you use data gathered in "
|
||||
"your ticket shop."),
|
||||
),
|
||||
'serializer_class': serializers.URLField,
|
||||
},
|
||||
'confirm_texts': {
|
||||
'default': LazyI18nStringList(),
|
||||
'type': LazyI18nStringList,
|
||||
@@ -1974,7 +2005,7 @@ Your {organizer} team"""))
|
||||
),
|
||||
},
|
||||
'theme_color_success': {
|
||||
'default': '#50A167',
|
||||
'default': '#50a167',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
@@ -1996,7 +2027,7 @@ Your {organizer} team"""))
|
||||
),
|
||||
},
|
||||
'theme_color_danger': {
|
||||
'default': '#C44F4F',
|
||||
'default': '#c44f4f',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
@@ -2018,7 +2049,7 @@ Your {organizer} team"""))
|
||||
),
|
||||
},
|
||||
'theme_color_background': {
|
||||
'default': '#FFFFFF',
|
||||
'default': '#f5f5f5',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
@@ -2444,7 +2475,7 @@ Your {organizer} team"""))
|
||||
)
|
||||
},
|
||||
'name_scheme': {
|
||||
'default': 'full',
|
||||
'default': 'full', # default for new events is 'given_family'
|
||||
'type': str
|
||||
},
|
||||
'giftcard_length': {
|
||||
@@ -2469,6 +2500,77 @@ Your {organizer} team"""))
|
||||
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
|
||||
)
|
||||
},
|
||||
'cookie_consent': {
|
||||
'default': 'False',
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Enable cookie consent management features"),
|
||||
),
|
||||
'type': bool,
|
||||
},
|
||||
'cookie_consent_dialog_text': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop(
|
||||
'By clicking "Accept all cookies", you agree to the storing of cookies and use of similar technologies on '
|
||||
'your device.'
|
||||
)),
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Dialog text"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
'cookie_consent_dialog_text_secondary': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop(
|
||||
'We use cookies and similar technologies to gather data that allows us to improve this website and our '
|
||||
'offerings. If you do not agree, we will only use cookies if they are essential to providing the services '
|
||||
'this website offers.'
|
||||
)),
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Secondary dialog text"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
'cookie_consent_dialog_title': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop('Privacy settings')),
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Dialog title'),
|
||||
widget=I18nTextInput,
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
'cookie_consent_dialog_button_yes': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop('Accept all cookies')),
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_('"Accept" button description'),
|
||||
widget=I18nTextInput,
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
'cookie_consent_dialog_button_no': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop('Required cookies only')),
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_('"Reject" button description'),
|
||||
widget=I18nTextInput,
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
'seating_choice': {
|
||||
'default': 'True',
|
||||
'form_class': forms.BooleanField,
|
||||
|
||||
@@ -14,16 +14,17 @@
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml><![endif]-->
|
||||
<style type="text/css">
|
||||
body {
|
||||
body, .container {
|
||||
background-color: #eee;
|
||||
background-position: top;
|
||||
background-repeat: repeat-x;
|
||||
font-family: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding-top: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
table.layout > tr > td,
|
||||
@@ -36,7 +37,8 @@
|
||||
table.layout > tr > td.logo,
|
||||
table.layout > tbody > tr > td.logo,
|
||||
table.layout > thead > tr > td.logo {
|
||||
padding: 20px 0 0 0;
|
||||
padding: {% if event.settings.logo_image_large %}0 0 0 0{% else %}20px 0 0 0{% endif %};
|
||||
mso-line-height-rule: at-least;
|
||||
}
|
||||
|
||||
table.layout > tr > td.header,
|
||||
@@ -112,10 +114,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: {{ color }};
|
||||
color: #FFF;
|
||||
@@ -149,7 +147,7 @@
|
||||
table.layout > tr > td.containertd,
|
||||
table.layout > tbody > tr > td.containertd,
|
||||
table.layout > thead > tr > td.containertd {
|
||||
padding: 15px 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
a.button {
|
||||
@@ -167,7 +165,8 @@
|
||||
}
|
||||
|
||||
.order-button {
|
||||
padding-top: 5px
|
||||
padding-top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.order-button a.button {
|
||||
font-size: 12px;
|
||||
@@ -214,14 +213,14 @@
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body align="center">
|
||||
<table width="100%"><tr><td align="center" class="container">
|
||||
<!--[if gte mso 9]>
|
||||
<table width="100%"><tr><td align="center">
|
||||
<table width="600"><tr><td align="center"
|
||||
<table width="600"><tr><td align="center">
|
||||
<![endif]-->
|
||||
<table class="layout" style="max-width:600px" border="0" cellspacing="0">
|
||||
{% if event.settings.logo_image %}
|
||||
<tr>
|
||||
<td style="line-height: 0; {% if event.settings.logo_image_large %}padding: 0;{% endif %}" align="center" class="logo">
|
||||
<td align="center" class="logo">
|
||||
{% if event.settings.logo_image_large %}
|
||||
<img src="{% if event.settings.logo_image|thumb:'600_x5000'|first == '/' %}{{ site_url }}{% endif %}{{ event.settings.logo_image|thumb:'600_x5000' }}" alt="{{ event.name }}" style="width:100%" />
|
||||
{% else %}
|
||||
@@ -232,9 +231,6 @@
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="header" align="center">
|
||||
<!--[if gte mso 9]>
|
||||
<table cellpadding="20"><tr><td align="center">
|
||||
<![endif]-->
|
||||
{% if event %}
|
||||
<h2><a href="{% abseventurl event "presale:event.index" %}" target="_blank">{{ event.name }}</a>
|
||||
</h2>
|
||||
@@ -247,51 +243,30 @@
|
||||
{% block header %}
|
||||
<h1>{{ subject }}</h1>
|
||||
{% endblock %}
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="containertd">
|
||||
<!--[if gte mso 9]>
|
||||
<table cellpadding="20"><tr><td>
|
||||
<![endif]-->
|
||||
<div class="content">
|
||||
{{ body|safe }}
|
||||
</div>
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
{% if order %}
|
||||
<tr>
|
||||
<td class="order containertd">
|
||||
<!--[if gte mso 9]>
|
||||
<table cellpadding="20"><tr><td>
|
||||
<![endif]-->
|
||||
<div class="content">
|
||||
{% include "pretixbase/email/order_details.html" %}
|
||||
</div>
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if signature %}
|
||||
<tr>
|
||||
<td class="order containertd">
|
||||
<!--[if gte mso 9]>
|
||||
<table cellpadding="20"><tr><td>
|
||||
<![endif]-->
|
||||
<div class="content">
|
||||
{{ signature | safe }}
|
||||
</div>
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
@@ -303,7 +278,7 @@
|
||||
<br/>
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Redirect" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-link fa-fw big-icon"></i>
|
||||
<div class="error-details">
|
||||
<h1>{% trans "Redirect" %}</h1>
|
||||
<h3>
|
||||
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
|
||||
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
Please only proceed if you trust this website to be safe.
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<p>
|
||||
<a href="{{ url }}" class="btn btn-primary btn-lg">
|
||||
{% blocktrans trimmed with host=hostname %}
|
||||
Proceed to {{ host }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
29
src/pretix/base/templatetags/classname.py
Normal file
29
src/pretix/base/templatetags/classname.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#
|
||||
# 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 template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def classname(obj):
|
||||
return obj.__class__.__name__
|
||||
@@ -47,7 +47,7 @@ class DownloadView(TemplateView):
|
||||
return HttpResponse('1' if self.object.file else '0')
|
||||
elif self.object.file:
|
||||
resp = ChunkBasedFileResponse(self.object.file.file, content_type=self.object.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename).encode('ascii', 'ignore')
|
||||
return resp
|
||||
else:
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@@ -24,21 +24,6 @@ import urllib.parse
|
||||
from django.core import signing
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def _is_samesite_referer(request):
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
if referer is None:
|
||||
return False
|
||||
|
||||
referer = urllib.parse.urlparse(referer)
|
||||
|
||||
# Make sure we have a valid URL for Referer.
|
||||
if '' in (referer.scheme, referer.netloc):
|
||||
return False
|
||||
|
||||
return (referer.scheme, referer.netloc) == (request.scheme, request.get_host())
|
||||
|
||||
|
||||
def redir_view(request):
|
||||
@@ -47,14 +32,6 @@ def redir_view(request):
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
if not _is_samesite_referer(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
return render(request, 'pretixbase/redirect.html', {
|
||||
'hostname': u.hostname,
|
||||
'url': url,
|
||||
})
|
||||
|
||||
r = HttpResponseRedirect(url)
|
||||
r['X-Robots-Tag'] = 'noindex'
|
||||
return r
|
||||
|
||||
@@ -639,6 +639,7 @@ class CancelSettingsForm(SettingsForm):
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_price',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_addons',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -751,6 +752,7 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
'invoice_reissue_after_modify',
|
||||
'invoice_generate',
|
||||
'invoice_attendee_name',
|
||||
'invoice_event_location',
|
||||
'invoice_include_expire_date',
|
||||
'invoice_numbers_consecutive',
|
||||
'invoice_numbers_prefix',
|
||||
|
||||
@@ -307,8 +307,14 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'theme_color_danger',
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font'
|
||||
|
||||
'primary_font',
|
||||
'privacy_url',
|
||||
'cookie_consent',
|
||||
'cookie_consent_dialog_title',
|
||||
'cookie_consent_dialog_text',
|
||||
'cookie_consent_dialog_text_secondary',
|
||||
'cookie_consent_dialog_button_yes',
|
||||
'cookie_consent_dialog_button_no',
|
||||
]
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
|
||||
@@ -70,6 +70,7 @@ class UserEditForm(forms.ModelForm):
|
||||
'require_2fa',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'needs_password_change',
|
||||
'last_login'
|
||||
]
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ class PermissionMiddleware:
|
||||
"user.settings.notifications.off",
|
||||
)
|
||||
|
||||
EXCEPTIONS_FORCED_PW_CHANGE = (
|
||||
"user.settings",
|
||||
"auth.logout"
|
||||
)
|
||||
|
||||
EXCEPTIONS_2FA = (
|
||||
"user.settings.2fa",
|
||||
"user.settings.2fa.add",
|
||||
@@ -130,6 +135,9 @@ class PermissionMiddleware:
|
||||
if url_name not in ('user.reauth', 'auth.logout'):
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
|
||||
return redirect(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
|
||||
and url_name not in self.EXCEPTIONS_2FA:
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
@@ -429,6 +429,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.needs_password_change %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
For security reasons, please change your password before you continue. Afterwards you
|
||||
will be redirected to your original destination.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<footer>
|
||||
|
||||
@@ -74,17 +74,17 @@
|
||||
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if c.type == "exit" %}
|
||||
{% if c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip"
|
||||
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
{% elif c.forced and c.successful %}
|
||||
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
|
||||
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
|
||||
{% elif c.forced and not c.successful %}
|
||||
<br>
|
||||
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
|
||||
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -42,13 +42,37 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Order changes" %}</legend>
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Allowing users to change their order is a feature under development. Therefore, currently only specific changes (such as changing the variation of a product) are possible. More options might be added later.
|
||||
{% endblocktrans %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Allowing customers to change their own orders is a complex process due to the many different options pretix provides. Therefore, this feature currently has the following
|
||||
limitations:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<ul>
|
||||
<li>{% trans "It is possible to switch to a different variation of the same product, but not to an entirely different product (except for add-on products)." %}</li>
|
||||
<li>{% trans "Changing the seat or the event date in an event series will become available in the future, but is not possible now." %}</li>
|
||||
<li>{% trans "If a change leads to a price change, there will not be a change to fees such as payment, service, or shipping fees, even though an additional payment might be required." %}</li>
|
||||
<li>{% trans "If an add-on product is newly added, the system currently does not validate if there are required questions or fields that need to be filled out." %}</li>
|
||||
<li>{% trans "Customers currently cannot switch to a product variation or add an add-on product that requires them to use a voucher or membership." %}</li>
|
||||
<li>{% trans "Additional constraints and validation steps added by plugins are not enforced." %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% bootstrap_field form.change_allow_user_variation layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_price layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_addons layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_until layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_price layout="control" %}
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If the change leads to a price reduction and automatic refunds are enabled for self-service cancellations,
|
||||
the system will try to refund the money automatically.
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
Refunds can be issued as a gift card if the respective option is set, but there is no customer choice between
|
||||
gift card and direct refund.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<legend>{% trans "Invoice customization" %}</legend>
|
||||
{% bootstrap_field form.invoice_renderer layout="control" %}
|
||||
{% bootstrap_field form.invoice_attendee_name layout="control" %}
|
||||
{% bootstrap_field form.invoice_event_location layout="control" %}
|
||||
{% bootstrap_field form.invoice_include_expire_date layout="control" %}
|
||||
{% bootstrap_field form.invoice_introductory_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
{% bootstrap_field sform.logo_show_title layout="control" %}
|
||||
{% bootstrap_field sform.og_image layout="control" %}
|
||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_round_borders" %}
|
||||
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_color_background" "theme_round_borders" %}
|
||||
{% bootstrap_field sform.primary_color layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_success layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
This is the address users can buy your tickets at. Should be short, only contain lowercase
|
||||
letters and numbers, and must be unique among your events. We recommend some kind of
|
||||
letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of
|
||||
abbreviation or a date with less than 10 characters that can be easily remembered, but you
|
||||
can also choose to use a random value.
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
<div class="quotabox availability" data-toggle="tooltip_html" data-placement="top"
|
||||
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
|
||||
title="{% trans "Quota:" %} {{ q.name }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
|
||||
{% if q.size|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-100">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
<a class="quotabox" data-toggle="tooltip_html" data-placement="top"
|
||||
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
|
||||
title="{% trans "Quota:" %} {{ q.name }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
|
||||
href="{% url "control:event.items.quotas.show" event=q.event.slug organizer=q.event.organizer.slug quota=q.pk %}">
|
||||
{% if q.size|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="progress">
|
||||
|
||||
@@ -360,19 +360,19 @@
|
||||
{% if line.checkins.all %}
|
||||
{% for c in line.all_checkins.all %}
|
||||
{% if not c.successful %}
|
||||
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% elif c.type == "exit" %}
|
||||
{% if c.auto_checked_in %}
|
||||
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% endif %}
|
||||
{% elif c.forced %}
|
||||
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -82,6 +82,49 @@
|
||||
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
|
||||
{% bootstrap_field sform.giftcard_length layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Privacy" %}</legend>
|
||||
{% bootstrap_field sform.privacy_url layout="control" %}
|
||||
<div class="alert alert-legal">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Some jurisdictions, including the European Union, require user consent before you
|
||||
are allowed to use cookies or similar technology for analytics, tracking, payment,
|
||||
or similar purposes.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
pretix itself only ever sets cookies that are required to provide the service
|
||||
requested by the user or to maintain an appropriate level of security. Therefore,
|
||||
cookies set by pretix itself do not require consent in all jurisdictions that we
|
||||
are aware of.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Therefore, the settings on this page will <strong>only</strong> have an affect
|
||||
if you use <strong>plugins</strong> that require additional cookies
|
||||
<strong>and</strong> participate in our cookie consent mechanism.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{% blocktrans trimmed %}
|
||||
Ultimately, it is your responsibility to make sure you comply with all relevant
|
||||
laws. We try to help by providing these settings, but we cannot assume liability
|
||||
since we do not know the exact configuration of your pretix usage, the legal details
|
||||
in your specific jurisdiction, or the agreements you have with third parties such as
|
||||
payment or tracking providers.
|
||||
{% endblocktrans %}</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% bootstrap_field sform.cookie_consent layout="control" %}
|
||||
{% bootstrap_field sform.cookie_consent_dialog_title layout="control" %}
|
||||
{% bootstrap_field sform.cookie_consent_dialog_text layout="control" %}
|
||||
{% bootstrap_field sform.cookie_consent_dialog_text_secondary layout="control" %}
|
||||
{% bootstrap_field sform.cookie_consent_dialog_button_yes layout="control" %}
|
||||
{% bootstrap_field sform.cookie_consent_dialog_button_no layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Invoices" %}</legend>
|
||||
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
{% bootstrap_field form.email layout='control' %}
|
||||
{% bootstrap_field form.new_pw layout='control' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
||||
{% bootstrap_field form.needs_password_change layout='control' %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
{% endif %}
|
||||
{% bootstrap_field form.last_login layout='control' %}
|
||||
{% bootstrap_field form.require_2fa layout='control' %}
|
||||
{% bootstrap_field form.needs_password_change layout='control' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Team memberships" %}</legend>
|
||||
|
||||
@@ -401,10 +401,10 @@ class QuestionList(ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['questions'] = list(ctx['questions'])
|
||||
questions = []
|
||||
|
||||
if self.request.event.settings.attendee_names_asked:
|
||||
ctx['questions'].append(
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='attendee_name_parts',
|
||||
question=_('Attendee name'),
|
||||
@@ -416,7 +416,7 @@ class QuestionList(ListView):
|
||||
)
|
||||
|
||||
if self.request.event.settings.attendee_emails_asked:
|
||||
ctx['questions'].append(
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='attendee_email',
|
||||
question=_('Attendee email'),
|
||||
@@ -427,8 +427,8 @@ class QuestionList(ListView):
|
||||
)
|
||||
)
|
||||
|
||||
if self.request.event.settings.attendee_emails_asked:
|
||||
ctx['questions'].append(
|
||||
if self.request.event.settings.attendee_company_asked:
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='company',
|
||||
question=_('Company'),
|
||||
@@ -440,7 +440,7 @@ class QuestionList(ListView):
|
||||
)
|
||||
|
||||
if self.request.event.settings.attendee_addresses_asked:
|
||||
ctx['questions'].append(
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='street',
|
||||
question=_('Street'),
|
||||
@@ -450,7 +450,7 @@ class QuestionList(ListView):
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
ctx['questions'].append(
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='zipcode',
|
||||
question=_('ZIP code'),
|
||||
@@ -460,7 +460,7 @@ class QuestionList(ListView):
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
ctx['questions'].append(
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='city',
|
||||
question=_('City'),
|
||||
@@ -470,7 +470,7 @@ class QuestionList(ListView):
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
ctx['questions'].append(
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='country',
|
||||
question=_('Country'),
|
||||
@@ -481,7 +481,9 @@ class QuestionList(ListView):
|
||||
)
|
||||
)
|
||||
|
||||
ctx['questions'].sort(key=lambda q: q.position)
|
||||
questions += list(ctx['questions'])
|
||||
questions.sort(key=lambda q: q.position)
|
||||
ctx['questions'] = questions
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -1177,6 +1177,19 @@ class OrderTransition(OrderView):
|
||||
to = self.request.POST.get('status', '')
|
||||
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p' and self.mark_paid_form.is_valid():
|
||||
ps = self.mark_paid_form.cleaned_data['amount']
|
||||
|
||||
if ps == Decimal('0.00') and self.order.pending_sum <= Decimal('0.00'):
|
||||
p = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED).last()
|
||||
if p:
|
||||
p._mark_order_paid(
|
||||
user=self.request.user,
|
||||
send_mail=self.mark_paid_form.cleaned_data['send_email'],
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False),
|
||||
payment_refund_sum=self.order.payment_refund_sum,
|
||||
)
|
||||
messages.success(self.request, _('The order has been marked as paid.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
p = self.order.payments.get(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
|
||||
|
||||
@@ -226,6 +226,7 @@ class UserSettings(UpdateView):
|
||||
msgs = []
|
||||
|
||||
if 'new_pw' in form.changed_data:
|
||||
self.request.user.needs_password_change = False
|
||||
msgs.append(_('Your password has been changed.'))
|
||||
|
||||
if 'email' in form.changed_data:
|
||||
@@ -243,6 +244,8 @@ class UserSettings(UpdateView):
|
||||
return sup
|
||||
|
||||
def get_success_url(self):
|
||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
||||
return self.request.GET.get("next")
|
||||
return reverse('control:user.settings')
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2021-09-13 09:48+0000\n"
|
||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||
"PO-Revision-Date: 2021-11-19 13:52+0000\n"
|
||||
"Last-Translator: Rasmus Kock Grusgaard <rasmus@grusgaard.com>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
|
||||
">\n"
|
||||
"Language: da\n"
|
||||
@@ -3254,42 +3254,34 @@ msgstr ""
|
||||
#: pretix/base/models/items.py:507 pretix/base/models/items.py:828
|
||||
#: pretix/control/forms/event.py:853 pretix/control/forms/item.py:512
|
||||
#: pretix/control/forms/item.py:688
|
||||
#, fuzzy
|
||||
#| msgid "Save changes"
|
||||
msgid "Sales channels"
|
||||
msgstr "Gem ændringer"
|
||||
msgstr "Salgskanaler"
|
||||
|
||||
#: pretix/base/models/items.py:512
|
||||
#, fuzzy
|
||||
#| msgid "The product the user waits for."
|
||||
msgid "This product is a gift card"
|
||||
msgstr "Produktet som brugere venter på."
|
||||
msgstr "Dette produkt er et gavekort"
|
||||
|
||||
#: pretix/base/models/items.py:513
|
||||
msgid ""
|
||||
"When a customer buys this product, they will get a gift card with a value "
|
||||
"corresponding to the product price."
|
||||
msgstr ""
|
||||
"Når en kunde køber dette produkt modtager de et gavekort med en værdi, der "
|
||||
"svarer til produktets pris."
|
||||
|
||||
#: pretix/base/models/items.py:518 pretix/base/models/items.py:803
|
||||
#: pretix/control/templates/pretixcontrol/item/include_variations.html:36
|
||||
#: pretix/control/templates/pretixcontrol/item/include_variations.html:116
|
||||
#, fuzzy
|
||||
#| msgid "Team memberships"
|
||||
msgid "Require a valid membership"
|
||||
msgstr "Team medlemskaber"
|
||||
msgstr "Kræver et gyldigt medlemskab"
|
||||
|
||||
#: pretix/base/models/items.py:523
|
||||
#, fuzzy
|
||||
#| msgid "Team memberships"
|
||||
msgid "Allowed membership types"
|
||||
msgstr "Team medlemskaber"
|
||||
msgstr "Tilladte medlemskabstyper"
|
||||
|
||||
#: pretix/base/models/items.py:527 pretix/base/models/items.py:812
|
||||
#, fuzzy
|
||||
#| msgid "Team memberships"
|
||||
msgid "Hide without a valid membership"
|
||||
msgstr "Team medlemskaber"
|
||||
msgstr "Skjul uden gyldigt medlemskab"
|
||||
|
||||
#: pretix/base/models/items.py:528 pretix/base/models/items.py:813
|
||||
msgid ""
|
||||
@@ -8923,10 +8915,8 @@ msgstr "Navn"
|
||||
#: pretix/base/settings.py:2649 pretix/base/settings.py:2663
|
||||
#: pretix/base/settings.py:2714 pretix/base/settings.py:2732
|
||||
#: pretix/base/settings.py:2751
|
||||
#, fuzzy
|
||||
#| msgid "Full name"
|
||||
msgid "Family name"
|
||||
msgstr "Fuldt navn"
|
||||
msgstr "Efternavn"
|
||||
|
||||
#: pretix/base/settings.py:2561 pretix/base/settings.py:2577
|
||||
#: pretix/base/settings.py:2593 pretix/base/settings.py:2608
|
||||
@@ -10323,35 +10313,35 @@ msgstr "Dato og tidspunkt"
|
||||
#: pretix/control/forms/filter.py:857 pretix/control/forms/subevents.py:526
|
||||
#: pretix/control/forms/subevents.py:565
|
||||
msgid "Weekday"
|
||||
msgstr ""
|
||||
msgstr "Ugedag"
|
||||
|
||||
#: pretix/control/forms/filter.py:859
|
||||
msgid "Monday"
|
||||
msgstr ""
|
||||
msgstr "Mandag"
|
||||
|
||||
#: pretix/control/forms/filter.py:860
|
||||
msgid "Tuesday"
|
||||
msgstr ""
|
||||
msgstr "Tirsdag"
|
||||
|
||||
#: pretix/control/forms/filter.py:861
|
||||
msgid "Wednesday"
|
||||
msgstr ""
|
||||
msgstr "Onsdag"
|
||||
|
||||
#: pretix/control/forms/filter.py:862
|
||||
msgid "Thursday"
|
||||
msgstr ""
|
||||
msgstr "Torsdag"
|
||||
|
||||
#: pretix/control/forms/filter.py:863
|
||||
msgid "Friday"
|
||||
msgstr ""
|
||||
msgstr "Fredag"
|
||||
|
||||
#: pretix/control/forms/filter.py:864
|
||||
msgid "Saturday"
|
||||
msgstr ""
|
||||
msgstr "Lørdag"
|
||||
|
||||
#: pretix/control/forms/filter.py:865
|
||||
msgid "Sunday"
|
||||
msgstr ""
|
||||
msgstr "Søndag"
|
||||
|
||||
#: pretix/control/forms/filter.py:1015 pretix/control/forms/filter.py:1017
|
||||
#: pretix/control/forms/filter.py:1065 pretix/control/forms/filter.py:1067
|
||||
@@ -11570,7 +11560,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/subevents.py:454
|
||||
msgid "day(s)"
|
||||
msgstr ""
|
||||
msgstr "dag(e)"
|
||||
|
||||
#: pretix/control/forms/subevents.py:459
|
||||
msgid "Interval"
|
||||
@@ -11610,7 +11600,7 @@ msgstr "Dag"
|
||||
|
||||
#: pretix/control/forms/subevents.py:527 pretix/control/forms/subevents.py:566
|
||||
msgid "Weekend day"
|
||||
msgstr ""
|
||||
msgstr "Weekend dag"
|
||||
|
||||
#: pretix/control/forms/users.py:121 pretix/control/views/user.py:212
|
||||
msgid "Your changes could not be saved. See below for details."
|
||||
@@ -15861,11 +15851,8 @@ msgid "Additional settings"
|
||||
msgstr "Yderligere indstillinger"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:135
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "All dates"
|
||||
msgid "days"
|
||||
msgstr "Alle datoer"
|
||||
msgstr "dage"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:136
|
||||
msgid "months"
|
||||
@@ -23145,10 +23132,8 @@ msgid "Send date"
|
||||
msgstr "Sluttidspunkt"
|
||||
|
||||
#: pretix/plugins/sendmail/models.py:196
|
||||
#, fuzzy
|
||||
#| msgid "Number of days"
|
||||
msgid "Time of day"
|
||||
msgstr "Antal dage"
|
||||
msgstr "Tid på dagen"
|
||||
|
||||
#: pretix/plugins/sendmail/models.py:207
|
||||
msgid "Only enabled rules are actually sent"
|
||||
@@ -25262,12 +25247,10 @@ msgstr ""
|
||||
"Bookingerne i din indkøbskurv er reserveret til dig i %(minutes)s minutter."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:392
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are no longer reserved for you."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr "Varerne i din kurv er ikke længere reserverede for dig."
|
||||
msgstr "Varerne i din indkøbskurv er ikke længere reserverede til dig."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:396
|
||||
msgid "Overview of your ordered products."
|
||||
@@ -25489,7 +25472,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar_week.html:32
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:39
|
||||
msgid "Week"
|
||||
msgstr ""
|
||||
msgstr "Uge"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:23
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar_week.html:51
|
||||
@@ -26490,7 +26473,7 @@ msgstr "Ingen arkiverede arrangementer fundet."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:149
|
||||
msgid "No public upcoming events found."
|
||||
msgstr ""
|
||||
msgstr "Der blev ikke fundet nogen kommende offentlige begivenheder."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/pagination.html:14
|
||||
#, python-format
|
||||
|
||||
@@ -5,7 +5,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2021-10-17 15:31+0000\n"
|
||||
"PO-Revision-Date: 2021-10-29 11:36+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
@@ -1880,16 +1880,21 @@ msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
"you additional taxes if you do not enter it."
|
||||
msgstr ""
|
||||
"Optional, jedoch müssen wir abhängig vom Land, in dem Sie sich befinden, "
|
||||
"zusätzliche Steuern erheben, wenn Sie keine USt-ID-Nummer angeben."
|
||||
|
||||
#: pretix/base/forms/questions.py:927 pretix/base/forms/questions.py:933
|
||||
msgid "If you are registered in Switzerland, you can enter your UID instead."
|
||||
msgstr ""
|
||||
msgstr "Wenn Sie in der Schweiz registriert sind, geben Sie bitte Ihre UID ein."
|
||||
|
||||
#: pretix/base/forms/questions.py:931
|
||||
msgid ""
|
||||
"Optional, but it might be required for you to claim tax benefits on your "
|
||||
"invoice depending on your and the seller’s country of residence."
|
||||
msgstr ""
|
||||
"Optional, aber abhängig von Ihrem Land und dem Land des Verkäufers "
|
||||
"möglicherweise notwendig, damit Sie die Rechnung steuerlich geltend machen "
|
||||
"können."
|
||||
|
||||
#: pretix/base/forms/questions.py:1020
|
||||
msgid "You need to provide a company name."
|
||||
@@ -6677,6 +6682,9 @@ msgid ""
|
||||
"only requested from business customers in the following countries: "
|
||||
"{countries}"
|
||||
msgstr ""
|
||||
"Funktioniert nur, wenn auch nach einer Rechnungsadresse gefragt ist. Die USt-"
|
||||
"ID ist nie ein Pflichtfeld und wird nur von Firmenkunden aus folgenden "
|
||||
"Ländern abgefragt: {countries}"
|
||||
|
||||
#: pretix/base/settings.py:389
|
||||
msgid "Invoice address explanation"
|
||||
@@ -16340,10 +16348,8 @@ msgid "View email history"
|
||||
msgstr "E-Mail-Verlauf"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:87
|
||||
#, fuzzy
|
||||
#| msgid "View email history"
|
||||
msgid "View transaction history"
|
||||
msgstr "E-Mail-Verlauf"
|
||||
msgstr "Transaktionen ansehen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:98
|
||||
msgid "Expire order"
|
||||
@@ -16937,44 +16943,32 @@ msgstr "Absenden"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:5
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:8
|
||||
#, fuzzy
|
||||
#| msgid "Transactions"
|
||||
msgid "Transaction history"
|
||||
msgstr "Transaktionen"
|
||||
msgstr "Transaktionsverlauf"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:23
|
||||
#, fuzzy
|
||||
#| msgid "Original price"
|
||||
msgid "Single price"
|
||||
msgstr "Ursprünglicher Preis"
|
||||
msgstr "Einzelpreis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:24
|
||||
#, fuzzy
|
||||
#| msgid "Total value"
|
||||
msgid "Total tax value"
|
||||
msgstr "Gesamtbetrag"
|
||||
msgstr "Gesamt-Steuerbetrag"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:25
|
||||
#, fuzzy
|
||||
#| msgid "Net price"
|
||||
msgid "Total price"
|
||||
msgstr "Nettopreis"
|
||||
msgstr "Gesamtbetrag"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:36
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This payment was created with an older version of pretix, therefore "
|
||||
#| "accurate data might not be available."
|
||||
msgid ""
|
||||
"This order was created before we introduced this table, therefore this data "
|
||||
"might be inaccurate."
|
||||
msgstr ""
|
||||
"Diese Zahlung wurde mit einer älteren pretix-Version erzeugt, daher sind "
|
||||
"vollständige und korrekte Daten gegebenenfalls nicht verfügbar."
|
||||
"Diese Bestellung wurde erstellt, bevor diese Tabelle eingeführt wurde. Die "
|
||||
"Daten können daher ungenau sein."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:65
|
||||
msgid "Sum"
|
||||
msgstr ""
|
||||
msgstr "Summe"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/orders/cancel.html:9
|
||||
msgid ""
|
||||
@@ -20419,13 +20413,10 @@ msgid "No country specified."
|
||||
msgstr "Es wurde kein Land angegeben."
|
||||
|
||||
#: pretix/control/views/orders.py:1336
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "VAT ID could not be checked since a non-EU country has been specified."
|
||||
msgid "VAT ID could not be checked since this country is not supported."
|
||||
msgstr ""
|
||||
"Die USt-ID-Nr. konnte nicht geprüft werden, da ein Nicht-EU-Land angegeben "
|
||||
"wurde."
|
||||
"Die USt-ID-Nr. konnte nicht geprüft werden, da dieses Land nicht unterstützt "
|
||||
"wird."
|
||||
|
||||
#: pretix/control/views/orders.py:1347
|
||||
msgid ""
|
||||
@@ -26213,7 +26204,7 @@ msgstr "Türkisch"
|
||||
|
||||
#: pretix/settings.py:517
|
||||
msgid "Galician"
|
||||
msgstr ""
|
||||
msgstr "Galicisch"
|
||||
|
||||
#: pretix/settings.py:831
|
||||
msgid "User profile only"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2021-10-17 15:32+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2021-11-03 22:00+0000\n"
|
||||
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
"Language: de_Informal\n"
|
||||
@@ -1881,16 +1881,21 @@ msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
"you additional taxes if you do not enter it."
|
||||
msgstr ""
|
||||
"Optional, jedoch müssen wir abhängig vom Land, in dem du sich befindest, "
|
||||
"zusätzliche Steuern erheben, wenn du keine USt-ID-Nummer angibst."
|
||||
|
||||
#: pretix/base/forms/questions.py:927 pretix/base/forms/questions.py:933
|
||||
msgid "If you are registered in Switzerland, you can enter your UID instead."
|
||||
msgstr ""
|
||||
msgstr "Wenn du in der Schweiz registriert bist, gib bitte deine UID ein."
|
||||
|
||||
#: pretix/base/forms/questions.py:931
|
||||
msgid ""
|
||||
"Optional, but it might be required for you to claim tax benefits on your "
|
||||
"invoice depending on your and the seller’s country of residence."
|
||||
msgstr ""
|
||||
"Optional, aber abhängig von deinem Land und dem Land des Verkäufers "
|
||||
"möglicherweise notwendig, damit du die Rechnung steuerlich geltend machen "
|
||||
"kannst."
|
||||
|
||||
#: pretix/base/forms/questions.py:1020
|
||||
msgid "You need to provide a company name."
|
||||
@@ -6670,6 +6675,9 @@ msgid ""
|
||||
"only requested from business customers in the following countries: "
|
||||
"{countries}"
|
||||
msgstr ""
|
||||
"Funktioniert nur, wenn auch nach einer Rechnungsadresse gefragt ist. Die USt-"
|
||||
"ID ist nie ein Pflichtfeld und wird nur von Firmenkunden aus folgenden "
|
||||
"Ländern abgefragt: {countries}"
|
||||
|
||||
#: pretix/base/settings.py:389
|
||||
msgid "Invoice address explanation"
|
||||
@@ -16319,10 +16327,8 @@ msgid "View email history"
|
||||
msgstr "E-Mail-Verlauf"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:87
|
||||
#, fuzzy
|
||||
#| msgid "View email history"
|
||||
msgid "View transaction history"
|
||||
msgstr "E-Mail-Verlauf"
|
||||
msgstr "Transaktionen ansehen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:98
|
||||
msgid "Expire order"
|
||||
@@ -16914,44 +16920,32 @@ msgstr "Absenden"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:5
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:8
|
||||
#, fuzzy
|
||||
#| msgid "Transactions"
|
||||
msgid "Transaction history"
|
||||
msgstr "Transaktionen"
|
||||
msgstr "Transaktionsverlauf"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:23
|
||||
#, fuzzy
|
||||
#| msgid "Original price"
|
||||
msgid "Single price"
|
||||
msgstr "Ursprünglicher Preis"
|
||||
msgstr "Einzelpreis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:24
|
||||
#, fuzzy
|
||||
#| msgid "Total value"
|
||||
msgid "Total tax value"
|
||||
msgstr "Gesamtbetrag"
|
||||
msgstr "Gesamt-Steuerbetrag"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:25
|
||||
#, fuzzy
|
||||
#| msgid "Net price"
|
||||
msgid "Total price"
|
||||
msgstr "Nettopreis"
|
||||
msgstr "Gesamtbetrag"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:36
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This payment was created with an older version of pretix, therefore "
|
||||
#| "accurate data might not be available."
|
||||
msgid ""
|
||||
"This order was created before we introduced this table, therefore this data "
|
||||
"might be inaccurate."
|
||||
msgstr ""
|
||||
"Diese Zahlung wurde mit einer älteren pretix-Version erzeugt, daher sind "
|
||||
"vollständige und korrekte Daten gegebenenfalls nicht verfügbar."
|
||||
"Diese Bestellung wurde erstellt, bevor diese Tabelle eingeführt wurde. Die "
|
||||
"Daten können daher ungenau sein."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:65
|
||||
msgid "Sum"
|
||||
msgstr ""
|
||||
msgstr "Summe"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/orders/cancel.html:9
|
||||
msgid ""
|
||||
@@ -20388,13 +20382,10 @@ msgid "No country specified."
|
||||
msgstr "Es wurde kein Land angegeben."
|
||||
|
||||
#: pretix/control/views/orders.py:1336
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "VAT ID could not be checked since a non-EU country has been specified."
|
||||
msgid "VAT ID could not be checked since this country is not supported."
|
||||
msgstr ""
|
||||
"Die USt-ID-Nr. konnte nicht geprüft werden, da ein Nicht-EU-Land angegeben "
|
||||
"wurde."
|
||||
"Die USt-ID-Nr. konnte nicht geprüft werden, da dieses Land nicht unterstützt "
|
||||
"wird."
|
||||
|
||||
#: pretix/control/views/orders.py:1347
|
||||
msgid ""
|
||||
@@ -22744,11 +22735,11 @@ msgstr ""
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:17
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:94
|
||||
msgid "Create a new rule"
|
||||
msgstr "Neue Steuer-Regel erstellen"
|
||||
msgstr "Neue Regel erstellen"
|
||||
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:24
|
||||
msgid "Email subject"
|
||||
msgstr "E-Mail wurde verschickt"
|
||||
msgstr "E-Mail-Betreff"
|
||||
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:26
|
||||
msgid "Scheduled time"
|
||||
@@ -25370,7 +25361,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:165
|
||||
msgid "The following payment methods will be used to refund the money to you:"
|
||||
msgstr ""
|
||||
"Die folgenden Zahlungsmethoden werden verwendet, um dir das Geld "
|
||||
"Die folgenden Zahlungsmethoden werden verwendet, um Ihnen das Geld "
|
||||
"zurückzuerstatten:"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:145
|
||||
@@ -26163,7 +26154,7 @@ msgstr "Türkisch"
|
||||
|
||||
#: pretix/settings.py:517
|
||||
msgid "Galician"
|
||||
msgstr ""
|
||||
msgstr "Galicisch"
|
||||
|
||||
#: pretix/settings.py:831
|
||||
msgid "User profile only"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2021-10-04 21:00+0000\n"
|
||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fi/>\n"
|
||||
@@ -2953,10 +2953,8 @@ msgid "Allowed membership types"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py:527 pretix/base/models/items.py:812
|
||||
#, fuzzy
|
||||
#| msgid "Has any membership"
|
||||
msgid "Hide without a valid membership"
|
||||
msgstr "On jäseniä"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py:528 pretix/base/models/items.py:813
|
||||
msgid ""
|
||||
@@ -7657,7 +7655,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py:2713 pretix/base/settings.py:2731
|
||||
#: pretix/base/settings.py:2750
|
||||
msgid "Given name"
|
||||
msgstr ""
|
||||
msgstr "Etunimi"
|
||||
|
||||
#: pretix/base/settings.py:2557 pretix/base/settings.py:2570
|
||||
#: pretix/base/settings.py:2586 pretix/base/settings.py:2602
|
||||
@@ -7666,7 +7664,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py:2714 pretix/base/settings.py:2732
|
||||
#: pretix/base/settings.py:2751
|
||||
msgid "Family name"
|
||||
msgstr ""
|
||||
msgstr "Sukunimi"
|
||||
|
||||
#: pretix/base/settings.py:2561 pretix/base/settings.py:2577
|
||||
#: pretix/base/settings.py:2593 pretix/base/settings.py:2608
|
||||
@@ -7703,11 +7701,11 @@ msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:2600 pretix/base/settings.py:2617
|
||||
msgid "First name"
|
||||
msgstr ""
|
||||
msgstr "Etunimi"
|
||||
|
||||
#: pretix/base/settings.py:2601 pretix/base/settings.py:2618
|
||||
msgid "Middle name"
|
||||
msgstr ""
|
||||
msgstr "Toiset nimet"
|
||||
|
||||
#: pretix/base/settings.py:2682 pretix/base/settings.py:2693
|
||||
#: pretix/control/forms/organizer.py:412
|
||||
@@ -7717,7 +7715,7 @@ msgstr "Matti Meikäläinen"
|
||||
|
||||
#: pretix/base/settings.py:2688
|
||||
msgid "Calling name"
|
||||
msgstr ""
|
||||
msgstr "Kutsumanimi"
|
||||
|
||||
#: pretix/base/settings.py:2701
|
||||
msgid "Latin transcription"
|
||||
@@ -9701,10 +9699,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/orders.py:619
|
||||
#, fuzzy
|
||||
#| msgid "All invoices"
|
||||
msgid "Attach invoices"
|
||||
msgstr "Kaikki laskut"
|
||||
msgstr "Liitä laskut"
|
||||
|
||||
#: pretix/control/forms/orders.py:645
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:25
|
||||
@@ -14505,10 +14501,8 @@ msgid "View email history"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:87
|
||||
#, fuzzy
|
||||
#| msgid "Show account history"
|
||||
msgid "View transaction history"
|
||||
msgstr "Näytä tilin historia"
|
||||
msgstr "Näytä toimenpidehistoria"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:98
|
||||
msgid "Expire order"
|
||||
@@ -14582,10 +14576,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:255
|
||||
#, fuzzy
|
||||
#| msgid "Canceled by customer"
|
||||
msgid "Invoice was emailed to customer"
|
||||
msgstr "Asiakkaan peruuttama"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:260
|
||||
msgid "Invoice was not yet emailed to customer"
|
||||
@@ -14615,10 +14607,8 @@ msgid "Cancel and reissue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:300
|
||||
#, fuzzy
|
||||
#| msgid "All invoices"
|
||||
msgid "Email invoices"
|
||||
msgstr "Kaikki laskut"
|
||||
msgstr "Lähetä laskut sähköpostilla"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:309
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:321
|
||||
@@ -15062,10 +15052,8 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:5
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:8
|
||||
#, fuzzy
|
||||
#| msgid "Check-in history"
|
||||
msgid "Transaction history"
|
||||
msgstr "Ilmoittautumishistoria"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:23
|
||||
#, fuzzy
|
||||
@@ -22052,19 +22040,18 @@ msgid "incl. %(tax_sum)s taxes"
|
||||
msgstr "sis. %(tax_sum)s veroja"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:388
|
||||
#, fuzzy, python-format
|
||||
#| msgid "The items in your cart are reserved for you for %(minutes)s minutes."
|
||||
#, python-format
|
||||
msgid "The items in your cart are reserved for you for %(minutes)s minutes."
|
||||
msgstr ""
|
||||
"Ostoskorissasi olevat tuotteet on varattu sinulle %(minutes)s minuutiksi."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:392
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are no longer reserved for you."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
|
||||
msgstr ""
|
||||
"Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle. Voit silti "
|
||||
"suorittaa tilauksen loppuun niin kauan kuin tuotteita on saatavilla."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:396
|
||||
msgid "Overview of your ordered products."
|
||||
@@ -22358,12 +22345,12 @@ msgstr "Tämän tapahtuman ennakkomyynti on päättynyt."
|
||||
#: pretix/presale/views/widget.py:664
|
||||
#, python-format
|
||||
msgid "The presale for this event will start on %(date)s at %(time)s."
|
||||
msgstr ""
|
||||
msgstr "Tämän tapahtuman myynti alkaa %(date)s klo %(time)s."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:153
|
||||
#: pretix/presale/views/waiting.py:102 pretix/presale/views/widget.py:669
|
||||
msgid "The presale for this event has not yet started."
|
||||
msgstr ""
|
||||
msgstr "Tapahtuman myynti ei ole vielä alkanut."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:163
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:164
|
||||
|
||||
@@ -8,10 +8,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
|
||||
"PO-Revision-Date: 2021-10-04 21:00+0000\n"
|
||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/fi/>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/fi/>\n"
|
||||
"Language: fi\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -497,12 +497,12 @@ msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are no longer reserved for you."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
|
||||
msgstr ""
|
||||
"Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle. Voit silti "
|
||||
"suorittaa tilauksen loppuun niin kauan kuin tuotteita on saatavilla."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||
msgid "Cart expired"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,199 +3,200 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"PO-Revision-Date: 2021-11-04 07:00+0000\n"
|
||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/gl/>\n"
|
||||
"Language: gl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
|
||||
msgid "Marked as paid"
|
||||
msgstr ""
|
||||
msgstr "Marcado como pagado"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
||||
msgid "Comment:"
|
||||
msgstr ""
|
||||
msgstr "Comentario:"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Placed orders"
|
||||
msgstr ""
|
||||
msgstr "Ordes enviadas"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Paid orders"
|
||||
msgstr ""
|
||||
msgstr "Ordes pagadas"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
msgid "Total revenue"
|
||||
msgstr ""
|
||||
msgstr "Ingresos totais"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr ""
|
||||
msgstr "Contactando con Stripe…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
msgstr "Total"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
msgstr "Confirmando o pagamento…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
msgstr "Contactando co banco…"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
|
||||
msgid "Select a check-in list"
|
||||
msgstr ""
|
||||
msgstr "Seleccion unha lista de rexistro"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
|
||||
msgid "No active check-in lists found."
|
||||
msgstr ""
|
||||
msgstr "Non se atoparon listas de rexistro activas."
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
|
||||
msgid "Switch check-in list"
|
||||
msgstr ""
|
||||
msgstr "Cambiar lista de rexistro"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
|
||||
msgid "Search results"
|
||||
msgstr ""
|
||||
msgstr "Resultados da procura"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
|
||||
msgid "No tickets found"
|
||||
msgstr ""
|
||||
msgstr "Non se atoparon tickets"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
msgstr "Resultado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
|
||||
msgid "This ticket requires special attention"
|
||||
msgstr ""
|
||||
msgstr "Este ticket require atención especial"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
|
||||
msgid "Switch direction"
|
||||
msgstr ""
|
||||
msgstr "Cambiar dirección"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
msgstr "Ingreso"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
msgstr "Saída"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
|
||||
msgid "Scan a ticket or search and press return…"
|
||||
msgstr ""
|
||||
msgstr "Escanee o ticket ou busque e presione volver…"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
|
||||
msgid "Load more"
|
||||
msgstr ""
|
||||
msgstr "Cargar máis"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
msgstr "Válido"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
|
||||
msgid "Unpaid"
|
||||
msgstr ""
|
||||
msgstr "Sen pagar"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
|
||||
msgid "Canceled"
|
||||
msgstr ""
|
||||
msgstr "Cancelado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||
msgid "Redeemed"
|
||||
msgstr ""
|
||||
msgstr "Trocado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
msgstr "Continuar"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||
msgid "Ticket not paid"
|
||||
msgstr ""
|
||||
msgstr "Ticket pendente de pago"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||
msgstr ""
|
||||
msgstr "Este ticket aínda non se pagou ¿Desexa continuar de todos modos?"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||
msgid "Additional information required"
|
||||
msgstr ""
|
||||
msgstr "Requírese de información adicional"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||
msgid "Valid ticket"
|
||||
msgstr ""
|
||||
msgstr "Ticket válido"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||
msgid "Exit recorded"
|
||||
msgstr ""
|
||||
msgstr "Saída rexistrada"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||
msgid "Ticket already used"
|
||||
msgstr ""
|
||||
msgstr "Este ticket xa foi utilizado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||
msgid "Information required"
|
||||
msgstr ""
|
||||
msgstr "Información requerida"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||
msgid "Unknown ticket"
|
||||
msgstr ""
|
||||
msgstr "Non se atopou ticket"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||
msgid "Ticket type not allowed here"
|
||||
msgstr ""
|
||||
msgstr "Tipo de ticket non permitido"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||
msgid "Entry not allowed"
|
||||
msgstr ""
|
||||
msgstr "Entrada non permitida"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||
msgid "Ticket code revoked/changed"
|
||||
msgstr ""
|
||||
msgstr "Código de ticket revocado/cambiado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||
msgid "Order canceled"
|
||||
msgstr ""
|
||||
msgstr "Orde cancelada"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||
msgid "Checked-in Tickets"
|
||||
msgstr ""
|
||||
msgstr "Rexistro de código QR"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||
msgid "Valid Tickets"
|
||||
msgstr ""
|
||||
msgstr "Tickets válidos"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||
msgid "Currently inside"
|
||||
msgstr ""
|
||||
msgstr "Actualmente dentro"
|
||||
|
||||
#: pretix/static/lightbox/js/lightbox.js:96
|
||||
msgid "close"
|
||||
msgstr ""
|
||||
msgstr "cerrar"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
@@ -203,11 +204,13 @@ msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
"A sua solicitude estase procesando. Isto pode tardar varios minutos, "
|
||||
"dependendo do tamaño do seu evento."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
msgstr "A sua solicitude foi enviada ao servidor e será procesada en breve."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
@@ -222,7 +225,7 @@ msgstr ""
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr ""
|
||||
msgstr "Ocurreu un error de tipo {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
@@ -243,7 +246,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr ""
|
||||
msgstr "Estamos procesando a sua solicitude…"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
@@ -255,32 +258,32 @@ msgstr ""
|
||||
#: pretix/static/pretixbase/js/asynctask.js:264
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:34
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
msgstr "Cerrar mensaxe"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/clipboard.js:23
|
||||
msgid "Copied!"
|
||||
msgstr ""
|
||||
msgstr "¡Copiado!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/clipboard.js:29
|
||||
msgid "Press Ctrl-C to copy!"
|
||||
msgstr ""
|
||||
msgstr "¡Presione Control+C para copiar!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
|
||||
msgid "is one of"
|
||||
msgstr ""
|
||||
msgstr "é un de"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
|
||||
msgid "is before"
|
||||
msgstr ""
|
||||
msgstr "está antes"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
|
||||
msgid "is after"
|
||||
msgstr ""
|
||||
msgstr "está despois"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
|
||||
msgid "Product"
|
||||
msgstr ""
|
||||
msgstr "Producto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
|
||||
msgid "Product variation"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2021-07-19 12:27+0000\n"
|
||||
"Last-Translator: dedecosta <dedecosta2@live.it>\n"
|
||||
"PO-Revision-Date: 2021-11-17 01:00+0000\n"
|
||||
"Last-Translator: +se <sebastiano@endsummercamp.org>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"it/>\n"
|
||||
"Language: it\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.6\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:28
|
||||
msgid ""
|
||||
@@ -3055,7 +3055,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py:548
|
||||
msgid "Membership duration in months"
|
||||
msgstr ""
|
||||
msgstr "Durata dell'abbonamento in mesi"
|
||||
|
||||
#: pretix/base/models/items.py:556 pretix/base/models/items.py:1253
|
||||
#: pretix/control/forms/filter.py:375 pretix/control/forms/filter.py:1390
|
||||
@@ -6913,11 +6913,11 @@ msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:1240 pretix/base/settings.py:1248
|
||||
msgid "Week calendar"
|
||||
msgstr ""
|
||||
msgstr "Calendario settimanale"
|
||||
|
||||
#: pretix/base/settings.py:1241 pretix/base/settings.py:1249
|
||||
msgid "Month calendar"
|
||||
msgstr ""
|
||||
msgstr "Calendario mensile"
|
||||
|
||||
#: pretix/base/settings.py:1245
|
||||
msgid "Default overview style"
|
||||
@@ -6928,6 +6928,8 @@ msgid ""
|
||||
"If your event series has more than 50 dates in the future, only the month or "
|
||||
"week calendar can be used."
|
||||
msgstr ""
|
||||
"Se la tua serie di eventi ha più di 50 date nel futuro, può essere usato "
|
||||
"solo il calendario mensile e o settimanale."
|
||||
|
||||
#: pretix/base/settings.py:1260
|
||||
msgid "Hide all unavailable dates from calendar or list views"
|
||||
@@ -7289,13 +7291,13 @@ msgid ""
|
||||
msgstr ""
|
||||
"Ciao,\n"
|
||||
"\n"
|
||||
"abbiamo ricevuto il tuo ordine per {event}\n"
|
||||
"per un valore totale di {total_with_currency}.\n"
|
||||
"Ti preghiamo di effettuare il pagamento entro il {expire_date}.\n"
|
||||
"abbiamo ricevuto il tuo ordine per {event} con un valore totale\n"
|
||||
"di {total_with_currency}. Ti preghiamo di effettuare il pagamento entro il "
|
||||
"{expire_date}.\n"
|
||||
"\n"
|
||||
"{payment_info}\n"
|
||||
"\n"
|
||||
"Puoi modificare i dettagli dell'ordine e vedere lo stato qui:\n"
|
||||
"Puoi modificare i dettagli e vedere lo stato dell'ordine qui:\n"
|
||||
"{url}\n"
|
||||
"\n"
|
||||
"Un saluto,\n"
|
||||
@@ -9242,7 +9244,7 @@ msgstr "Prevendita non ancora attiva"
|
||||
#: pretix/control/templates/pretixcontrol/organizers/detail.html:106
|
||||
#: pretix/control/templates/pretixcontrol/subevents/index.html:152
|
||||
msgid "Presale over"
|
||||
msgstr "Prevendita esaurita"
|
||||
msgstr "Prevendita conclusa"
|
||||
|
||||
#: pretix/control/forms/filter.py:833 pretix/control/forms/filter.py:836
|
||||
#: pretix/control/forms/filter.py:1835
|
||||
@@ -9271,7 +9273,7 @@ msgstr "Data fino a"
|
||||
#: pretix/control/forms/filter.py:857 pretix/control/forms/subevents.py:526
|
||||
#: pretix/control/forms/subevents.py:565
|
||||
msgid "Weekday"
|
||||
msgstr ""
|
||||
msgstr "Giorno della settimana"
|
||||
|
||||
#: pretix/control/forms/filter.py:859
|
||||
msgid "Monday"
|
||||
@@ -10396,11 +10398,11 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/subevents.py:452
|
||||
msgid "month(s)"
|
||||
msgstr ""
|
||||
msgstr "mese(i)"
|
||||
|
||||
#: pretix/control/forms/subevents.py:453
|
||||
msgid "week(s)"
|
||||
msgstr ""
|
||||
msgstr "settimana (e)"
|
||||
|
||||
#: pretix/control/forms/subevents.py:454
|
||||
msgid "day(s)"
|
||||
@@ -10444,7 +10446,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/subevents.py:527 pretix/control/forms/subevents.py:566
|
||||
msgid "Weekend day"
|
||||
msgstr ""
|
||||
msgstr "Giorno del fine settimana"
|
||||
|
||||
#: pretix/control/forms/users.py:121 pretix/control/views/user.py:212
|
||||
msgid "Your changes could not be saved. See below for details."
|
||||
@@ -14403,7 +14405,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:136
|
||||
msgid "months"
|
||||
msgstr ""
|
||||
msgstr "mesi"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:138
|
||||
msgid "Membership duration after purchase"
|
||||
@@ -17071,12 +17073,12 @@ msgstr ""
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:204
|
||||
#, python-format
|
||||
msgid "On the %(setpos)s %(weekday)s of %(month)s"
|
||||
msgstr ""
|
||||
msgstr "Il %(setpos)s %(weekday)s del %(month)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:103
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:214
|
||||
msgid "At the same date every month"
|
||||
msgstr ""
|
||||
msgstr "Ogni mese lo stesso giorno"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:107
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:218
|
||||
@@ -18266,7 +18268,7 @@ msgstr "Nessuna data"
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:120
|
||||
#: pretix/presale/views/widget.py:385
|
||||
msgid "Sale over"
|
||||
msgstr ""
|
||||
msgstr "Prenotazioni chiuse"
|
||||
|
||||
#: pretix/control/views/dashboards.py:533
|
||||
#: pretix/presale/templates/pretixpresale/fragment_calendar.html:99
|
||||
@@ -18349,7 +18351,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/event.py:697 pretix/control/views/organizer.py:320
|
||||
msgid "invalid item"
|
||||
msgstr ""
|
||||
msgstr "oggetto non valido"
|
||||
|
||||
#: pretix/control/views/event.py:754
|
||||
msgid "Unknown e-mail renderer."
|
||||
@@ -18717,12 +18719,10 @@ msgid "We've been unable to parse the uploaded file as a CSV file."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py:326
|
||||
#, fuzzy
|
||||
#| msgid "All invoices"
|
||||
msgid "Your invoice"
|
||||
msgid_plural "Your invoices"
|
||||
msgstr[0] "Tutte le fatture"
|
||||
msgstr[1] "Tutte le fatture"
|
||||
msgstr[0] "La tua fattura"
|
||||
msgstr[1] "Le tue fatture"
|
||||
|
||||
#: pretix/control/views/orders.py:328
|
||||
#, python-brace-format
|
||||
@@ -23217,12 +23217,12 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:8
|
||||
#, python-format
|
||||
msgid "Show previous month, %(month)s"
|
||||
msgstr ""
|
||||
msgstr "Mostra il mese precedente, %(month)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:20
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar.html:45
|
||||
msgid "Select month and year to show"
|
||||
msgstr ""
|
||||
msgstr "Seleziona un mese e anno da mostrare"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:21
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar.html:36
|
||||
@@ -23230,7 +23230,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar_week.html:38
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:44
|
||||
msgid "Month"
|
||||
msgstr ""
|
||||
msgstr "Mese"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:26
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:26
|
||||
@@ -23248,23 +23248,23 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:39
|
||||
#, python-format
|
||||
msgid "Show next month, %(month)s"
|
||||
msgstr ""
|
||||
msgstr "Mostra il mese successivo, %(month)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:8
|
||||
#, python-format
|
||||
msgid "Show previous week, %(week)s"
|
||||
msgstr ""
|
||||
msgstr "Mostra la settimana precedente, %(week)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:20
|
||||
msgid "Select week and year to show"
|
||||
msgstr ""
|
||||
msgstr "Seleziona la settimana e l'anno da mostrare"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:21
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar.html:30
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar_week.html:32
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:39
|
||||
msgid "Week"
|
||||
msgstr ""
|
||||
msgstr "Settimana"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:23
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar_week.html:51
|
||||
@@ -23274,7 +23274,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:39
|
||||
#, python-format
|
||||
msgid "Show next week, %(week)s"
|
||||
msgstr ""
|
||||
msgstr "Mostra la prossima settimana, %(week)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html:24
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html:37
|
||||
@@ -23346,7 +23346,7 @@ msgstr "Altre date"
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:138
|
||||
#: pretix/presale/views/waiting.py:98 pretix/presale/views/widget.py:662
|
||||
msgid "The presale period for this event is over."
|
||||
msgstr ""
|
||||
msgstr "Il periodo di prevendita per questo evento è concluso."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:146
|
||||
#: pretix/presale/views/widget.py:664
|
||||
@@ -24270,7 +24270,7 @@ msgstr "Il tuo carrello è vuoto"
|
||||
|
||||
#: pretix/presale/views/checkout.py:59
|
||||
msgid "The presale for this event is over or has not yet started."
|
||||
msgstr ""
|
||||
msgstr "La prevendita pre questo evento è conclusa o non è ancora iniziata."
|
||||
|
||||
#: pretix/presale/views/customer.py:179
|
||||
msgid ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2020-11-27 04:00+0000\n"
|
||||
"PO-Revision-Date: 2021-11-03 10:49+0000\n"
|
||||
"Last-Translator: Svyatoslav <slava@digitalarthouse.eu>\n"
|
||||
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"lv/>\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= "
|
||||
"19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n"
|
||||
"X-Generator: Weblate 3.10.3\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:28
|
||||
msgid ""
|
||||
@@ -1532,7 +1532,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:831
|
||||
msgid "Blocking vouchers"
|
||||
msgstr "Bloķēt kuponus "
|
||||
msgstr "Bloķēt promokods"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:832 pretix/control/views/item.py:926
|
||||
msgid "Current user's carts"
|
||||
@@ -3015,7 +3015,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py:448
|
||||
msgid "This product can only be bought using a voucher."
|
||||
msgstr "Šo produktu var iegādāties tikai ar kuponu."
|
||||
msgstr "Šo produktu var iegādāties tikai ar promokodu."
|
||||
|
||||
#: pretix/base/models/items.py:450
|
||||
msgid ""
|
||||
@@ -3754,10 +3754,8 @@ msgid "Maximum usages"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/memberships.py:57
|
||||
#, fuzzy
|
||||
#| msgid "Number of times this voucher can be redeemed."
|
||||
msgid "Number of times this membership can be used in a purchase."
|
||||
msgstr "Cik reizes šo kuponu var izmantot."
|
||||
msgstr "Cik reizes šo promokodu var izmantot."
|
||||
|
||||
#: pretix/base/models/memberships.py:123
|
||||
#: pretix/control/templates/pretixcontrol/items/question.html:26
|
||||
@@ -4171,11 +4169,11 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/organizer.py:296
|
||||
msgid "Can view vouchers"
|
||||
msgstr "Var apskatīt kuponus"
|
||||
msgstr "Var apskatīt promokodu"
|
||||
|
||||
#: pretix/base/models/organizer.py:300
|
||||
msgid "Can change vouchers"
|
||||
msgstr "Var mainīt kuponus"
|
||||
msgstr "Var mainīt promokodi"
|
||||
|
||||
#: pretix/base/models/organizer.py:304
|
||||
#, python-format
|
||||
@@ -4307,7 +4305,7 @@ msgstr "Samaziniet produkta cenu par (%)"
|
||||
|
||||
#: pretix/base/models/vouchers.py:195
|
||||
msgid "Number of times this voucher can be redeemed."
|
||||
msgstr "Cik reizes šo kuponu var izmantot."
|
||||
msgstr "Cik reizes šo promokodu var izmantot."
|
||||
|
||||
#: pretix/base/models/vouchers.py:199 pretix/control/views/vouchers.py:109
|
||||
msgid "Redeemed"
|
||||
@@ -4345,7 +4343,7 @@ msgid ""
|
||||
"receive a ticket."
|
||||
msgstr ""
|
||||
"Aktivizējot šo kuponu, tas tiks atņemts no attiecīgā produkta kvotām tā, ka "
|
||||
"tiek garantēts, ka ikviens, kam ir šis kupona kods, saņem biļeti."
|
||||
"tiek garantēts, ka ikviens, kam ir šis promokods, saņem biļeti."
|
||||
|
||||
#: pretix/base/models/vouchers.py:223
|
||||
msgid "Allow to bypass quota"
|
||||
@@ -4400,8 +4398,8 @@ msgid ""
|
||||
"have been redeemed etc."
|
||||
msgstr ""
|
||||
"Varat izmantot šo lauku, lai grupētu vairākus kuponus kopā. Ja vairākiem "
|
||||
"kuponiem ievadāt vienādu vērtību, varat iegūt statistiku par to, cik daudz "
|
||||
"no tiem ir izmantoti utt."
|
||||
"promokodiem ievadāt vienādu vērtību, varat iegūt statistiku par to, cik "
|
||||
"daudz no tiem ir izmantoti utt."
|
||||
|
||||
#: pretix/base/models/vouchers.py:286
|
||||
msgid "Shows hidden products that match this voucher"
|
||||
@@ -4446,7 +4444,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/vouchers.py:335
|
||||
msgid "It is currently not possible to create vouchers for add-on products."
|
||||
msgstr "Pašlaik nav iespējams izveidot kuponus papildinājumu produktiem."
|
||||
msgstr "Pašlaik nav iespējams izveidot promokodi papildinājumu produktiem."
|
||||
|
||||
#: pretix/base/models/vouchers.py:337 pretix/base/models/vouchers.py:430
|
||||
#, fuzzy
|
||||
@@ -4480,8 +4478,8 @@ msgid ""
|
||||
"You cannot create a voucher that blocks quota as the selected product or "
|
||||
"quota is currently sold out or completely reserved."
|
||||
msgstr ""
|
||||
"Jūs nevarat izveidot kuponu, kas bloķētu kvotu, jo izvēlētais produkts vai "
|
||||
"kvota šobrīd ir izpārdota vai pilnībā rezervēta."
|
||||
"Jūs nevarat izveidot promokodu, kas bloķētu kvotu, jo izvēlētais produkts "
|
||||
"vai kvota šobrīd ir izpārdota vai pilnībā rezervēta."
|
||||
|
||||
#: pretix/base/models/vouchers.py:440
|
||||
msgid "A voucher with this code already exists."
|
||||
@@ -5181,7 +5179,7 @@ msgid ""
|
||||
"You entered a voucher instead of a gift card. Vouchers can only be entered "
|
||||
"on the first page of the shop below the product selection."
|
||||
msgstr ""
|
||||
"Dāvanu kartes vietā ievadījāt kuponu. Kuponus var ievadīt tikai veikala "
|
||||
"Dāvanu kartes vietā ievadījāt kuponu. Promokodi var ievadīt tikai veikala "
|
||||
"pirmajā lapā zem preces izvēles."
|
||||
|
||||
#: pretix/base/payment.py:1248 pretix/base/payment.py:1290
|
||||
@@ -5756,7 +5754,7 @@ msgid ""
|
||||
"or that you tried to redeem it before but did not complete the checkout "
|
||||
"process. You can try to use it again in %d minutes."
|
||||
msgstr ""
|
||||
"Šis kupona kods šobrīd ir bloķēts, jo tas jau ir ietverts grozā. Tas varētu "
|
||||
"Šis promokods šobrīd ir bloķēts, jo tas jau ir ietverts grozā. Tas varētu "
|
||||
"nozīmēt, ka kāds cits šobrīd izmanto šo kuponu vai arī jūs mēģinājāt to "
|
||||
"izpirkt iepriekš, bet nepabeidzāt norēķinu procesu. Jūs varat mēģināt to "
|
||||
"izmantot vēlreiz pēc %d minūtēm."
|
||||
@@ -23534,7 +23532,7 @@ msgstr "Tukšs grozs"
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:48
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:297
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "Izmantot kuponu"
|
||||
msgstr "Izmantot promokodu"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:51
|
||||
msgid "We're applying this voucher to your cart..."
|
||||
@@ -23543,7 +23541,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:59
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:319
|
||||
msgid "Redeem voucher"
|
||||
msgstr "Izmantot kuponu"
|
||||
msgstr "Izmantot promokodu"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_checkoutflow.html:2
|
||||
#, fuzzy
|
||||
|
||||
@@ -7,10 +7,10 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 10:09+0000\n"
|
||||
"PO-Revision-Date: 2021-10-29 02:00+0000\n"
|
||||
"PO-Revision-Date: 2021-11-15 00:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
|
||||
">\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -1884,16 +1884,20 @@ msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
"you additional taxes if you do not enter it."
|
||||
msgstr ""
|
||||
"Optioneel, maar afhankelijk van het land waarin u woont moeten we misschien "
|
||||
"extra belasting rekenen als u dit niet invoert."
|
||||
|
||||
#: pretix/base/forms/questions.py:927 pretix/base/forms/questions.py:933
|
||||
msgid "If you are registered in Switzerland, you can enter your UID instead."
|
||||
msgstr ""
|
||||
msgstr "Als u in Zwitserland geregistreert bent kunt u hier uw UID invoeren."
|
||||
|
||||
#: pretix/base/forms/questions.py:931
|
||||
msgid ""
|
||||
"Optional, but it might be required for you to claim tax benefits on your "
|
||||
"invoice depending on your and the seller’s country of residence."
|
||||
msgstr ""
|
||||
"Optioneel, maar afhankelijk van het land waarin u woont en het land van de "
|
||||
"verkoper heeft u dit misschien nodig om belasting terug te kunnen vragen."
|
||||
|
||||
#: pretix/base/forms/questions.py:1020
|
||||
msgid "You need to provide a company name."
|
||||
@@ -6665,6 +6669,9 @@ msgid ""
|
||||
"only requested from business customers in the following countries: "
|
||||
"{countries}"
|
||||
msgstr ""
|
||||
"Werkt alleen als er om een factuuradres wordt gevraagd. Btw-nummer is nooit "
|
||||
"verplicht en wordt alleen gevraagd aan zakelijke klanten in de volgende "
|
||||
"landen: {countries}"
|
||||
|
||||
#: pretix/base/settings.py:389
|
||||
msgid "Invoice address explanation"
|
||||
@@ -16306,10 +16313,8 @@ msgid "View email history"
|
||||
msgstr "Toon emailgeschiedenis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:87
|
||||
#, fuzzy
|
||||
#| msgid "View email history"
|
||||
msgid "View transaction history"
|
||||
msgstr "Toon emailgeschiedenis"
|
||||
msgstr "Toon transactiegeschiedenis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:98
|
||||
msgid "Expire order"
|
||||
@@ -16902,44 +16907,32 @@ msgstr "Versturen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:5
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:8
|
||||
#, fuzzy
|
||||
#| msgid "Transactions"
|
||||
msgid "Transaction history"
|
||||
msgstr "Transacties"
|
||||
msgstr "Transactiegeschiedenis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:23
|
||||
#, fuzzy
|
||||
#| msgid "Original price"
|
||||
msgid "Single price"
|
||||
msgstr "Originele prijs"
|
||||
msgstr "Eenheidsprijs"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:24
|
||||
#, fuzzy
|
||||
#| msgid "Total value"
|
||||
msgid "Total tax value"
|
||||
msgstr "Totaalwaarde"
|
||||
msgstr "Totale belastingwaarde"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:25
|
||||
#, fuzzy
|
||||
#| msgid "Net price"
|
||||
msgid "Total price"
|
||||
msgstr "Nettoprijs"
|
||||
msgstr "Totaalbedrag"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:36
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This payment was created with an older version of pretix, therefore "
|
||||
#| "accurate data might not be available."
|
||||
msgid ""
|
||||
"This order was created before we introduced this table, therefore this data "
|
||||
"might be inaccurate."
|
||||
msgstr ""
|
||||
"Deze betaling is aangemaakt met een oudere versie van pretix, hierom kan "
|
||||
"nauwkeurige data mogelijk niet aanwezig zijn."
|
||||
"Deze bestelling is aangemaakt voordat deze tabel is aangemaakt. Hierom "
|
||||
"kunnen deze gegevens mogelijk onnauwkeurig zijn."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:65
|
||||
msgid "Sum"
|
||||
msgstr ""
|
||||
msgstr "Som"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/orders/cancel.html:9
|
||||
msgid ""
|
||||
@@ -20378,13 +20371,10 @@ msgid "No country specified."
|
||||
msgstr "Geen land opgegeven."
|
||||
|
||||
#: pretix/control/views/orders.py:1336
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "VAT ID could not be checked since a non-EU country has been specified."
|
||||
msgid "VAT ID could not be checked since this country is not supported."
|
||||
msgstr ""
|
||||
"Btw-nummer kon niet worden gecontroleerd, omdat een land van buiten de EU "
|
||||
"was opgegeven."
|
||||
"Btw-nummer kon niet worden gecontroleerd omdat dit land niet wordt "
|
||||
"ondersteund."
|
||||
|
||||
#: pretix/control/views/orders.py:1347
|
||||
msgid ""
|
||||
@@ -22246,7 +22236,7 @@ msgstr "Laatste update"
|
||||
#: pretix/plugins/paypal/templates/pretixplugins/paypal/control.html:13
|
||||
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:53
|
||||
msgid "Total value"
|
||||
msgstr "Totaalwaarde"
|
||||
msgstr "Totaalbedrag"
|
||||
|
||||
#: pretix/plugins/paypal/templates/pretixplugins/paypal/pending.html:4
|
||||
msgid ""
|
||||
@@ -23440,7 +23430,7 @@ msgstr ""
|
||||
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:29
|
||||
msgid ""
|
||||
"The payment transaction could not be completed for the following reason:"
|
||||
msgstr "De betalingstransactie kon om de volgende reden niet voltooid worden:"
|
||||
msgstr "De betaling kon om de volgende reden niet voltooid worden:"
|
||||
|
||||
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html:5
|
||||
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca_return.html:6
|
||||
@@ -23499,7 +23489,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/views.py:565 pretix/plugins/stripe/views.py:568
|
||||
msgid "Sorry, there was an error in the payment process."
|
||||
msgstr "Sorry, is iets misgegaan in het betalingsproces."
|
||||
msgstr "Sorry, er is iets misgegaan in het betalingsproces."
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/apps.py:44
|
||||
#: pretix/plugins/ticketoutputpdf/apps.py:47
|
||||
@@ -24955,6 +24945,9 @@ msgid ""
|
||||
"you want, you can add yourself to the waiting list. We will then notify if "
|
||||
"seats are available again."
|
||||
msgstr ""
|
||||
"Sommige van de categorieën in de stoelplattegrond zijn momenteel "
|
||||
"uitverkocht. Als u dit wilt kunt u uzelf op de wachtlijst zetten om een "
|
||||
"melding te krijgen als er weer plaatsen beschikbaar zijn."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:262
|
||||
msgid "Join waiting list"
|
||||
@@ -25321,6 +25314,8 @@ msgid ""
|
||||
"The organizer will get in touch with you to clarify the details of your "
|
||||
"refund."
|
||||
msgstr ""
|
||||
"De organisator zal contact met u opnemen om de details van uw terugbetaling "
|
||||
"uit te leggen."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:97
|
||||
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:118
|
||||
@@ -25538,6 +25533,10 @@ msgid ""
|
||||
"need the ticket any more, please be so kind and remove your ticket from the "
|
||||
"list so we can pass it on to the next person waiting as quickly as possible!"
|
||||
msgstr ""
|
||||
"U bent geselecteerd van onze wachtlijst om een ticket te kunnen kopen. Als u "
|
||||
"geen ticket meer wilt kopen willen we u vragen om uw plek op de wachtlijst "
|
||||
"op te geven, zodat we uw plaats sneller aan de volgende persoon op de "
|
||||
"wachtlijst kunnen aanbieden."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/waitinglist_remove.html:16
|
||||
msgctxt "waitinglist"
|
||||
@@ -26045,6 +26044,7 @@ msgid ""
|
||||
"Thank you very much! We will assign your spot on the waiting list to someone "
|
||||
"else."
|
||||
msgstr ""
|
||||
"Bedankt! We zullen uw plaats op de wachtlijst toewijzen aan iemand anders."
|
||||
|
||||
#: pretix/presale/views/widget.py:320
|
||||
msgid "This ticket shop is currently disabled."
|
||||
@@ -26141,7 +26141,7 @@ msgstr "Turks"
|
||||
|
||||
#: pretix/settings.py:517
|
||||
msgid "Galician"
|
||||
msgstr ""
|
||||
msgstr "Gallisch"
|
||||
|
||||
#: pretix/settings.py:831
|
||||
msgid "User profile only"
|
||||
|
||||
@@ -182,11 +182,11 @@ def parse(file):
|
||||
transaction_details = parse_transaction_details(td.replace("\n", ""))
|
||||
|
||||
payer = {
|
||||
'name': transaction_details.get('accountholder', ''),
|
||||
'name': transaction_details.get('accountholder', '') or t.data.get('applicant_name', ''),
|
||||
# In reality, these fields are sometimes IBANs and BICs, and sometimes legacy numbers. We don't
|
||||
# really know (except for a syntax check) which will be performed anyways much later in the stack.
|
||||
'iban': transaction_details.get('accountnumber', ''),
|
||||
'bic': transaction_details.get('blz', ''),
|
||||
'iban': transaction_details.get('accountnumber', '') or t.data.get('applicant_iban', ''),
|
||||
'bic': transaction_details.get('blz', '') or t.data.get('applicant_bin', ''),
|
||||
}
|
||||
reference, eref = join_reference(transaction_details.get('reference', '').split('\n'), payer)
|
||||
if not eref:
|
||||
@@ -200,11 +200,19 @@ def parse(file):
|
||||
**{k: payer[k].strip() for k in ("iban", "bic") if payer.get(k)}
|
||||
})
|
||||
else:
|
||||
payer = {
|
||||
'payer': t.data.get('applicant_name', ''),
|
||||
# In reality, these fields are sometimes IBANs and BICs, and sometimes legacy numbers. We don't
|
||||
# really know (except for a syntax check) which will be performed anyways much later in the stack.
|
||||
'iban': t.data.get('applicant_iban', ''),
|
||||
'bic': t.data.get('applicant_bin', ''),
|
||||
}
|
||||
result.append({
|
||||
'reference': "\n".join([
|
||||
t.data.get(f) for f in ('transaction_details', 'customer_reference', 'bank_reference',
|
||||
t.data.get(f) for f in ('transaction_details', 'customer_reference', 'bank_reference', 'purpose',
|
||||
'extra_details', 'non_swift_text') if t.data.get(f, '')]),
|
||||
'amount': str(round_decimal(t.data['amount'].amount)),
|
||||
'date': t.data['date'].isoformat()
|
||||
'date': t.data['date'].isoformat(),
|
||||
**{k: payer[k].strip() for k in ("iban", "bic", "payer") if payer.get(k)}
|
||||
})
|
||||
return result
|
||||
|
||||
111
src/pretix/plugins/sendmail/api.py
Normal file
111
src/pretix/plugins/sendmail/api.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#
|
||||
# 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.core.exceptions import ValidationError
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.plugins.sendmail.models import Rule
|
||||
|
||||
|
||||
class RuleSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Rule
|
||||
fields = ['id', 'subject', 'template', 'all_products', 'limit_products', 'include_pending',
|
||||
'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
|
||||
'offset_to_event_end', 'offset_is_after', 'send_to', 'enabled']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('date_is_absolute') is not False:
|
||||
if any([k in data for k in ['offset_to_event_end', 'offset_is_after']]):
|
||||
raise ValidationError('date_is_absolute and offset_* are mutually exclusive')
|
||||
if not full_data.get('send_date'):
|
||||
raise ValidationError('send_date is required for date_is_absolute=True')
|
||||
else:
|
||||
if not all([full_data.get(k) for k in ['send_offset_days', 'send_offset_time']]):
|
||||
raise ValidationError('send_offset_days and send_offset_time are required for date_is_absolute=False')
|
||||
|
||||
if full_data.get('all_products') is False:
|
||||
if not full_data.get('limit_products'):
|
||||
raise ValidationError('limit_products is required when all_products=False')
|
||||
|
||||
return full_data
|
||||
|
||||
def save(self, **kwargs):
|
||||
return super().save(event=self.context['request'].event)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class RuleFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Rule
|
||||
fields = ['id', 'all_products', 'include_pending', 'date_is_absolute',
|
||||
'offset_to_event_end', 'offset_is_after', 'send_to', 'enabled']
|
||||
|
||||
|
||||
class RuleViewSet(viewsets.ModelViewSet):
|
||||
queryset = Rule.objects.none()
|
||||
serializer_class = RuleSerializer
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
filterset_class = RuleFilter
|
||||
ordering = ('id',)
|
||||
ordering_fields = ('id',)
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return Rule.objects.filter(event=self.request.event)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
super().perform_create(serializer)
|
||||
serializer.instance.log_action(
|
||||
'pretix.plugins.sendmail.rule.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.log_action(
|
||||
'pretix.plugins.sendmail.rule.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.plugins.sendmail.rule.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
@@ -34,6 +34,7 @@ from pretix.base.email import get_email_context
|
||||
from pretix.base.models import (
|
||||
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
|
||||
)
|
||||
from pretix.base.models.base import LoggingMixin
|
||||
from pretix.base.services.mail import SendMailException
|
||||
|
||||
|
||||
@@ -44,10 +45,10 @@ class ScheduledMail(models.Model):
|
||||
STATE_MISSED = 'missed'
|
||||
|
||||
STATE_CHOICES = [
|
||||
(STATE_SCHEDULED, STATE_SCHEDULED),
|
||||
(STATE_FAILED, STATE_FAILED),
|
||||
(STATE_COMPLETED, STATE_COMPLETED),
|
||||
(STATE_MISSED, STATE_MISSED),
|
||||
(STATE_SCHEDULED, _('scheduled')),
|
||||
(STATE_FAILED, _('failed')),
|
||||
(STATE_COMPLETED, _('completed')),
|
||||
(STATE_MISSED, _('missed')),
|
||||
]
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
@@ -70,6 +71,9 @@ class ScheduledMail(models.Model):
|
||||
super().save(**kwargs)
|
||||
|
||||
def recompute(self):
|
||||
if self.state in (self.STATE_COMPLETED, self.STATE_MISSED):
|
||||
return
|
||||
|
||||
if self.rule.date_is_absolute:
|
||||
self.computed_datetime = self.rule.send_date
|
||||
else:
|
||||
@@ -103,6 +107,8 @@ class ScheduledMail(models.Model):
|
||||
orders = orders.filter(
|
||||
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), subevent=self.subevent))
|
||||
)
|
||||
elif e.has_subevents:
|
||||
return # This rule should not even exist
|
||||
|
||||
if not self.rule.all_products:
|
||||
orders = orders.filter(
|
||||
@@ -164,7 +170,7 @@ class ScheduledMail(models.Model):
|
||||
self.last_successful_order_id = o.pk
|
||||
|
||||
|
||||
class Rule(models.Model):
|
||||
class Rule(models.Model, LoggingMixin):
|
||||
CUSTOMERS = "orders"
|
||||
ATTENDEES = "attendees"
|
||||
BOTH = "both"
|
||||
|
||||
@@ -63,7 +63,7 @@ def scheduled_mail_create(sender, **kwargs):
|
||||
existing_rules = ScheduledMail.objects.filter(subevent=subevent).values_list('rule_id', flat=True)
|
||||
to_create = []
|
||||
for rule in event.sendmail_rules.all():
|
||||
if rule.pk not in existing_rules:
|
||||
if rule.pk not in existing_rules and subevent:
|
||||
sm = ScheduledMail(rule=rule, event=event, subevent=subevent)
|
||||
sm.recompute()
|
||||
to_create.append(sm)
|
||||
@@ -119,6 +119,8 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
'pretix.plugins.sendmail.sent': _('Email was sent'),
|
||||
'pretix.plugins.sendmail.order.email.sent': _('The order received a mass email.'),
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee': _('A ticket holder of this order received a mass email.'),
|
||||
'pretix.plugins.sendmail.rule.added': _('An email rule was created'),
|
||||
'pretix.plugins.sendmail.rule.changed': _('An email rule was updated'),
|
||||
'pretix.plugins.sendmail.rule.order.email.sent': _('A scheduled email was sent to the order'),
|
||||
'pretix.plugins.sendmail.rule.order.position.email.sent': _('A scheduled email was sent to a ticket holder'),
|
||||
'pretix.plugins.sendmail.rule.deleted': _('An email rule was deleted'),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Inspect Email Rule" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Inspect Email Rule" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This page shows when your rule is planned to be sent.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Email subject" %}</dt>
|
||||
<dd>{{ rule.subject }}</dd>
|
||||
<dt>{% trans "Recipient" %}</dt>
|
||||
<dd>{{ rule.get_send_to_display }}</dd>
|
||||
<dt>{% trans "Scheduled time" %}</dt>
|
||||
<dd>{{ rule.human_readable_time }}</dd>
|
||||
</dl>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Scheduled time" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Last schedule computation" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sm in scheduled_mails %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ sm.computed_datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>
|
||||
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=sm.subevent.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||
{{ sm.subevent.name }}
|
||||
</a><br>
|
||||
{{ sm.get_date_range_display }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<span class="label {% if sm.state == "missed" %}label-warning{% elif sm.state == "failed" %}label-danger{% elif sm.state == "scheduled" %}label-info{% elif sm.state == "completed" %}label-success{% endif %}">
|
||||
{{ sm.get_state_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ sm.last_computed|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -74,6 +74,7 @@
|
||||
{{ r.sent_mails }} / {{ r.total_mails }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a class="btn btn-sm btn-default" href="{% url "plugins:sendmail:rule.schedule" organizer=request.organizer.slug event=request.event.slug rule=r.pk %}" data-toggle="tooltip" title="{% trans "Inspect scheduled times" %}"><i class="fa fa-list"></i></a>
|
||||
<a class="btn btn-sm btn-default" href="{% url "plugins:sendmail:rule.update" organizer=request.organizer.slug event=request.event.slug rule=r.pk %}"><i class="fa fa-edit"></i></a>
|
||||
<a class="btn btn-sm btn-danger" href="{% url "plugins:sendmail:rule.delete" organizer=request.organizer.slug event=request.event.slug rule=r.pk %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
@@ -95,4 +96,4 @@
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,8 +10,21 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
{% bootstrap_field form.enabled layout='control' %}
|
||||
{% if rule.total_mails == rule.sent_mails %}
|
||||
<div class="alert alert-warning">
|
||||
{% if event.has_subevents %}
|
||||
{% trans "This email has already been sent for all existing dates. Changing it will have no effect unless you create additional dates in this event series." %}
|
||||
{% else %}
|
||||
{% trans "This email has already been sent. Changing it will have no effect." %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif rule.total_mails > 0 and event.has_subevents %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "This email has already been sent for some of the dates in your series. Changing it will only have an effect on dates for which the email has not yet been sent." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_field form.enabled layout='control' %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Content" %}</legend>
|
||||
{% bootstrap_field form.subject layout='control' %}
|
||||
@@ -63,4 +76,4 @@
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -34,7 +34,10 @@
|
||||
|
||||
from django.conf.urls import re_path
|
||||
|
||||
from pretix.api.urls import event_router
|
||||
|
||||
from . import views
|
||||
from .api import RuleViewSet
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/$', views.SenderView.as_view(),
|
||||
@@ -43,6 +46,9 @@ urlpatterns = [
|
||||
name='history'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules/create', views.CreateRule.as_view(),
|
||||
name='rule.create'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules/(?P<rule>[^/]+)/schedule',
|
||||
views.ScheduleView.as_view(),
|
||||
name='rule.schedule'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules/(?P<rule>[^/]+)/delete',
|
||||
views.DeleteRule.as_view(),
|
||||
name='rule.delete'),
|
||||
@@ -52,3 +58,4 @@ urlpatterns = [
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules', views.ListRules.as_view(),
|
||||
name='rule.list'),
|
||||
]
|
||||
event_router.register(r'sendmail_rules', RuleViewSet)
|
||||
|
||||
@@ -43,6 +43,7 @@ from django.db.models import Count, Exists, Max, Min, OuterRef, Q
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DeleteView, FormView, ListView
|
||||
@@ -365,7 +366,10 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
|
||||
|
||||
form.instance.event = self.request.event
|
||||
|
||||
self.object = form.save()
|
||||
with transaction.atomic():
|
||||
self.object = form.save()
|
||||
form.instance.log_action('pretix.plugins.sendmail.rule.added', user=self.request.user,
|
||||
data=dict(form.cleaned_data))
|
||||
|
||||
return redirect(
|
||||
'plugins:sendmail:rule.update',
|
||||
@@ -382,7 +386,14 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_object(self, queryset=None) -> Rule:
|
||||
return get_object_or_404(Rule, event=self.request.event, id=self.kwargs['rule'])
|
||||
return get_object_or_404(
|
||||
Rule.objects.annotate(
|
||||
total_mails=Count('scheduledmail'),
|
||||
sent_mails=Count('scheduledmail', filter=Q(scheduledmail__state=ScheduledMail.STATE_COMPLETED)),
|
||||
),
|
||||
event=self.request.event,
|
||||
id=self.kwargs['rule']
|
||||
)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('plugins:sendmail:rule.update', kwargs={
|
||||
@@ -391,8 +402,11 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
|
||||
'rule': self.object.pk,
|
||||
})
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
form.instance.log_action('pretix.plugins.sendmail.rule.changed', user=self.request.user,
|
||||
data=dict(form.cleaned_data))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
@@ -479,3 +493,23 @@ class DeleteRule(EventPermissionRequiredMixin, DeleteView):
|
||||
self.object.delete()
|
||||
messages.success(self.request, _('The selected rule has been deleted.'))
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
|
||||
class ScheduleView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixplugins/sendmail/rule_inspect.html'
|
||||
model = ScheduledMail
|
||||
context_object_name = 'scheduled_mails'
|
||||
|
||||
@cached_property
|
||||
def rule(self):
|
||||
return get_object_or_404(Rule, event=self.request.event, id=self.kwargs['rule'])
|
||||
|
||||
def get_queryset(self):
|
||||
return self.rule.scheduledmail_set.select_related('subevent').order_by(
|
||||
'-computed_datetime'
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['rule'] = self.rule
|
||||
return ctx
|
||||
|
||||
@@ -585,7 +585,7 @@ class StripeMethod(BasePaymentProvider):
|
||||
err = {'message': str(e)}
|
||||
logger.exception('Stripe error: %s' % str(e))
|
||||
|
||||
refund.info = err
|
||||
refund.info_data = err
|
||||
refund.state = OrderRefund.REFUND_STATE_FAILED
|
||||
refund.execution_date = now()
|
||||
refund.save()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="form-horizontal stripe-container">
|
||||
{% if is_moto %}
|
||||
<h1>
|
||||
<span class="label label-info pull-right flip" data-toggle="tooltip" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span>
|
||||
<span class="label label-info pull-right flip" data-toggle="tooltip_html" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span>
|
||||
</h1>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
|
||||
@@ -194,22 +194,31 @@ def webhook(request, *args, **kwargs):
|
||||
if event_json['data']['object']['object'] == "charge":
|
||||
func = charge_webhook
|
||||
objid = event_json['data']['object']['id']
|
||||
lookup_ids = [
|
||||
objid,
|
||||
(event_json['data']['object'].get('source') or {}).get('id')
|
||||
]
|
||||
elif event_json['data']['object']['object'] == "dispute":
|
||||
func = charge_webhook
|
||||
objid = event_json['data']['object']['charge']
|
||||
lookup_ids = [objid]
|
||||
elif event_json['data']['object']['object'] == "source":
|
||||
func = source_webhook
|
||||
objid = event_json['data']['object']['id']
|
||||
lookup_ids = [objid]
|
||||
elif event_json['data']['object']['object'] == "payment_intent":
|
||||
func = paymentintent_webhook
|
||||
objid = event_json['data']['object']['id']
|
||||
lookup_ids = [objid]
|
||||
else:
|
||||
return HttpResponse("Not interested in this data type", status=200)
|
||||
|
||||
try:
|
||||
rso = ReferencedStripeObject.objects.select_related('order', 'order__event').get(reference=objid)
|
||||
rso = ReferencedStripeObject.objects.select_related('order', 'order__event').filter(
|
||||
reference__in=[lid for lid in lookup_ids if lid]
|
||||
).first()
|
||||
if rso:
|
||||
return func(rso.order.event, event_json, objid, rso)
|
||||
except ReferencedStripeObject.DoesNotExist:
|
||||
else:
|
||||
if event_json['data']['object']['object'] == "charge" and 'payment_intent' in event_json['data']['object']:
|
||||
# If we receive a charge webhook *before* the payment intent webhook, we don't know the charge ID yet
|
||||
# and can't match it -- but we know the payment intent ID!
|
||||
@@ -398,7 +407,6 @@ def source_webhook(event, event_json, source_id, rso):
|
||||
prov._charge_source(None, source_id, payment)
|
||||
except PaymentException:
|
||||
logger.exception('Webhook error')
|
||||
|
||||
elif src.status == 'failed':
|
||||
payment.fail(info=str(src))
|
||||
elif src.status == 'canceled' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
|
||||
@@ -475,7 +475,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation',
|
||||
).order_by('pk'):
|
||||
formsetentry = {
|
||||
'cartpos': cartpos,
|
||||
'pos': cartpos,
|
||||
'item': cartpos.item,
|
||||
'variation': cartpos.variation,
|
||||
'categories': []
|
||||
@@ -582,13 +582,13 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
for i in category['items']:
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
val = int(self.request.POST.get(f'cp_{form["cartpos"].pk}_variation_{i.pk}_{v.pk}') or '0')
|
||||
price = self.request.POST.get(f'cp_{form["cartpos"].pk}_variation_{i.pk}_{v.pk}_price') or '0'
|
||||
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}') or '0')
|
||||
price = self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}_price') or '0'
|
||||
if val:
|
||||
selected[i, v] = val, price
|
||||
else:
|
||||
val = int(self.request.POST.get(f'cp_{form["cartpos"].pk}_item_{i.pk}') or '0')
|
||||
price = self.request.POST.get(f'cp_{form["cartpos"].pk}_item_{i.pk}_price') or '0'
|
||||
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}') or '0')
|
||||
price = self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}_price') or '0'
|
||||
if val:
|
||||
selected[i, None] = val, price
|
||||
|
||||
@@ -627,7 +627,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
validate_cart_addons.send(
|
||||
sender=self.event,
|
||||
addons={k: v[0] for k, v in selected.items()},
|
||||
base_position=form["cartpos"],
|
||||
base_position=form["pos"],
|
||||
iao=category['iao']
|
||||
)
|
||||
except CartError as e:
|
||||
@@ -648,7 +648,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
|
||||
for (i, v), (c, price) in selected.items():
|
||||
data.append({
|
||||
'addon_to': f['cartpos'].pk,
|
||||
'addon_to': f['pos'].pk,
|
||||
'item': i.pk,
|
||||
'variation': v.pk if v else None,
|
||||
'count': c,
|
||||
@@ -705,6 +705,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
})
|
||||
if self.cart_customer:
|
||||
initial['email'] = self.cart_customer.email
|
||||
initial['email_repeat'] = self.cart_customer.email
|
||||
|
||||
f = ContactForm(data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
@@ -712,6 +713,8 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
initial=initial, all_optional=self.all_optional)
|
||||
if wd.get('email', '') and wd.get('fix', '') == "true" or self.cart_customer:
|
||||
f.fields['email'].disabled = True
|
||||
if 'email_repeat' in f.fields:
|
||||
f.fields['email_repeat'].disabled = True
|
||||
|
||||
for overrides in override_sets:
|
||||
for fname, val in overrides.items():
|
||||
|
||||
@@ -45,6 +45,7 @@ from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.helpers.i18n import (
|
||||
get_javascript_format_without_seconds, get_moment_locale,
|
||||
)
|
||||
from .cookies import get_cookie_providers
|
||||
|
||||
from ..base.i18n import get_language_without_region
|
||||
from .signals import (
|
||||
@@ -140,6 +141,8 @@ def _default_context(request):
|
||||
ctx['event'] = request.event
|
||||
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales]
|
||||
|
||||
ctx['cookie_providers'] = get_cookie_providers(request.event, request)
|
||||
|
||||
if request.resolver_match:
|
||||
ctx['cart_namespace'] = request.resolver_match.kwargs.get('cart_namespace', '')
|
||||
elif hasattr(request, 'organizer'):
|
||||
|
||||
64
src/pretix/presale/cookies.py
Normal file
64
src/pretix/presale/cookies.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from pretix.presale.signals import register_cookie_providers
|
||||
|
||||
|
||||
class UsageClass(Enum):
|
||||
FUNCTIONAL = 1
|
||||
ANALYTICS = 2
|
||||
MARKETING = 3
|
||||
SOCIAL = 4
|
||||
|
||||
|
||||
class CookieProvider:
|
||||
def __init__(self, identifier: str, usage_classes: List[UsageClass], provider_name: str, privacy_url: str = None, **kwargs):
|
||||
self.identifier = identifier
|
||||
self.usage_classes = usage_classes
|
||||
self.provider_name = provider_name
|
||||
self.privacy_url = privacy_url
|
||||
|
||||
|
||||
def get_cookie_providers(event, request):
|
||||
c = [
|
||||
]
|
||||
for receiver, response in register_cookie_providers.send(event, request=request):
|
||||
if isinstance(response, list):
|
||||
c += response
|
||||
else:
|
||||
c.append(response)
|
||||
c.sort(key=lambda k: str(k.provider_name))
|
||||
return c
|
||||
@@ -57,7 +57,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
pname = str(i)
|
||||
variations = list(i.variations.all())
|
||||
|
||||
if variations:
|
||||
if variations and event.settings.change_allow_user_variation:
|
||||
current_quotas = (
|
||||
instance.variation.quotas.filter(subevent=instance.subevent)
|
||||
if instance.variation
|
||||
@@ -126,6 +126,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
else:
|
||||
choices.append((str(i.pk), '%s' % pname))
|
||||
self.fields['itemvar'].widget.attrs['disabled'] = True
|
||||
self.fields['itemvar'].help_text = _('No other variations of this product exist.')
|
||||
if event.settings.change_allow_user_variation:
|
||||
self.fields['itemvar'].help_text = _('No other variations of this product exist.')
|
||||
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
@@ -52,9 +52,9 @@ class WaitingListForm(forms.ModelForm):
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.event, self.instance.subevent, require_seat=None,
|
||||
memberships=(
|
||||
self.request.customer.usable_memberships(
|
||||
customer.usable_memberships(
|
||||
for_event=self.instance.subevent or self.event,
|
||||
testmode=self.request.event.testmode
|
||||
testmode=self.event.testmode
|
||||
)
|
||||
if customer else None
|
||||
),
|
||||
|
||||
@@ -401,3 +401,13 @@ This signal is sent out when the description of an item or variation is rendered
|
||||
additional text to the description. You are passed the ``item`` and ``variation`` and expected to return
|
||||
HTML.
|
||||
"""
|
||||
|
||||
register_cookie_providers = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
This signal is sent out to get all cookie providers that could set a cookie on this page, regardless of
|
||||
consent state. Receivers should return a list of pretix.presale.cookies.CookieProvider objects.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -169,6 +169,12 @@
|
||||
{% if request.event.settings.contact_mail %}
|
||||
<li><a href="mailto:{{ request.event.settings.contact_mail }}">{% trans "Contact event organizer" %}</a></li>
|
||||
{% endif %}
|
||||
{% if request.event.settings.privacy_url %}
|
||||
<li><a href="{% safelink request.event.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>
|
||||
{% endif %}
|
||||
{% if request.event.settings.cookie_consent and cookie_providers %}
|
||||
<li><a href="#" id="cookie-consent-reopen">{% trans "Cookie settings" %}</a></li>
|
||||
{% endif %}
|
||||
{% if request.event.settings.imprint_url %}
|
||||
<li><a href="{% safelink request.event.settings.imprint_url %}" target="_blank" rel="noopener">{% trans "Imprint" %}</a></li>
|
||||
{% endif %}
|
||||
|
||||
@@ -24,327 +24,19 @@
|
||||
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
|
||||
</h3>
|
||||
</summary>
|
||||
<div id="cp{{ form.cartpos.pk }}">
|
||||
<div id="cp{{ form.pos.pk }}">
|
||||
<div class="panel-body">
|
||||
{% if form.cartpos.subevent %}
|
||||
{% if form.pos.subevent %}
|
||||
<p>
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{{ form.cartpos.subevent.name }} · {{ form.cartpos.subevent.get_date_range_display_as_html }}
|
||||
{% if form.cartpos.event.settings.show_times %}
|
||||
{{ form.pos.subevent.name }} · {{ form.pos.subevent.get_date_range_display_as_html }}
|
||||
{% if form.pos.event.settings.show_times %}
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{{ form.cartpos.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{{ form.pos.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% for c in form.categories %}
|
||||
<fieldset>
|
||||
<legend>{{ c.category.name }}</legend>
|
||||
{% if c.category.description %}
|
||||
{{ c.category.description|rich_text }}
|
||||
{% endif %}
|
||||
{% if c.min_count == c.max_count %}
|
||||
<p>
|
||||
{% blocktrans trimmed count min_count=c.min_count %}
|
||||
You need to choose exactly one option from this category.
|
||||
{% plural %}
|
||||
You need to choose {{ min_count }} options from this category.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %}
|
||||
{% elif c.min_count == 0 %}
|
||||
<p>
|
||||
{% blocktrans trimmed count max_count=c.max_count %}
|
||||
You can choose {{ max_count }} option from this category.
|
||||
{% plural %}
|
||||
You can choose up to {{ max_count }} options from this category.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
|
||||
You can choose between {{ min_count }} and {{ max_count }} options from
|
||||
this category.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% for item in c.items %}
|
||||
{% if item.has_variations %}
|
||||
<article aria-labelledby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="item-{{ item.pk }}">
|
||||
<div class="row-fluid product-row headline">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumb:'60x60^' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="product-description {% if item.picture %}with-picture{% endif %}">
|
||||
<h4 id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
|
||||
{% if item.description %}
|
||||
<div id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description" class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.min_per_order and item.min_per_order > 1 %}
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
<p>
|
||||
{% if c.price_included %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% elif item.free_price %}
|
||||
{% blocktrans trimmed with price=item.min_price|money:event.currency %}
|
||||
from {{ price }}
|
||||
{% endblocktrans %}
|
||||
{% elif item.min_price != item.max_price %}
|
||||
<span class="sr-only">
|
||||
{% blocktrans trimmed with from_price=item.min_price|money:event.currency to_price=item.max_price|money:event.currency %}
|
||||
from {{ from_price }} to {{ to_price }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
<span aria-hidden="true">{{ item.min_price|money:event.currency }} – {{ item.max_price|money:event.currency }}</span>
|
||||
{% elif not item.min_price and not item.max_price %}
|
||||
{% else %}
|
||||
{{ item.min_price|money:event.currency }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 availability-box">
|
||||
{% if not event.settings.show_variations_expanded %}
|
||||
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
|
||||
data-label-alt="{% trans "Hide variants" %}"
|
||||
aria-expanded="false"
|
||||
aria-label="{% blocktrans trimmed with item=item.name count=item.available_variations|length %}Show {{count}} variants of {{item}}{% endblocktrans %}">
|
||||
{% trans "Show variants" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
|
||||
{% for var in item.available_variations %}
|
||||
<article aria-labelledby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<h5 id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
|
||||
{% if var.description %}
|
||||
<div id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
|
||||
{{ var.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.do_show_quota_left %}
|
||||
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if not c.price_included %}
|
||||
{% if var.original_price %}
|
||||
<del><span class="sr-only">{% trans "Original price:" %}</span>
|
||||
{% if event.settings.display_net_prices %}
|
||||
{{ var.original_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ var.original_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
</del>
|
||||
<ins><span class="sr-only">{% trans "New price:" %}</span>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price"
|
||||
placeholder="0"
|
||||
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}_price"
|
||||
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
step="any"
|
||||
value="{% if event.settings.display_net_prices %}{{ var.initial_price.net|money_numberfield:event.currency }}{% else %}{{ var.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
>
|
||||
</div>
|
||||
{% elif not var.display_price.gross %}
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ var.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ var.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price or var.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
{% if item.includes_mixed_tax_rate %}
|
||||
{% if event.settings.display_net_prices %}
|
||||
<small>{% trans "plus taxes" %}</small>
|
||||
{% else %}
|
||||
<small>{% trans "incl. taxes" %}</small>
|
||||
{% endif %}
|
||||
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% elif var.display_price.rate and var.display_price.gross %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if var.cached_availability.0 == 100 or var.initial %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available">
|
||||
{% if c.max_count == 1 or not c.multi_allowed %}
|
||||
<label class="item-checkbox-label">
|
||||
<input type="checkbox" value="1"
|
||||
{% if var.initial %}checked="checked"{% endif %}
|
||||
id="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
name="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
data-exclusive-prefix="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_"
|
||||
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||
{% if var.initial %}value="{{ var.initial }}"{% endif %}
|
||||
max="{{ c.max_count }}"
|
||||
id="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
name="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
aria-label="{% blocktrans with item=item.name var=var %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross avail=var.cached_availability.0 event=event item=item var=var %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<article aria-labelledby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description"{% endif %} class="row-fluid product-row simple">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumb:'60x60^' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="product-description {% if item.picture %}with-picture{% endif %}">
|
||||
<h4 id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
|
||||
{% if item.description %}
|
||||
<div id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description" class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.do_show_quota_left %}
|
||||
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
|
||||
{% endif %}
|
||||
{% if item.min_per_order and item.min_per_order > 1 %}
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
<p>
|
||||
{% if not c.price_included %}
|
||||
{% if item.original_price %}
|
||||
<del><span class="sr-only">{% trans "Original price:" %}</span>
|
||||
{% if event.settings.display_net_prices %}
|
||||
{{ item.original_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ item.original_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
</del>
|
||||
<ins><span class="sr-only">{% trans "New price:" %}</span>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price" placeholder="0"
|
||||
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.cartpos.pk }}_item_{{ item.id }}_price"
|
||||
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
value="{% if event.settings.display_net_prices %}{{ item.initial_price.net|money_numberfield:event.currency }}{% else %}{{ item.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
step="any">
|
||||
</div>
|
||||
{% elif not item.display_price.gross %}
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ item.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ item.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
{% if item.includes_mixed_tax_rate %}
|
||||
{% if event.settings.display_net_prices %}
|
||||
<small>{% trans "plus taxes" %}</small>
|
||||
{% else %}
|
||||
<small>{% trans "incl. taxes" %}</small>
|
||||
{% endif %}
|
||||
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% elif item.display_price.rate and item.display_price.gross %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if item.cached_availability.0 == 100 or item.initial %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available">
|
||||
{% if c.max_count == 1 or not c.multi_allowed %}
|
||||
<label class="item-checkbox-label">
|
||||
<input type="checkbox" value="1"
|
||||
{% if item.initial %}checked="checked"{% endif %}
|
||||
name="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
|
||||
id="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
|
||||
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
|
||||
{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.id }}-description"{% endif %}>
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||
max="{{ c.max_count }}"
|
||||
{% if item.initial %}value="{{ item.initial }}"{% endif %}
|
||||
name="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
|
||||
id="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
|
||||
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
|
||||
{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.id }}-description"{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross avail=item.cached_availability.0 event=event item=item var=0 %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% empty %}
|
||||
<em>
|
||||
{% trans "There are no add-ons available for this product." %}
|
||||
</em>
|
||||
{% endfor %}
|
||||
{% include "pretixpresale/event/fragment_addon_choice.html" with form=form %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% load thumb %}
|
||||
{% load eventsignal %}
|
||||
{% load rich_text %}
|
||||
{% for c in form.categories %}
|
||||
<fieldset>
|
||||
<legend>{{ c.category.name }}</legend>
|
||||
{% if c.category.description %}
|
||||
{{ c.category.description|rich_text }}
|
||||
{% endif %}
|
||||
{% if c.min_count == c.max_count %}
|
||||
<p>
|
||||
{% blocktrans trimmed count min_count=c.min_count %}
|
||||
You need to choose exactly one option from this category.
|
||||
{% plural %}
|
||||
You need to choose {{ min_count }} options from this category.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %}
|
||||
{% elif c.min_count == 0 %}
|
||||
<p>
|
||||
{% blocktrans trimmed count max_count=c.max_count %}
|
||||
You can choose {{ max_count }} option from this category.
|
||||
{% plural %}
|
||||
You can choose up to {{ max_count }} options from this category.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
|
||||
You can choose between {{ min_count }} and {{ max_count }} options from
|
||||
this category.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% for item in c.items %}
|
||||
{% if item.has_variations %}
|
||||
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="item-{{ item.pk }}">
|
||||
<div class="row-fluid product-row headline">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumb:'60x60^' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="product-description {% if item.picture %}with-picture{% endif %}">
|
||||
<h4 id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
|
||||
{% if item.description %}
|
||||
<div id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description" class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.min_per_order and item.min_per_order > 1 %}
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
<p>
|
||||
{% if c.price_included %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% elif item.free_price %}
|
||||
{% blocktrans trimmed with price=item.min_price|money:event.currency %}
|
||||
from {{ price }}
|
||||
{% endblocktrans %}
|
||||
{% elif item.min_price != item.max_price %}
|
||||
<span class="sr-only">
|
||||
{% blocktrans trimmed with from_price=item.min_price|money:event.currency to_price=item.max_price|money:event.currency %}
|
||||
from {{ from_price }} to {{ to_price }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
<span aria-hidden="true">{{ item.min_price|money:event.currency }} – {{ item.max_price|money:event.currency }}</span>
|
||||
{% elif not item.min_price and not item.max_price %}
|
||||
{% else %}
|
||||
{{ item.min_price|money:event.currency }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 availability-box">
|
||||
{% if not event.settings.show_variations_expanded %}
|
||||
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
|
||||
data-label-alt="{% trans "Hide variants" %}"
|
||||
aria-expanded="false"
|
||||
aria-label="{% blocktrans trimmed with item=item.name count=item.available_variations|length %}Show {{count}} variants of {{item}}{% endblocktrans %}">
|
||||
{% trans "Show variants" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
|
||||
{% for var in item.available_variations %}
|
||||
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<h5 id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
|
||||
{% if var.description %}
|
||||
<div id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
|
||||
{{ var.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.do_show_quota_left %}
|
||||
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if not c.price_included %}
|
||||
{% if var.original_price %}
|
||||
<del><span class="sr-only">{% trans "Original price:" %}</span>
|
||||
{% if event.settings.display_net_prices %}
|
||||
{{ var.original_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ var.original_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
</del>
|
||||
<ins><span class="sr-only">{% trans "New price:" %}</span>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price"
|
||||
placeholder="0"
|
||||
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}_price"
|
||||
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
step="any"
|
||||
value="{% if event.settings.display_net_prices %}{{ var.initial_price.net|money_numberfield:event.currency }}{% else %}{{ var.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
>
|
||||
</div>
|
||||
{% elif not var.display_price.gross %}
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ var.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ var.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price or var.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
{% if item.includes_mixed_tax_rate %}
|
||||
{% if event.settings.display_net_prices %}
|
||||
<small>{% trans "plus taxes" %}</small>
|
||||
{% else %}
|
||||
<small>{% trans "incl. taxes" %}</small>
|
||||
{% endif %}
|
||||
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% elif var.display_price.rate and var.display_price.gross %}
|
||||
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if var.cached_availability.0 == 100 or var.initial %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available">
|
||||
{% if c.max_count == 1 or not c.multi_allowed %}
|
||||
<label class="item-checkbox-label">
|
||||
<input type="checkbox" value="1"
|
||||
{% if var.initial %}checked="checked"{% endif %}
|
||||
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
data-exclusive-prefix="cp_{{ form.pos.pk }}_variation_{{ item.id }}_"
|
||||
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||
{% if var.initial %}value="{{ var.initial }}"{% endif %}
|
||||
max="{{ c.max_count }}"
|
||||
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
aria-label="{% blocktrans with item=item.name var=var %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross avail=var.cached_availability.0 event=event item=item var=var %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description"{% endif %} class="row-fluid product-row simple">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumb:'60x60^' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="product-description {% if item.picture %}with-picture{% endif %}">
|
||||
<h4 id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
|
||||
{% if item.description %}
|
||||
<div id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description" class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.do_show_quota_left %}
|
||||
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
|
||||
{% endif %}
|
||||
{% if item.min_per_order and item.min_per_order > 1 %}
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
<p>
|
||||
{% if not c.price_included %}
|
||||
{% if item.original_price %}
|
||||
<del><span class="sr-only">{% trans "Original price:" %}</span>
|
||||
{% if event.settings.display_net_prices %}
|
||||
{{ item.original_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ item.original_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
</del>
|
||||
<ins><span class="sr-only">{% trans "New price:" %}</span>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price" placeholder="0"
|
||||
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.pos.pk }}_item_{{ item.id }}_price"
|
||||
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
value="{% if event.settings.display_net_prices %}{{ item.initial_price.net|money_numberfield:event.currency }}{% else %}{{ item.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
step="any">
|
||||
</div>
|
||||
{% elif not item.display_price.gross %}
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ item.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ item.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
{% if item.includes_mixed_tax_rate %}
|
||||
{% if event.settings.display_net_prices %}
|
||||
<small>{% trans "plus taxes" %}</small>
|
||||
{% else %}
|
||||
<small>{% trans "incl. taxes" %}</small>
|
||||
{% endif %}
|
||||
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% elif item.display_price.rate and item.display_price.gross %}
|
||||
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
|
||||
incl. {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if item.cached_availability.0 == 100 or item.initial %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available">
|
||||
{% if c.max_count == 1 or not c.multi_allowed %}
|
||||
<label class="item-checkbox-label">
|
||||
<input type="checkbox" value="1"
|
||||
{% if item.initial %}checked="checked"{% endif %}
|
||||
name="cp_{{ form.pos.pk }}_item_{{ item.id }}"
|
||||
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
|
||||
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
|
||||
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||
max="{{ c.max_count }}"
|
||||
{% if item.initial %}value="{{ item.initial }}"{% endif %}
|
||||
name="cp_{{ form.pos.pk }}_item_{{ item.id }}"
|
||||
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
|
||||
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
|
||||
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross avail=item.cached_availability.0 event=event item=item var=0 %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% empty %}
|
||||
<em>
|
||||
{% trans "There are no add-ons available for this product." %}
|
||||
</em>
|
||||
{% endfor %}
|
||||
@@ -2,7 +2,9 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block title %}{% trans "Modify order" %}{% endblock %}
|
||||
{% block title %}{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
{% endblocktrans %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
@@ -11,7 +13,7 @@
|
||||
</h2>
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
{% for position, positions in formgroups.items %}
|
||||
{% for position, addon_positions in formgroups.items %}
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
@@ -21,43 +23,48 @@
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body addons">
|
||||
<div class="form-order-change form-horizontal">
|
||||
{% if position.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Date" context "subevent" %}
|
||||
</label>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
{{ pos.subevent.name }} · {{ pos.subevent.get_date_range_display_as_html }}
|
||||
{% if pos.event.settings.show_times %}
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{{ pos.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for p in positions %}
|
||||
{% if p.pk != position.pk %}
|
||||
{# Add-Ons #}
|
||||
<legend>+ {{ p.item.name }}{% if p.variation %} – {{ p.variation.value }}{% endif %}</legend>
|
||||
{% endif %}
|
||||
{% if p.attendee_name %}
|
||||
<div class="form-order-change-main">
|
||||
{% if position.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Attendee name" %}
|
||||
{% trans "Date" context "subevent" %}
|
||||
</label>
|
||||
<div class="col-md-9 form-control-text">
|
||||
{{ p.attendee_name }}
|
||||
<ul class="addon-list">
|
||||
{{ pos.subevent.name }} · {{ pos.subevent.get_date_range_display_as_html }}
|
||||
{% if pos.event.settings.show_times %}
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{{ pos.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form p.form layout="checkout" %}
|
||||
{% endfor %}
|
||||
|
||||
{% for p in addon_positions %}
|
||||
{% if p.pk != position.pk %}
|
||||
{# Add-Ons #}
|
||||
<legend>+ {{ p.item.name }}{% if p.variation %} – {{ p.variation.value }}{% endif %}</legend>
|
||||
{% endif %}
|
||||
{% if p.attendee_name %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Attendee name" %}
|
||||
</label>
|
||||
<div class="col-md-9 form-control-text">
|
||||
{{ p.attendee_name }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form p.form layout="checkout" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if position.addon_form %}
|
||||
{% include "pretixpresale/event/fragment_addon_choice.html" with form=position.addon_form %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -71,7 +78,7 @@
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Save changes" %}
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load classname %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% block title %}{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
{% endblocktrans %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
|
||||
<form method="post" class="form-horizontal" href="">
|
||||
{% csrf_token %}
|
||||
|
||||
<p>{% trans "Please confirm the following changes to your order." %}</p>
|
||||
<div class="row-fluid">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Change summary" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
{% for op in operations %}
|
||||
{% if op|classname == "ItemOperation" %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if op.position.variation or op.variation %}
|
||||
{% blocktrans trimmed with positionid=op.position.positionid old_item=op.position.item.name old_variation=op.position.variation new_item=op.item.name new_variation=op.variation %}
|
||||
Change position #{{ positionid }} from "{{ old_item }} – {{ old_variation }}
|
||||
" to "{{ new_item }} – {{ new_variation }}"
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with positionid=op.position.positionid old_item=op.position.item.name new_item=op.item.name %}
|
||||
Change position #{{ positionid }} from "{{ old_item }}" to "{{ new_item }}"
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% if op.position.addon_to %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<small>{% blocktrans with positionid=op.position.addon_to.positionid %}
|
||||
Add-on product to position #{{ positionid }}{% endblocktrans %}</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
</td>
|
||||
</tr>
|
||||
{% elif op|classname == "SubeventOperation" %}
|
||||
<tr>
|
||||
<td>
|
||||
{% blocktrans trimmed with positionid=op.position.positionid old=op.position.subevent new=op.subevent %}
|
||||
Change date of position #{{ positionid }} from "{{ old }}" to "{{ new }}"
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
</td>
|
||||
</tr>
|
||||
{% elif op|classname == "PriceOperation" %}
|
||||
<tr>
|
||||
<td>
|
||||
{% blocktrans trimmed with positionid=op.position.positionid old=op.position.price new=op.price %}
|
||||
Change price of position #{{ positionid }} from {{ old }} to {{ new }}
|
||||
{% endblocktrans %}
|
||||
{% if op.position.addon_to %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<small>{% blocktrans with positionid=op.position.addon_to.positionid %}
|
||||
Add-on product to position #{{ positionid }}{% endblocktrans %}</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ op.price_diff|money:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif op|classname == "AddOperation" %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if op.variation %}
|
||||
{% blocktrans trimmed with positionid=op.position.positionid item=op.item.name variation=op.variation.value %}
|
||||
Add position #{{ positionid }} ({{ item }} – {{ variation }})
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with positionid=op.position.positionid item=op.item.name %}
|
||||
Add position #{{ positionid }} ({{ item }})
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% if op.addon_to %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<small>{% blocktrans with positionid=op.addon_to.positionid %}Add-on product
|
||||
to position #{{ positionid }}{% endblocktrans %}</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ op.price.gross|money:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif op|classname == "CancelOperation" %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if op.position.variation %}
|
||||
{% blocktrans trimmed with positionid=op.position.positionid item=op.position.item.name variation=op.position.variation.value %}
|
||||
Remove position #{{ positionid }} ({{ item }} – {{ variation }})
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with positionid=op.position.positionid item=op.position.item.name %}
|
||||
Remove position #{{ positionid }} ({{ item }})
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% if op.position.addon_to %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<small>{% blocktrans with positionid=op.position.addon_to.positionid %}
|
||||
Add-on product to position #{{ positionid }}{% endblocktrans %}</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ op.price_diff|money:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>{% trans "Total price change" %}</strong></td>
|
||||
<td class="text-right flip">
|
||||
<strong>
|
||||
{{ totaldiff|money:request.event.currency }}
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{% if totaldiff %}
|
||||
<tr>
|
||||
<td><strong>{% trans "New order total" %}</strong></td>
|
||||
<td class="text-right flip">
|
||||
{{ totaldiff|add:order.total|money:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% trans "You already paid" %}</strong></td>
|
||||
<td class="text-right flip">
|
||||
{{ order.payment_refund_sum|money:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{% if new_pending_sum > 0 %}
|
||||
<strong>{% trans "You will need to pay" %}</strong>
|
||||
<br>
|
||||
<span class="text-muted">
|
||||
{% trans "Your entire order will be considered unpaid until you paid this difference." %}
|
||||
</span>
|
||||
{% else %}
|
||||
<strong>{% trans "You will be refunded" %}</strong>
|
||||
<br>
|
||||
<span class="text-muted">
|
||||
{% if request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "manually" %}
|
||||
{% trans "The organizer will get in touch with you to clarify the details of your refund." %}
|
||||
{% elif request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "force" %}
|
||||
{% trans "The refund will be issued in form of a gift card that you can use for further purchases." %}
|
||||
{% else %}
|
||||
{% if can_auto_refund %}
|
||||
{% blocktrans trimmed %}
|
||||
The refund amount will automatically be sent back to your original payment method. Depending
|
||||
on the payment method, please allow for up to two weeks before this appears on your
|
||||
statement.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
With the payment method you used, the refund amount <strong>can not be sent back to you
|
||||
automatically</strong>. Instead, the event organizer will need to initiate the transfer
|
||||
manually. Please be patient as this might take a bit longer.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<strong>
|
||||
{{ new_pending_sum|money:request.event.currency }}
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% for k, l in request.POST.lists %}
|
||||
{% for v in l %}
|
||||
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% eventurl request.event "presale:event.order.change" secret=order.secret order=order.code %}">
|
||||
{% trans "Back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit" name="confirm" value="true">
|
||||
{% trans "Perform changes" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,123 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
<div class="day-calendar cal-size-{{ raster_to_shortest_ratio }}{% if no_headlines %} no-headlines{% endif %}"
|
||||
data-raster-size="{{ raster_size }}"
|
||||
data-duration="{{ calendar_duration }}"
|
||||
data-start="{{ start|date:"c" }}"
|
||||
data-timezone="{{ cal_tz }}">
|
||||
<h3 aria-hidden="true" class="day-row-name"><span hidden>{% trans "Time of day" %}</span></h3>
|
||||
<ul aria-hidden="true" class="day-timeline ticks">
|
||||
{% for t in time_ticks %}
|
||||
<li data-offset="{{ t.offset|date:"H:i" }}"
|
||||
data-duration="{{ t.duration|date:"H:i" }}"
|
||||
data-start="{{ t.start|date:"H:i" }}"
|
||||
class="text-muted">{{ t.start|date:"TIME_FORMAT" }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for series, collection in collections %}
|
||||
<h3 class="day-row-name">
|
||||
{% if series %}
|
||||
<a href="{% eventurl series "presale:event.index" %}">
|
||||
{{ series.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "Single events" context "day calendar" %}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<ul class="day-timeline" data-concurrency="{{ collection.concurrency }}">
|
||||
{% for t in time_ticks %}
|
||||
{% if not forloop.counter|divisibleby:2 %}
|
||||
<li data-offset="{{ t.offset|date:"H:i" }}"
|
||||
data-duration="{{ t.duration|date:"H:i" }}" class="tick"> </li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for event in collection.events %}
|
||||
<li data-offset="{{ event.offset_rastered|date:"H:i" }}"
|
||||
data-offset-shift="{{ event.offset_shift_start }}:{{ event.offset_shift_end }}"
|
||||
data-duration="{{ event.duration_rastered }}"
|
||||
data-concurrency="{{ event.concurrency }}">
|
||||
<a class="event {% if event.continued %}continued{% else %} {% spaceless %}
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% if event.event.best_availability_state == 100 %}
|
||||
available
|
||||
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
|
||||
waitinglist
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
reserved
|
||||
{% elif event.event.best_availability_state < 20 %}
|
||||
soldout
|
||||
{% endif %}
|
||||
{% elif event.event.presale_is_running %}
|
||||
running
|
||||
{% elif event.event.presale_has_ended %}
|
||||
over
|
||||
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
|
||||
soon
|
||||
{% else %}
|
||||
soon
|
||||
{% endif %}
|
||||
{% endspaceless %}{% endif %}"
|
||||
href="{{ event.url }}">
|
||||
{% if show_names|default_if_none:True %}
|
||||
<span class="event-name">
|
||||
{{ event.event.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not event.continued %}
|
||||
{% if event.time %}
|
||||
<span class="event-time" data-time="{{ event.event.date_from.isoformat }}"
|
||||
data-timezone="{{ event.timezone }}" data-time-short>
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{% if not show_names|default_if_none:True %}
|
||||
<strong>
|
||||
{% endif %}
|
||||
<time datetime="{{ event.time|date:"H:i" }}">{{ event.time|date:"TIME_FORMAT" }}</time>
|
||||
{% if event.time_end %}
|
||||
<span role="img" aria-label="{% trans "to" context "timerange" %}">–</span>
|
||||
<time datetime="{{ event.time_end|date:"H:i" }}">{{ event.time_end|date:"TIME_FORMAT" }}</time>
|
||||
{% endif %}
|
||||
{% if not show_names|default_if_none:True %}
|
||||
</strong>
|
||||
{% endif %}
|
||||
{% if multiple_timezones %}
|
||||
{{ event.timezone }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="event-status">
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% if event.event.best_availability_state == 100 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
|
||||
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Reserved" %}
|
||||
{% elif event.event.best_availability_state < 20 %}
|
||||
{% if event.event.has_paid_item %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sold out" %}
|
||||
{% else %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span>
|
||||
{% trans "Fully booked" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif event.event.presale_is_running %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
|
||||
{% elif event.event.presale_has_ended %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sale over" %}
|
||||
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span>
|
||||
{% blocktrans with start_date=event.event.presale_start|date:"SHORT_DATE_FORMAT" %}
|
||||
from {{ start_date }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Soon" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -14,6 +14,7 @@
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/widget/floatformat.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cookieconsent.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% load safelink %}
|
||||
<div id="ajaxerr">
|
||||
</div>
|
||||
<div id="loadingmodal" hidden aria-live="polite">
|
||||
@@ -15,3 +17,90 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if request.organizer and request.organizer.settings.cookie_consent and cookie_providers %}
|
||||
<script type="text/plain" id="cookie-consent-storage-key">cookie-consent-{{ request.organizer.slug }}</script>
|
||||
<div id="cookie-consent-modal" hidden aria-live="polite">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-content">
|
||||
<h3 id="cookie-consent-modal-label"></h3>
|
||||
<div id="cookie-consent-modal-description">
|
||||
{% with request.event|default:request.organizer as sh %}
|
||||
<h3>{{ sh.settings.cookie_consent_dialog_title }}</h3>
|
||||
{{ sh.settings.cookie_consent_dialog_text|rich_text }}
|
||||
{% if sh.settings.cookie_consent_dialog_text_secondary %}
|
||||
<div class="text-muted">
|
||||
{{ sh.settings.cookie_consent_dialog_text_secondary|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<details id="cookie-consent-details">
|
||||
<summary>
|
||||
<span class="fa fa-fw chevron"></span>
|
||||
{% trans "Adjust settings in detail" %}
|
||||
</summary>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" disabled checked="">
|
||||
{% trans "Required cookies" %}<br>
|
||||
<span class="text-muted">
|
||||
{% trans "Functional cookies (e.g. shopping cart, login, payment, language preference) and technical cookies (e.g. security purposes)" %}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{% for cp in cookie_providers %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ cp.identifier }}">
|
||||
{{ cp.provider_name }}<br>
|
||||
<span class="text-muted">
|
||||
{% for c in cp.usage_classes %}
|
||||
{% if forloop.counter0 > 0 %}· {% endif %}
|
||||
{% if c.value == 1 %}
|
||||
{% trans "Functionality" context "cookie_usage" %}
|
||||
{% elif c.value == 2 %}
|
||||
{% trans "Analytics" context "cookie_usage" %}
|
||||
{% elif c.value == 3 %}
|
||||
{% trans "Marketing" context "cookie_usage" %}
|
||||
{% elif c.value == 4 %}
|
||||
{% trans "Social features" context "cookie_usage" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if cp.privacy_url %}
|
||||
·
|
||||
<a href="{% safelink cp.privacy_url %}" target="_blank">
|
||||
{% trans "Privacy policy" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<p>
|
||||
<button type="button" class="btn btn-lg btn-block btn-primary" id="cookie-consent-button-no"
|
||||
data-summary-text="{{ sh.settings.cookie_consent_dialog_button_no }}"
|
||||
data-detail-text="{% trans "Save selection" %}">
|
||||
{{ sh.settings.cookie_consent_dialog_button_no }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<p>
|
||||
<button type="button" class="btn btn-lg btn-block btn-primary" id="cookie-consent-button-yes">
|
||||
{{ sh.settings.cookie_consent_dialog_button_yes }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if sh.settings.privacy_url %}
|
||||
<p class="text-center">
|
||||
<a href="{% safelink sh.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
{% if not request.event and request.organizer.settings.contact_mail %}
|
||||
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}">{% trans "Contact event organizer" %}</a></li>
|
||||
{% endif %}
|
||||
{% if not request.event and request.organizer.settings.privacy_url %}
|
||||
<li><a href="{% safelink request.organizer.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>
|
||||
{% endif %}
|
||||
{% if not request.event and request.organizer.settings.cookie_consent and cookie_providers %}
|
||||
<li><a href="#" id="cookie-consent-reopen">{% trans "Cookie settings" %}</a></li>
|
||||
{% endif %}
|
||||
{% if not request.event and request.organizer.settings.imprint_url %}
|
||||
<li><a href="{% safelink request.organizer.settings.imprint_url %}" target="_blank" rel="noopener">{% trans "Imprint" %}</a></li>
|
||||
{% endif %}
|
||||
|
||||
@@ -20,21 +20,26 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12 text-left flip">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" %}" type="button" class="btn btn-default">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" "" %}" type="button" class="btn btn-default">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "old" "" "month" "" "year" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "week" "old" "" "month" "" "year" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "old" "" "week" "" %}"
|
||||
<a href="?{% url_replace request "style" "calendar" "old" "" "week" "" "date" "" %}"
|
||||
type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" %}"
|
||||
class="btn btn-default">
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Event overview" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% if organizer_homepage_text %}
|
||||
<div>
|
||||
{{ organizer_homepage_text | rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{{ date|date:"DATE_FORMAT" }}</h3>
|
||||
<form class="form-inline" method="get" id="monthselform"
|
||||
action="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-sm-4 hidden-xs text-left flip">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "year" "" "date" "" %}"
|
||||
type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" "date" "" %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar-plus-o" aria-hidden="true"></span>
|
||||
{% trans "iCal" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-12 text-center">
|
||||
<input class="datepickerfield form-control" value="{{ date|date:"SHORT_DATE_FORMAT" }}" name="date">
|
||||
<button type="submit" class="js-hidden btn btn-default">
|
||||
{% trans "Go" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-4 hidden-xs text-right flip">
|
||||
{% if has_before %}
|
||||
<a href="?{% url_replace request "date" before.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-arrow-left" aria-hidden="true"></span>
|
||||
{{ before|date:"SHORT_DATE_FORMAT" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if has_after %}
|
||||
<a href="?{% url_replace request "date" after.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
{{ after|date:"SHORT_DATE_FORMAT" }}
|
||||
<span class="fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixpresale/fragment_day_calendar.html" with show_avail=request.organizer.settings.event_list_availability %}
|
||||
<div class="col-sm-4 visible-xs text-center">
|
||||
{% if has_before %}
|
||||
<a href="?{% url_replace request "date" before.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-arrow-left" aria-hidden="true"></span>
|
||||
{{ before|date:"SHORT_DATE_FORMAT" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if has_after %}
|
||||
<a href="?{% url_replace request "date" after.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
{{ after|date:"SHORT_DATE_FORMAT" }}
|
||||
<span class="fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if multiple_timezones %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Note that the events in this view are in different timezones.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -21,22 +21,27 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12 text-left flip">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "date" "" %}" type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "year" "" %}"
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "year" "" "date" "" %}"
|
||||
type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" %}"
|
||||
class="btn btn-default">
|
||||
|
||||
@@ -28,21 +28,26 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" ""%}" type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "page" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "page" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "page" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "page" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" %}"
|
||||
class="btn btn-default">
|
||||
|
||||
@@ -661,5 +661,5 @@ class AnswerDownload(EventViewMixin, View):
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
)
|
||||
).encode("ascii", "ignore")
|
||||
return resp
|
||||
|
||||
@@ -569,9 +569,14 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
voucher,
|
||||
)
|
||||
|
||||
context['show_names'] = ebd.get('_subevents_different_names', False) or sum(
|
||||
len(i) for i in ebd.values() if isinstance(i, list)
|
||||
) < 2
|
||||
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
|
||||
# in the calendar. We previously only looked at the current time range for this condition which caused weird side-effects, so we need
|
||||
# an extra query to look at the entire series. For performance reasons, we have a limit on how many different names we look at.
|
||||
context['show_names'] = sum(len(i) for i in ebd.values() if isinstance(i, list)) < 2 or self.request.event.cache.get_or_set(
|
||||
'has_different_subevent_names',
|
||||
lambda: len(set(str(n) for n in self.request.event.subevents.values_list('name', flat=True).annotate(c=Count('*'))[:250])) != 1,
|
||||
timeout=120,
|
||||
)
|
||||
context['weeks'] = weeks_for_template(ebd, self.year, self.month)
|
||||
context['months'] = [date(self.year, i + 1, 1) for i in range(12)]
|
||||
context['years'] = range(now().year - 2, now().year + 3)
|
||||
@@ -598,9 +603,14 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
voucher,
|
||||
)
|
||||
|
||||
context['show_names'] = ebd.get('_subevents_different_names', False) or sum(
|
||||
len(i) for i in ebd.values() if isinstance(i, list)
|
||||
) < 2
|
||||
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
|
||||
# in the calendar. We previously only looked at the current time range for this condition which caused weird side-effects, so we need
|
||||
# an extra query to look at the entire series. For performance reasons, we have a limit on how many different names we look at.
|
||||
context['show_names'] = sum(len(i) for i in ebd.values() if isinstance(i, list)) < 2 or self.request.event.cache.get_or_set(
|
||||
'has_different_subevent_names',
|
||||
lambda: len(set(str(n) for n in self.request.event.subevents.values_list('name', flat=True).annotate(c=Count('*'))[:250])) != 1,
|
||||
timeout=120,
|
||||
)
|
||||
context['days'] = days_for_template(ebd, week)
|
||||
context['weeks'] = [
|
||||
(date_fromisocalendar(self.year, i + 1, 1), date_fromisocalendar(self.year, i + 1, 7))
|
||||
|
||||
@@ -38,19 +38,20 @@ import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import Exists, OuterRef, Q, Sum
|
||||
from django.http import (
|
||||
FileResponse, Http404, HttpResponseRedirect, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -65,6 +66,7 @@ from pretix.base.models.orders import (
|
||||
CachedCombinedTicket, InvoiceAddress, OrderFee, OrderPayment, OrderRefund,
|
||||
QuestionAnswer,
|
||||
)
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
|
||||
@@ -72,7 +74,8 @@ from pretix.base.services.invoices import (
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, cancel_order, change_payment_provider,
|
||||
OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
|
||||
change_payment_provider, error_messages,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate, invalidate_cache
|
||||
@@ -90,6 +93,7 @@ from pretix.presale.signals import question_form_fields_overrides
|
||||
from pretix.presale.views import (
|
||||
CartMixin, EventViewMixin, iframe_entry_view_wrapper,
|
||||
)
|
||||
from pretix.presale.views.event import get_grouped_items
|
||||
from pretix.presale.views.robots import NoSearchIndexViewMixin
|
||||
|
||||
|
||||
@@ -577,16 +581,22 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
|
||||
@transaction.atomic()
|
||||
def mark_paid_free(self):
|
||||
p = self.order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='manual',
|
||||
amount=Decimal('0.00'),
|
||||
fee=None
|
||||
)
|
||||
try:
|
||||
p.confirm()
|
||||
except SendMailException:
|
||||
pass
|
||||
p = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED).last()
|
||||
if not p:
|
||||
p = self.order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='free',
|
||||
amount=Decimal('0.00'),
|
||||
fee=None
|
||||
)
|
||||
try:
|
||||
p.confirm()
|
||||
except SendMailException:
|
||||
pass
|
||||
else:
|
||||
p._mark_order_paid(
|
||||
payment_refund_sum=self.order.payment_refund_sum
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.order.pending_sum <= Decimal('0.00'):
|
||||
@@ -1159,6 +1169,8 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
def formdict(self):
|
||||
storage = OrderedDict()
|
||||
for pos in self.positions:
|
||||
if self.request.event.settings.change_allow_user_addons and pos.addon_to_id:
|
||||
continue
|
||||
if pos.addon_to_id:
|
||||
if pos.addon_to not in storage:
|
||||
storage[pos.addon_to] = []
|
||||
@@ -1180,10 +1192,11 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
def positions(self):
|
||||
positions = list(
|
||||
self.order.positions.select_related('item', 'item__tax_rule').prefetch_related(
|
||||
'item__variations',
|
||||
'item__variations', 'addons',
|
||||
)
|
||||
)
|
||||
quota_cache = {}
|
||||
item_cache = {}
|
||||
try:
|
||||
ia = self.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
@@ -1192,6 +1205,87 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p,
|
||||
invoice_address=ia, event=self.request.event, quota_cache=quota_cache,
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
|
||||
if p.addon_to_id is None and self.request.event.settings.change_allow_user_addons:
|
||||
p.addon_form = {
|
||||
'pos': p,
|
||||
'categories': []
|
||||
}
|
||||
current_addon_products = defaultdict(list)
|
||||
for a in p.addons.all():
|
||||
if a.canceled:
|
||||
continue
|
||||
if not a.is_bundled:
|
||||
current_addon_products[a.item_id, a.variation_id].append(a)
|
||||
|
||||
for iao in p.item.addons.all():
|
||||
ckey = '{}-{}'.format(p.subevent.pk if p.subevent else 0, iao.addon_category.pk)
|
||||
|
||||
if ckey not in item_cache:
|
||||
# Get all items to possibly show
|
||||
items, _btn = get_grouped_items(
|
||||
self.request.event,
|
||||
subevent=p.subevent,
|
||||
voucher=None,
|
||||
channel=self.order.sales_channel,
|
||||
base_qs=iao.addon_category.items,
|
||||
allow_addons=True,
|
||||
quota_cache=quota_cache,
|
||||
memberships=(
|
||||
self.request.customer.usable_memberships(
|
||||
for_event=p.subevent or self.request.event,
|
||||
testmode=self.order.testmode
|
||||
)
|
||||
if self.order.customer else None
|
||||
),
|
||||
)
|
||||
item_cache[ckey] = items
|
||||
else:
|
||||
items = item_cache[ckey]
|
||||
|
||||
for i in items:
|
||||
i.allow_waitinglist = False
|
||||
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
v.initial = len(current_addon_products[i.pk, v.pk])
|
||||
if v.initial and i.free_price:
|
||||
a = current_addon_products[i.pk, v.pk][0]
|
||||
v.initial_price = TaxedPrice(
|
||||
net=a.price - a.tax_value,
|
||||
gross=a.price,
|
||||
tax=a.tax_value,
|
||||
name=a.item.tax_rule.name if a.item.tax_rule else "",
|
||||
rate=a.tax_rate,
|
||||
)
|
||||
else:
|
||||
v.initial_price = v.display_price
|
||||
i.expand = any(v.initial for v in i.available_variations)
|
||||
else:
|
||||
i.initial = len(current_addon_products[i.pk, None])
|
||||
if i.initial and i.free_price:
|
||||
a = current_addon_products[i.pk, None][0]
|
||||
i.initial_price = TaxedPrice(
|
||||
net=a.price - a.tax_value,
|
||||
gross=a.price,
|
||||
tax=a.tax_value,
|
||||
name=a.item.tax_rule.name if a.item.tax_rule else "",
|
||||
rate=a.tax_rate,
|
||||
)
|
||||
else:
|
||||
i.initial_price = i.display_price
|
||||
|
||||
if items:
|
||||
p.addon_form['categories'].append({
|
||||
'category': iao.addon_category,
|
||||
'price_included': iao.price_included,
|
||||
'multi_allowed': iao.multi_allowed,
|
||||
'min_count': iao.min_count,
|
||||
'max_count': iao.max_count,
|
||||
'iao': iao,
|
||||
'items': [i for i in items if not i.require_voucher]
|
||||
})
|
||||
|
||||
return positions
|
||||
|
||||
def _process_change(self, ocm):
|
||||
@@ -1235,7 +1329,56 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
return False
|
||||
return True
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
def _clean_category(self, form, category):
|
||||
selected = {}
|
||||
for i in category['items']:
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}') or '0')
|
||||
price = self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}_price') or '0'
|
||||
if val:
|
||||
selected[i, v] = val, price
|
||||
else:
|
||||
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}') or '0')
|
||||
price = self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}_price') or '0'
|
||||
if val:
|
||||
selected[i, None] = val, price
|
||||
|
||||
if sum(a[0] for a in selected.values()) > category['max_count']:
|
||||
# TODO: Proper pluralization
|
||||
raise ValidationError(
|
||||
_(error_messages['addon_max_count']),
|
||||
'addon_max_count',
|
||||
{
|
||||
'base': str(form['pos'].item.name),
|
||||
'max': category['max_count'],
|
||||
'cat': str(category['category'].name),
|
||||
}
|
||||
)
|
||||
elif sum(a[0] for a in selected.values()) < category['min_count']:
|
||||
# TODO: Proper pluralization
|
||||
raise ValidationError(
|
||||
_(error_messages['addon_min_count']),
|
||||
'addon_min_count',
|
||||
{
|
||||
'base': str(form['pos'].item.name),
|
||||
'min': category['min_count'],
|
||||
'cat': str(category['category'].name),
|
||||
}
|
||||
)
|
||||
elif any(sum(v[0] for k, v in selected.items() if k[0] == i) > 1 for i in category['items']) and not category['multi_allowed']:
|
||||
raise ValidationError(
|
||||
_(error_messages['addon_no_multi']),
|
||||
'addon_no_multi',
|
||||
{
|
||||
'base': str(form['pos'].item.name),
|
||||
'cat': str(category['category'].name),
|
||||
}
|
||||
)
|
||||
|
||||
return selected
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
was_paid = self.order.status == Order.STATUS_PAID
|
||||
ocm = OrderChangeManager(
|
||||
self.order,
|
||||
@@ -1243,28 +1386,106 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
notify=True,
|
||||
reissue_invoice=True,
|
||||
)
|
||||
form_valid = self._process_change(ocm)
|
||||
|
||||
addons_data = []
|
||||
for p in self.positions:
|
||||
if p.addon_to_id or not hasattr(p, 'addon_form'):
|
||||
continue
|
||||
for c in p.addon_form['categories']:
|
||||
try:
|
||||
selected = self._clean_category(p.addon_form, c)
|
||||
except ValidationError as e:
|
||||
messages.error(request, e.message % e.params if e.params else e.message)
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
for (i, v), (c, price) in selected.items():
|
||||
addons_data.append({
|
||||
'addon_to': p.pk,
|
||||
'item': i.pk,
|
||||
'variation': v.pk if v else None,
|
||||
'count': c,
|
||||
'price': price,
|
||||
})
|
||||
try:
|
||||
ocm.set_addons(addons_data)
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
form_valid = False
|
||||
else:
|
||||
form_valid = self._process_change(ocm)
|
||||
|
||||
if not form_valid:
|
||||
messages.error(self.request, _('An error occurred. Please see the details below.'))
|
||||
else:
|
||||
try:
|
||||
ocm.commit(check_quotas=True)
|
||||
self._validate_total_diff(ocm)
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
|
||||
if self.order.status != Order.STATUS_PAID and was_paid:
|
||||
messages.success(self.request, _('The order has been changed. You can now proceed by paying the open amount of {amount}.').format(
|
||||
amount=money_filter(self.order.pending_sum, self.request.event.currency)
|
||||
))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.order.pay.change', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}))
|
||||
if "confirm" in request.POST:
|
||||
try:
|
||||
ocm.commit(check_quotas=True)
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been changed.'))
|
||||
if self.order.pending_sum < Decimal('0.00'):
|
||||
auto_refund = (
|
||||
not self.request.event.settings.cancel_allow_user_paid_require_approval
|
||||
and self.request.event.settings.cancel_allow_user_paid_refund_as_giftcard != "manually"
|
||||
)
|
||||
refund_as_giftcard = self.request.event.settings.cancel_allow_user_paid_refund_as_giftcard == 'force'
|
||||
if auto_refund:
|
||||
try:
|
||||
_try_auto_refund(self.order, refund_as_giftcard=refund_as_giftcard)
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
|
||||
if self.order.status != Order.STATUS_PAID and was_paid:
|
||||
messages.success(self.request, _('The order has been changed. You can now proceed by paying the open amount of {amount}.').format(
|
||||
amount=money_filter(self.order.pending_sum, self.request.event.currency)
|
||||
))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.order.pay.change', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been changed.'))
|
||||
|
||||
return redirect(self.get_order_url())
|
||||
elif not ocm._operations:
|
||||
messages.info(self.request, _('You did not make any changes.'))
|
||||
return redirect(self.get_order_url())
|
||||
else:
|
||||
new_pending_sum = self.order.pending_sum + ocm._totaldiff
|
||||
can_auto_refund = False
|
||||
if new_pending_sum < Decimal('0.00'):
|
||||
proposals = self.order.propose_auto_refunds(Decimal('-1.00') * new_pending_sum)
|
||||
can_auto_refund = sum(proposals.values()) == Decimal('-1.00') * new_pending_sum
|
||||
|
||||
return self.get(*args, **kwargs)
|
||||
return render(request, 'pretixpresale/event/order_change_confirm.html', {
|
||||
'operations': ocm._operations,
|
||||
'totaldiff': ocm._totaldiff,
|
||||
'order': self.order,
|
||||
'payment_refund_sum': self.order.payment_refund_sum,
|
||||
'new_pending_sum': new_pending_sum,
|
||||
'can_auto_refund': can_auto_refund,
|
||||
})
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def _validate_total_diff(self, ocm):
|
||||
if ocm._totaldiff < Decimal('0.00') and self.request.event.settings.change_allow_user_price == 'gte':
|
||||
raise OrderError(_('You may not change your order in a way that reduces the total price.'))
|
||||
if ocm._totaldiff <= Decimal('0.00') and self.request.event.settings.change_allow_user_price == 'gt':
|
||||
raise OrderError(_('You may only change your order in a way that increases the total price.'))
|
||||
if ocm._totaldiff != Decimal('0.00') and self.request.event.settings.change_allow_user_price == 'eq':
|
||||
raise OrderError(_('You may not change your order in a way that changes the total price.'))
|
||||
|
||||
if ocm._totaldiff > Decimal('0.00') and self.order.status == Order.STATUS_PAID:
|
||||
self.order.set_expires(
|
||||
now(),
|
||||
self.order.event.subevents.filter(id__in=self.order.positions.values_list('subevent_id', flat=True))
|
||||
)
|
||||
if self.order.expires < now():
|
||||
raise OrderError(_('You may not change your order in a way that increases the total price since '
|
||||
'payments are no longer being accepted for this event.'))
|
||||
|
||||
@@ -31,13 +31,15 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import calendar
|
||||
import hashlib
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from functools import reduce
|
||||
from urllib.parse import quote
|
||||
|
||||
import dateutil
|
||||
import isoweek
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
@@ -376,6 +378,10 @@ class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView):
|
||||
cv = CalendarView()
|
||||
cv.request = request
|
||||
return cv.get(request, *args, **kwargs)
|
||||
elif style == "day":
|
||||
cv = DayCalendarView()
|
||||
cv.request = request
|
||||
return cv.get(request, *args, **kwargs)
|
||||
elif style == "week":
|
||||
cv = WeekCalendarView()
|
||||
cv.request = request
|
||||
@@ -441,6 +447,11 @@ def add_events_for_days(request, baseqs, before, after, ebd, timezones):
|
||||
)) and event.settings.show_times
|
||||
else None
|
||||
),
|
||||
'time_end_today': (
|
||||
datetime_to.time().replace(tzinfo=None)
|
||||
if date_to == d and event.settings.show_times
|
||||
else None
|
||||
),
|
||||
'url': eventreverse(event, 'presale:event.index'),
|
||||
'timezone': event.settings.timezone,
|
||||
})
|
||||
@@ -470,7 +481,6 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
|
||||
if se.presale_is_running:
|
||||
quotas_to_compute += se.active_quotas
|
||||
|
||||
name = None
|
||||
qcache = {}
|
||||
if quotas_to_compute:
|
||||
qa = QuotaAvailability()
|
||||
@@ -500,10 +510,6 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
|
||||
tz = pytz.timezone(s.timezone)
|
||||
datetime_from = se.date_from.astimezone(tz)
|
||||
date_from = datetime_from.date()
|
||||
if name is None:
|
||||
name = str(se.name)
|
||||
elif str(se.name) != name:
|
||||
ebd['_subevents_different_names'] = True
|
||||
if s.show_date_to and se.date_to:
|
||||
datetime_to = se.date_to.astimezone(tz)
|
||||
date_to = se.date_to.astimezone(tz).date()
|
||||
@@ -521,6 +527,11 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
|
||||
)) and s.show_times
|
||||
else None
|
||||
),
|
||||
'time_end_today': (
|
||||
datetime_to.time().replace(tzinfo=None)
|
||||
if date_to == d and s.show_times
|
||||
else None
|
||||
),
|
||||
'event': se,
|
||||
'url': (
|
||||
eventreverse(se.event, 'presale:event.redeem',
|
||||
@@ -719,6 +730,335 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
|
||||
return ebd
|
||||
|
||||
|
||||
class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
|
||||
template_name = 'pretixpresale/organizers/calendar_day.html'
|
||||
|
||||
def _set_date_to_next_event(self):
|
||||
next_ev = filter_qs_by_attr(Event.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
Q(date_from__gte=now()) | Q(date_to__isnull=False, date_to__gte=now()),
|
||||
organizer=self.request.organizer,
|
||||
live=True,
|
||||
is_public=True,
|
||||
date_from__gte=now(),
|
||||
), self.request).order_by('date_from').first()
|
||||
next_sev = filter_qs_by_attr(SubEvent.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
Q(date_from__gte=now()) | Q(date_to__isnull=False, date_to__gte=now()),
|
||||
event__organizer=self.request.organizer,
|
||||
event__is_public=True,
|
||||
event__live=True,
|
||||
active=True,
|
||||
is_public=True,
|
||||
), self.request).select_related('event').order_by('date_from').first()
|
||||
|
||||
datetime_from = None
|
||||
if (next_ev and next_sev and next_sev.date_from < next_ev.date_from) or (next_sev and not next_ev):
|
||||
datetime_from = next_sev.date_from
|
||||
next_ev = next_sev.event
|
||||
elif next_ev:
|
||||
datetime_from = next_ev.date_from
|
||||
|
||||
if datetime_from:
|
||||
self.tz = pytz.timezone(next_ev.settings.timezone)
|
||||
self.date = datetime_from.astimezone(self.tz).date()
|
||||
else:
|
||||
self.tz = self.request.organizer.timezone
|
||||
self.date = now().astimezone(self.tz).date()
|
||||
|
||||
def _set_date(self):
|
||||
if 'date' in self.request.GET:
|
||||
self.tz = self.request.organizer.timezone
|
||||
try:
|
||||
self.date = dateutil.parser.parse(self.request.GET.get('date')).date()
|
||||
except ValueError:
|
||||
self.date = now().astimezone(self.tz).date()
|
||||
else:
|
||||
self._set_date_to_next_event()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._set_date()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
before = datetime(
|
||||
self.date.year, self.date.month, self.date.day, 0, 0, 0, tzinfo=UTC
|
||||
) - timedelta(days=1)
|
||||
after = datetime(
|
||||
self.date.year, self.date.month, self.date.day, 0, 0, 0, tzinfo=UTC
|
||||
) + timedelta(days=1)
|
||||
|
||||
ctx['date'] = self.date
|
||||
ctx['cal_tz'] = self.tz
|
||||
ctx['before'] = before
|
||||
ctx['after'] = after
|
||||
|
||||
ctx['has_before'], ctx['has_after'] = has_before_after(
|
||||
self.request.organizer.events.filter(
|
||||
sales_channels__contains=self.request.sales_channel.identifier
|
||||
),
|
||||
SubEvent.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
event__is_public=True,
|
||||
event__live=True,
|
||||
event__sales_channels__contains=self.request.sales_channel.identifier
|
||||
),
|
||||
before,
|
||||
after,
|
||||
)
|
||||
|
||||
ebd = self._events_by_day(before, after)
|
||||
if not ebd[self.date]:
|
||||
return ctx
|
||||
|
||||
events = ebd[self.date]
|
||||
shortest_duration = self._get_shortest_duration(events).total_seconds() // 60
|
||||
# pick the next biggest tick_duration based on shortest_duration, max. 180 minutes
|
||||
tick_duration = next((d for d in [5, 10, 15, 30, 60, 120, 180] if d >= shortest_duration), 180)
|
||||
|
||||
raster_size = min(self._get_raster_size(events), tick_duration)
|
||||
events, start, end = self._rasterize_events(events, tick_duration=tick_duration, raster_size=raster_size)
|
||||
calendar_duration = self._get_time_duration(start, end)
|
||||
ctx["calendar_duration"] = self._format_duration(calendar_duration)
|
||||
ctx['time_ticks'] = self._get_time_ticks(start, end, tick_duration)
|
||||
ctx['start'] = datetime.combine(self.date, start)
|
||||
ctx['raster_size'] = raster_size
|
||||
# ctx['end'] = end
|
||||
# size of each grid-column is based on shortest event duration and raster_size
|
||||
# raster_size is based on start/end times, so it could happen we have a small raster but long running events
|
||||
# raster_size will always be smaller or equals tick_duration
|
||||
ctx['raster_to_shortest_ratio'] = round((8 * raster_size) / shortest_duration)
|
||||
|
||||
ctx['events'] = events
|
||||
|
||||
events_by_series = self._grid_for_template(events)
|
||||
ctx['collections'] = events_by_series
|
||||
ctx['no_headlines'] = not any([series for series, events in events_by_series])
|
||||
ctx['multiple_timezones'] = self._multiple_timezones
|
||||
return ctx
|
||||
|
||||
def _get_raster_size(self, events):
|
||||
# get best raster-size for min. # of columns in grid
|
||||
# due to grid-col-calculations in CSS raster_size cannot be bigger than 60 (minutes)
|
||||
|
||||
# all start- and end-times (minute-part) except full hour
|
||||
times = [
|
||||
e["time"].minute for e in events if e["time"] and e["time"].minute
|
||||
] + [
|
||||
e["time_end_today"].minute for e in events if "time_end_today" in e and e["time_end_today"] and e["time_end_today"].minute
|
||||
]
|
||||
if not times:
|
||||
# no time other than full hour, so raster can be 1 hour/60 minutes
|
||||
return 60
|
||||
gcd = reduce(math.gcd, set(times))
|
||||
return next((d for d in [5, 10, 15, 30, 60] if d >= gcd), 60)
|
||||
|
||||
def _get_time_duration(self, start, end):
|
||||
midnight = time(0, 0)
|
||||
return datetime.combine(
|
||||
self.date if end != midnight else self.date + timedelta(days=1),
|
||||
end
|
||||
) - datetime.combine(
|
||||
self.date,
|
||||
start
|
||||
)
|
||||
|
||||
def _format_duration(self, duration):
|
||||
return ":".join([
|
||||
"%02d" % i for i in (
|
||||
(duration.days * 24) + (duration.seconds // 3600),
|
||||
(duration.seconds // 60) % 60
|
||||
)
|
||||
])
|
||||
|
||||
def _floor_time(self, t, raster_size=5):
|
||||
# raster_size based on minutes, might be factored into a helper class with a timedelta as raster
|
||||
minutes = t.hour * 60 + t.minute
|
||||
if minutes % raster_size:
|
||||
minutes = (minutes // raster_size) * raster_size
|
||||
return t.replace(hour=minutes // 60, minute=minutes % 60)
|
||||
return t
|
||||
|
||||
def _ceil_time(self, t, raster_size=5):
|
||||
# raster_size based on minutes, might be factored into a helper class with a timedelta as raster
|
||||
minutes = t.hour * 60 + t.minute
|
||||
if not minutes % raster_size:
|
||||
return t
|
||||
minutes = math.ceil(minutes / raster_size) * raster_size
|
||||
minute = minutes % 60
|
||||
hour = minutes // 60
|
||||
if hour > 23:
|
||||
hour = hour % 24
|
||||
return t.replace(minute=minute, hour=hour)
|
||||
|
||||
def _rasterize_events(self, events, tick_duration, raster_size=5):
|
||||
rastered_events = []
|
||||
start, end = self._get_time_range(events)
|
||||
start = self._floor_time(start, raster_size=tick_duration)
|
||||
end = self._ceil_time(end, raster_size=tick_duration)
|
||||
|
||||
midnight = time(0, 0)
|
||||
for e in events:
|
||||
t = e["time"] or time(0, 0)
|
||||
e["offset_shift_start"] = 0
|
||||
if e["continued"]:
|
||||
e["time_rastered"] = midnight
|
||||
elif t.minute % raster_size:
|
||||
e["time_rastered"] = t.replace(minute=(t.minute // raster_size) * raster_size)
|
||||
e["offset_shift_start"] = t.minute % raster_size
|
||||
else:
|
||||
e["time_rastered"] = t
|
||||
|
||||
e["offset_shift_end"] = 0
|
||||
if "time_end_today" in e and e["time_end_today"]:
|
||||
if e["time_end_today"].minute % raster_size:
|
||||
minute = math.ceil(e["time_end_today"].minute / raster_size) * raster_size
|
||||
hour = e["time_end_today"].hour
|
||||
if minute > 59:
|
||||
minute = minute % 60
|
||||
hour = (hour + 1) % 24
|
||||
e["time_end_today_rastered"] = e["time_end_today"].replace(minute=minute, hour=hour)
|
||||
e["offset_shift_end"] = raster_size - e["time_end_today"].minute % raster_size
|
||||
else:
|
||||
e["time_end_today_rastered"] = e["time_end_today"]
|
||||
else:
|
||||
e["time_end_today"] = e["time_end_today_rastered"] = time(0, 0)
|
||||
|
||||
e["duration_rastered"] = self._format_duration(datetime.combine(
|
||||
self.date if e["time_end_today_rastered"] != midnight else self.date + timedelta(days=1),
|
||||
e["time_end_today_rastered"]
|
||||
) - datetime.combine(
|
||||
self.date,
|
||||
e['time_rastered']
|
||||
))
|
||||
|
||||
e["offset_rastered"] = datetime.combine(self.date, time(0, 0)) + self._get_time_duration(start, e["time_rastered"])
|
||||
|
||||
rastered_events.append(e)
|
||||
|
||||
return rastered_events, start, end
|
||||
|
||||
def _get_shortest_duration(self, events):
|
||||
midnight = time(0, 0)
|
||||
durations = [
|
||||
datetime.combine(
|
||||
self.date if e.get('time_end_today') and e['time_end_today'] != midnight else self.date + timedelta(days=1),
|
||||
e['time_end_today'] if e.get('time_end_today') else time(0, 0)
|
||||
)
|
||||
-
|
||||
datetime.combine(
|
||||
self.date,
|
||||
time(0, 0) if e['continued'] else (e['time'] or time(0, 0))
|
||||
)
|
||||
for e in events
|
||||
]
|
||||
return min([d for d in durations])
|
||||
|
||||
def _get_time_range(self, events):
|
||||
if any(e['continued'] for e in events) or any(e['time'] is None for e in events):
|
||||
starting_at = time(0, 0)
|
||||
else:
|
||||
starting_at = min(e['time'] for e in events)
|
||||
|
||||
if any(e.get('time_end_today') is None for e in events):
|
||||
ending_at = time(0, 0)
|
||||
else:
|
||||
ending_at = max(e['time_end_today'] for e in events)
|
||||
|
||||
return starting_at, ending_at
|
||||
|
||||
def _get_time_ticks(self, start, end, tick_duration):
|
||||
ticks = []
|
||||
tick_duration = timedelta(minutes=tick_duration)
|
||||
|
||||
# convert time to datetime for timedelta calc
|
||||
start = datetime.combine(self.date, start)
|
||||
end = datetime.combine(self.date, end)
|
||||
if end <= start:
|
||||
end = end + timedelta(days=1)
|
||||
|
||||
tick_start = start
|
||||
offset = datetime.utcfromtimestamp(0)
|
||||
duration = datetime.utcfromtimestamp(tick_duration.total_seconds())
|
||||
while tick_start < end:
|
||||
tick = {
|
||||
"start": tick_start,
|
||||
"duration": duration,
|
||||
"offset": offset,
|
||||
}
|
||||
ticks.append(tick)
|
||||
tick_start += tick_duration
|
||||
offset += tick_duration
|
||||
|
||||
return ticks
|
||||
|
||||
def _grid_for_template(self, events):
|
||||
midnight = time(0, 0)
|
||||
rows_by_collection = defaultdict(list)
|
||||
|
||||
# We sort the events into "collections": all subevents from the same
|
||||
# event series together and all non-series events into a "None"
|
||||
# collection. Then, we look if there's already an event in the
|
||||
# collection that overlaps, in which case we need to split the
|
||||
# collection into multiple rows.
|
||||
for counter, e in enumerate(events):
|
||||
collection = e['event'].event if isinstance(e['event'], SubEvent) else None
|
||||
|
||||
placed_in_row = False
|
||||
for row in rows_by_collection[collection]:
|
||||
if any(
|
||||
(e['time_rastered'] < o['time_end_today_rastered'] or o['time_end_today_rastered'] == midnight) and
|
||||
(o['time_rastered'] < e['time_end_today_rastered'] or e['time_end_today_rastered'] == midnight)
|
||||
for o in row
|
||||
):
|
||||
continue
|
||||
row.append(e)
|
||||
placed_in_row = True
|
||||
break
|
||||
|
||||
if not placed_in_row:
|
||||
rows_by_collection[collection].append([e])
|
||||
|
||||
# flatten rows to one stream of events with attribute row
|
||||
# for better keyboard-tab-order in html
|
||||
for collection in rows_by_collection:
|
||||
for i, row in enumerate(rows_by_collection[collection]):
|
||||
concurrency = i + 1
|
||||
for e in row:
|
||||
e["concurrency"] = concurrency
|
||||
rows_by_collection[collection] = {
|
||||
"concurrency": len(rows_by_collection[collection]),
|
||||
"events": sorted([e for row in rows_by_collection[collection] for e in row], key=lambda d: d['time'] or time(0, 0)),
|
||||
}
|
||||
|
||||
def sort_key(c):
|
||||
collection, row = c
|
||||
if collection is None:
|
||||
return ''
|
||||
else:
|
||||
return str(collection.name)
|
||||
return sorted(rows_by_collection.items(), key=sort_key)
|
||||
|
||||
def _events_by_day(self, before, after):
|
||||
ebd = defaultdict(list)
|
||||
timezones = set()
|
||||
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
|
||||
settings.DATABASE_REPLICA
|
||||
).filter(
|
||||
sales_channels__contains=self.request.sales_channel.identifier
|
||||
), before, after, ebd, timezones)
|
||||
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
event__is_public=True,
|
||||
event__live=True,
|
||||
event__sales_channels__contains=self.request.sales_channel.identifier
|
||||
).prefetch_related(
|
||||
'event___settings_objects', 'event__organizer___settings_objects'
|
||||
)), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
|
||||
self._multiple_timezones = len(timezones) > 1
|
||||
return ebd
|
||||
|
||||
|
||||
@method_decorator(cache_page(300), name='dispatch')
|
||||
class OrganizerIcalDownload(OrganizerViewMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user