Compare commits

..

87 Commits
v4.4.1 ... a

Author SHA1 Message Date
Raphael Michel
756c004d66 Improve dialog 2021-11-20 12:08:47 +01:00
Raphael Michel
61243e4a5a Show additional cookie info 2021-11-20 12:08:47 +01:00
Raphael Michel
c10a8575ad Start python-level API 2021-11-20 12:08:47 +01:00
Raphael Michel
202f34ad5b First steps 2021-11-20 12:08:47 +01:00
Raphael Michel
6f0f4755ef Restrict day calendar JS to day calendar page 2021-11-19 19:02:46 +01:00
Richard Schreiber
910a35dedc Fix: calculate day calendar grid in JS as chrome does not support calc-division in CSS-grid (#2340)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-19 17:42:16 +01:00
Raphael Michel
e694bd8c21 Fix next crash in day calendar if there is no start time 2021-11-19 17:08:05 +01:00
Raphael Michel
29cf384c28 Fix crash in day calendar if there is no start time 2021-11-19 16:32:07 +01:00
Raphael Michel
492288f437 Allow customers to change add-ons on existing orders (#2283) 2021-11-19 14:59:54 +01:00
Raphael Michel
34e4f7e0fc Add day calendar to organizer page (#2100)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-11-19 14:59:35 +01:00
Rasmus Kock Grusgaard
f6f3bbcce6 Translations: Update Danish
Currently translated at 35.9% (1613 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2021-11-19 14:59:06 +01:00
Raphael Michel
16054893ed Avoid creation of manual payments with zero amount (#2325) 2021-11-19 12:02:36 +01:00
dependabot[bot]
f6038d2c39 Update django-statici18n requirement from ==1.9.* to >=1.9,<2.2 in /src (#2332)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 17:41:37 +01:00
dependabot[bot]
8d13b51271 Bump pycparser from 2.13 to 2.21 in /src (#2334)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 17:40:59 +01:00
Raphael Michel
83e1f365c2 Sendmail rules: Add warnings and scheduling view (#2328) 2021-11-18 12:48:27 +01:00
Raphael Michel
146e1aeb67 Upgrade mt-940 to 4.* (#2331) 2021-11-18 12:24:54 +01:00
dependabot[bot]
f9b2920984 Update libsass requirement from ==0.20.* to >=0.20,<0.22 in /src (#2315)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 12:14:24 +01:00
dependabot[bot]
2c01b214a7 Update pyflakes requirement from ==2.1.* to >=2.1,<2.5 in /src (#2313)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 12:14:01 +01:00
dependabot[bot]
fdab45e5ce Update bleach requirement from ==3.3.* to >=3.3,<4.2 in /src (#2317)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 12:13:17 +01:00
pretix translation bot
9d2cf18543 Translations update from Weblate (#2327)
Co-authored-by: +se <sebastiano@endsummercamp.org>
2021-11-18 12:12:42 +01:00
Martin Gross
2206ab1d35 Validate Swiss VAT ID against PROD and not TEST-env 2021-11-17 14:07:14 +01:00
Raphael Michel
ecd2c80dce Downgrade 'markdown' package (#2329) 2021-11-17 11:21:59 +01:00
Raphael Michel
3387df491a Fix error handling in Swiss VAT ID validation 2021-11-17 10:30:52 +01:00
pretix translation bot
b6974e0c77 Translations update from Weblate (#2319)
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
Co-authored-by: +se <sebastiano@endsummercamp.org>
2021-11-16 16:58:21 +01:00
Raphael Michel
31751cbd79 Stripe: Fix storage of failed refunds 2021-11-16 12:18:33 +01:00
Raphael Michel
993da5a392 VAT validation: Move cache to data directory 2021-11-16 10:21:08 +01:00
Richard Schreiber
72455209bb CSP: Strip keys with empty values from header (#2322) 2021-11-16 09:24:19 +01:00
Richard Schreiber
803aa0b70d Setup: Allow django-hijack v2.2 (#2321) 2021-11-16 09:24:06 +01:00
Bentrex95
954d86337c Docs: Fix typo in dev-setup-command (#2316) 2021-11-12 12:42:07 +01:00
Raphael Michel
38a58d62f3 Change default settings for background color, invoice attachmentes and name scheme (#2288) 2021-11-11 12:20:34 +01:00
Raphael Michel
e67b39a57b Increase padding if background color is set (#2301) 2021-11-11 12:20:20 +01:00
dependabot[bot]
148b67ac3f Update django-filter requirement from ==2.4.* to >=2.4,<21.2 in /src (#2311)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-11 11:14:35 +01:00
dependabot[bot]
d261cb3b6b Bump django-libsass from 0.8 to 0.9 in /src (#2312)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-11 11:14:07 +01:00
ser8phin
169a6c51b4 Add check to force users to change password (#2284) 2021-11-11 11:10:33 +01:00
Raphael Michel
245ad644ff Subevent calendar: Improve heuristic on when to show names (#2308) 2021-11-11 10:02:45 +01:00
Jaakko Rinta-Filppula
4fdce0d126 Translated on translate.pretix.eu (Finnish)
Currently translated at 50.0% (86 of 172 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fi/

powered by weblate
2021-11-11 10:02:32 +01:00
Jaakko Rinta-Filppula
a542bc7a5a Translated on translate.pretix.eu (Finnish)
Currently translated at 19.0% (856 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2021-11-11 10:02:32 +01:00
dependabot[bot]
3164919923 Update pytest-rerunfailures requirement from ==9.* to >=9,<11 in /src (#2303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-09 19:22:43 +01:00
dependabot[bot]
8085311eb6 Update django-localflavor requirement from ==3.0.* to >=3.0,<3.2 in /src (#2305)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 19:21:53 +01:00
dependabot[bot]
3887a65961 Update pytest-mock requirement from ==2.0.* to >=2.0,<3.7 in /src (#2302)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 19:20:47 +01:00
dependabot[bot]
b229c6156a Update chardet requirement from <3.1.0,>=3.0.2 to >=3.0.2,<4.1.0 in /src (#2304)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 19:20:40 +01:00
Raphael Michel
c45298544e Fix incorrect settings propagagion 2021-11-09 18:45:45 +01:00
Maarten van den Berg
7bb9d3fc3d Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4482 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2021-11-09 17:25:38 +01:00
Ismael Menéndez Fernández
8607df5a9c Translated on translate.pretix.eu (Galician)
Currently translated at 31.3% (54 of 172 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/gl/

powered by weblate
2021-11-09 17:25:38 +01:00
Ismael Menéndez Fernández
c4150473fc Translated on translate.pretix.eu (Galician)
Currently translated at 0.4% (20 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2021-11-09 17:25:38 +01:00
Martin Gross
172b2f74e0 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4483 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2021-11-09 17:25:38 +01:00
Svyatoslav
9586f71dc2 Translated on translate.pretix.eu (Latvian)
Currently translated at 24.0% (1077 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/lv/

powered by weblate
2021-11-09 17:25:38 +01:00
Raphael Michel
25692d180f Make weblate script more robust 2021-11-09 16:34:57 +01:00
Raphael Michel
ae047037dc Docs: Add style guide for commit messages (#2281)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-11-09 16:30:32 +01:00
dependabot[bot]
265106034b Update django-otp requirement from ==0.7.*,>=0.7.5 to >=0.7,<1.2 in /src (#2290)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 12:00:15 +01:00
Raphael Michel
dd0a4df914 Fix error 500 on non-ASCII attachment file names 2021-11-09 11:55:03 +01:00
dependabot[bot]
b0ae40c264 Bump rollup from 1.32.1 to 2.59.0 in /src/pretix/static/npm_dir (#2298)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 11:54:28 +01:00
dependabot[bot]
ad95815043 Update redis requirement from ==3.4.* to >=3.4,<3.6 in /src (#2293)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 11:54:07 +01:00
dependabot[bot]
f68522ec0d Bump @babel/core from 7.13.14 to 7.16.0 in /src/pretix/static/npm_dir (#2297)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:51:38 +01:00
dependabot[bot]
b831e57351 Bump @rollup/plugin-node-resolve from 11.2.0 to 11.2.1 in /src/pretix/static/npm_dir (#2299)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:50:08 +01:00
dependabot[bot]
51166786ee Update phonenumberslite requirement from ==8.11.* to >=8.11,<8.13 in /src (#2291)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:49:52 +01:00
dependabot[bot]
909e7906ff Update sentry-sdk requirement from ==1.1.* to >=1.1,<1.5 in /src (#2292)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:47:55 +01:00
dependabot[bot]
e185d5f0e7 Bump @babel/preset-env from 7.13.12 to 7.16.0 in /src/pretix/static/npm_dir (#2295)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:46:21 +01:00
dependabot[bot]
ce8edf621b Bump vue and vue-template-compiler in /src/pretix/static/npm_dir (#2296)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:45:24 +01:00
Raphael Michel
e58b512876 Fix ordering of questions in backend if all system questions are 0 2021-11-09 09:44:44 +01:00
Raphael Michel
d1754f6d1b GitHub: Enable dependabot (#2289) 2021-11-09 09:43:52 +01:00
Raphael Michel
ff2f1b7424 Fix incorrect check for enabled fields in QuestionList 2021-11-09 09:32:52 +01:00
Raphael Michel
fb1838a2f0 Fix incorrect help text 2021-11-09 09:32:52 +01:00
Raphael Michel
d7b05063a4 Allow to print event location on invoices (#2278)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-11-05 09:47:41 +01:00
Raphael Michel
f64a42d61a Stripe: Fix handling of charges without source 2021-11-04 18:21:29 +01:00
Raphael Michel
c1994e89a5 Stripe: Fix MultipleObjectsReturned in webhook 2021-11-04 17:58:24 +01:00
Raphael Michel
f37de1ad2f Invoice renderer: Do not show end date if same as start date 2021-11-04 17:34:44 +01:00
Raphael Michel
e1ff6f8590 Stripe: Look up charges by their source ID as well 2021-11-04 17:20:45 +01:00
Raphael Michel
a5dd22eb4d Reduce number of global locks needed for confirming payments 2021-11-04 17:18:48 +01:00
Raphael Michel
19cde63505 Fix incorrect setting if Invoice.full_invoice_no 2021-11-04 13:48:39 +01:00
Raphael Michel
754d4f4f62 Sendmail: Fix subevent-less rules in event series 2021-11-04 10:21:03 +01:00
Bentrex95
e433230573 Docs: Update dependencies for dev setup (#2282)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-03 12:35:26 +01:00
Julia Luna
f8927396d3 API: Add endpoints for automated email rules (#2178)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-03 11:49:01 +01:00
Raphael Michel
60be99fbb2 Another attempt at correct sanitization of HTML in invoice content (#2279) 2021-11-03 11:13:43 +01:00
Raphael Michel
0c508c5ba4 Fix remaining DST error in auto check-out 2021-11-03 09:34:50 +01:00
Richard Schreiber
ea6067ab3f Fix Outlook >= 2010 trimming header image (#2277)
* fix image cutoff with mso-line-height: at-least
* align text to the left; fully centered text is hard to read
* remove mso cellpadding-tables as they double up the spacing
* additionally add background-color to a table with width=100% for broader support (e.g. Yahoo and AOL)
2021-11-02 12:59:09 +01:00
Raphael Michel
9d0fa84277 Add nodejs to update notes 2021-10-31 18:32:16 +01:00
Raphael Michel
a6835d3b14 Fix bug in 03de0d5d2 2021-10-31 18:26:45 +01:00
Raphael Michel
9ff565f772 Fix unreadable active tab 2021-10-31 17:28:35 +01:00
Raphael Michel
5d41b20bae Fix crash in waiting list 2021-10-31 17:28:29 +01:00
Raphael Michel
03de0d5d2e Do not ask authenticated customers to re-type their email address 2021-10-29 17:23:26 +02:00
Raphael Michel
2937acdc66 Bump to 4.5.0.dev0 2021-10-29 15:38:52 +02:00
Raphael Michel
6fd09e99e2 Bump version to 4.4.0 2021-10-29 15:38:52 +02:00
Raphael Michel
290e14689d Fix check_order_transactions on SQLite 2021-10-29 15:38:52 +02:00
Raphael Michel
89c937089b Translated on translate.pretix.eu (Galician)
Currently translated at 0.0% (0 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2021-10-29 14:17:23 +02:00
Raphael Michel
0e02febe76 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4483 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2021-10-29 14:17:23 +02:00
Raphael Michel
771f822e5f Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4483 of 4483 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2021-10-29 14:17:23 +02:00
125 changed files with 17392 additions and 5521 deletions

15
.github/dependabot.yml vendored Normal file
View 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"

View File

@@ -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
"""""""""""""""""""""""""

View File

@@ -31,5 +31,6 @@ Resources and endpoints
webhooks
seatingplans
exporters
sendmail_rules
billing_invoices
billing_var

View File

@@ -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",

View 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.

View File

@@ -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/

View File

@@ -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]"

View File

@@ -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

View File

@@ -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"

View File

@@ -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',

View File

@@ -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):

View File

@@ -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):

View File

@@ -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(

View File

@@ -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

View File

@@ -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)

View File

@@ -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),
))

View File

@@ -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):

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View 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),
),
]

View File

@@ -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,
),
]

View File

@@ -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,

View File

@@ -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):

View File

@@ -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)

View File

@@ -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')

View File

@@ -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

View File

@@ -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)

View File

@@ -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 '',

View File

@@ -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:

View File

@@ -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):

View File

@@ -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,

View File

@@ -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>

View File

@@ -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 %}

View 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__

View File

@@ -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)

View File

@@ -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

View File

@@ -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',

View File

@@ -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(

View File

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

View File

@@ -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'))

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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" %}

View File

@@ -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" %}

View File

@@ -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 %}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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" %}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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

View File

@@ -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),

View File

@@ -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')

View File

@@ -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 theyre 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

View File

@@ -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 sellers 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"

View File

@@ -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 sellers 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"

View File

@@ -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 theyre 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

View File

@@ -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 theyre 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

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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 sellers 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"

View File

@@ -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

View 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)

View File

@@ -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"

View File

@@ -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'),

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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 %}

View File

@@ -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):

View File

@@ -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():

View File

@@ -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'):

View 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

View File

@@ -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

View File

@@ -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
),

View File

@@ -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.
"""

View File

@@ -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 %}

View File

@@ -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 }} &middot; {{ form.cartpos.subevent.get_date_range_display_as_html }}
{% if form.cartpos.event.settings.show_times %}
{{ form.pos.subevent.name }} &middot; {{ 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>

View File

@@ -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 %}

View File

@@ -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 }} &middot; {{ 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 }} &middot; {{ 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>

View File

@@ -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 %}

View File

@@ -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">&nbsp;</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>

View File

@@ -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>

View File

@@ -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 %}&middot; {% 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 %}
&middot;
<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 %}

View File

@@ -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 %}

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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

View File

@@ -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))

View File

@@ -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.'))

View File

@@ -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