Compare commits

..
Author SHA1 Message Date
Kara Engelhardt 6ce98de931 Fix widget not regenerating
Adds support for use_vite variant to updateassets management command and unifies widget generation code
2026-08-13 11:01:38 +02:00
7fe31634e6 Allow RelativeDate in relation to a moment of order (#6160)
* initial implementation

* factor out _resolve_base_date

* add js to prevent illegal inputs

* fix tests

* Update src/pretix/base/reldate.py

Co-authored-by: Raphael Michel <michel@pretix.eu>

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* move js includes of to fragment_js.html

* add type annotations

* moves logic from RelativeDateWrapper into RelativeDate and adds BaseChoice for configuring which models attributes support which relationship

* fix tests

* test upgrade behaviour

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* move reldate.js include in correct file

* add OptionAttrsSelect to allow select options with their own attributes per value

* add a little bit of information to the tests for future reference

* rewrite reldate.js use data-attributes

* general cleanup

* add test for order.subevents cases

* use correct choice format

* remove order.subevent variants

* various cleanup

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* remove empty docstrings

* add depreciation warning

* change event listener to pretix:bind-forms

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-08-12 15:37:29 +02:00
f25c233e91 Fix performance issues in download reminder (#6393)
* Fix performance issues in download reminder

* Update src/pretix/base/services/orders.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Fixes after review

* Fix check in loop

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-08-10 16:13:54 +02:00
robbi5andGitHub 5cf28f1b81 Fix typo in devicesecurity blocking checkinrpc.annul (#6467) 2026-08-10 15:58:15 +02:00
Raphael MichelandGitHub d08216d8c5 API: Allow to simulate check-ins (#6360)
* API: Allow to simulate check-ins

* Add missing file
2026-08-07 18:59:01 +02:00
Lukas BockstallerandGitHub 4a28689690 log paypal payment durations (#6461)
* log payment processing durations

* remove log noise

* fix attribute access

* remove debugging import
2026-08-07 16:34:36 +02:00
luelistaandGitHub 958f75b109 Add tests to prevent reintroducing CSP nonces (Z#23240534) (#6409)
As discussed in PR #6387
2026-08-07 14:36:46 +02:00
Richard SchreiberandGitHub 81f58456e5 Fix API-docs example for addon_to on order-change (#6451)
* Fix API-docs example for addon_to on order-change

* Update orders.rst
2026-08-07 11:03:36 +02:00
4d9dfa88fe Paypal2: handle incomming capture webhooks (Z#23240966) (#6456)
* store the state of the payment regardless of the state

control.html shows the banner that the payment is in review depending on payment.info

* handle capture ressource

* Update src/pretix/plugins/paypal2/views.py

Co-authored-by: Phin Wolkwitz <wolkwitz@pretix.eu>

* add test

* cleanup logic regarding uninteresting resource_type

* store payment.info during _execute_payment asap

---------

Co-authored-by: Phin Wolkwitz <wolkwitz@pretix.eu>
2026-08-06 15:06:20 +02:00
Raphael MichelandGitHub ea792e76b2 Device list: Show online status (#6455)
* Device list: Show online status

* Fixes post-review
2026-08-06 09:07:20 +02:00
Raphael MichelandGitHub 5c448279f0 API: Make all mutating endpoints transactional (#6430)
All of our API endpoints that do something in the system do at least two
SQL queries, one for the actual change and one for the log entry. Often
many more. We want all of this to happen in a transaction so we know an
API call was executed or not at all, not half-way.
2026-08-05 16:52:28 +02:00
dependabot[bot]andGitHub e3fa1aed7f Bump django-localflavor from 5.0 to 5.1 (#6449) 2026-08-05 14:56:53 +02:00
Luca Sorace \"StranckandRaphael Michel b164cf44c0 Translations: Update Italian
Currently translated at 39.8% (2544 of 6387 strings)

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

powered by weblate
2026-08-05 14:56:14 +02:00
Luca Sorace \"StranckandRaphael Michel 7bc79337c3 Translations: Update Italian
Currently translated at 99.6% (259 of 260 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/it/

powered by weblate
2026-08-05 14:56:14 +02:00
Luca Sorace \"StranckandRaphael Michel 5d8ba189fa Translations: Update Italian
Currently translated at 39.6% (2530 of 6387 strings)

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

powered by weblate
2026-08-05 14:56:14 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 1ea1c1789c Translations: Update Hungarian
Currently translated at 99.6% (6362 of 6387 strings)

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

powered by weblate
2026-08-05 14:56:14 +02:00
NikolaiandRaphael Michel 1a6edae769 Translations: Update Danish
Currently translated at 58.0% (151 of 260 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/da/

powered by weblate
2026-08-05 14:56:14 +02:00
NikolaiandRaphael Michel c4950e714d Translations: Update Danish
Currently translated at 62.7% (4011 of 6387 strings)

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

powered by weblate
2026-08-05 14:56:14 +02:00
Raphael MichelandGitHub 407728cc55 Bulk refund: Fix only first refund being made (Z#23239965) (#6376) 2026-08-05 11:46:52 +02:00
Raphael MichelandGitHub 9fe25544c1 Email settings: Fix DMARC warning (#6452) 2026-08-05 11:04:56 +02:00
Raphael MichelandGitHub 8133061fe1 Devices: Store timestamp of last contact (#6453) 2026-08-05 10:57:13 +02:00
Raphael Michel 288ac50600 Merge branch 'html-injection-placeholder' into 'master'
Prevent HTML injection in email preview (Z#23241741)

See merge request pretix/pretix!47
2026-08-05 10:56:46 +02:00
Raphael Michel e7657a3dd3 Prevent HTML injection in email preview (Z#23241741) 2026-08-05 10:56:46 +02:00
Martin Gross b659534772 isort 2026-08-04 23:01:29 +02:00
Martin Gross c11ebb1391 Skip over Exporters with ExportError-Exceptions (such as ORMQL-Exporter) (Fixes PRETIXEU-F3X) 2026-08-04 22:59:19 +02:00
pajowuandGitHub b20557a996 Show correct number of currency placed in widget (Z#23241522) (#6425) 2026-08-04 11:19:43 +02:00
Martin Gross e4a0bba3bc Fix font enumeration 2026-08-04 10:22:09 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c369ba5f60 Update cryptography requirement from >=49.0.0 to >=50.0.0 (#6443)
Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 08:47:14 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2c876057bf Bump markdown from 3.10.2 to 3.10.3 (#6442)
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.10.2 to 3.10.3.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.10.2...3.10.3)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: 3.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 08:46:52 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b70c1b02c2 Bump django-bootstrap3 from 26.1 to 26.2 (#6445)
Bumps [django-bootstrap3](https://github.com/zostera/django-bootstrap3) from 26.1 to 26.2.
- [Changelog](https://github.com/zostera/django-bootstrap3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zostera/django-bootstrap3/compare/v26.1...v26.2)

---
updated-dependencies:
- dependency-name: django-bootstrap3
  dependency-version: '26.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 08:46:25 +02:00
pajowuandGitHub 01d736361d Cart Manager: Round custom price to currency when adding (#6426) 2026-08-03 14:26:57 +02:00
Raphael Michel 7627e4b548 Add Hungarian language 2026-07-31 16:56:43 +02:00
Raphael Michel 541e7412e1 Bump version to 2026.8.0.dev0 2026-07-30 20:39:47 +02:00
77 changed files with 1765 additions and 2518 deletions
-202
View File
@@ -1,202 +0,0 @@
Führe den vollständigen Upstream-Merge-Workflow für pretix_cgo durch. Gehe dabei strikt Schritt für Schritt vor und warte an den markierten Stellen auf Bestätigung.
---
## Schritt 1 Master aktualisieren
```bash
git checkout master
git pull
```
---
## Schritt 2 Upstream-Tags holen & neuesten Tag ermitteln
```bash
git fetch upstream --tags
```
Bestimme danach den neuesten upstream-Tag (Format `vYYYY.M.P`, **ohne** das `.0`-Suffix unserer eigenen Tags):
```bash
git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1
```
Merke dir diesen Tag als `UPSTREAM_TAG` (z.B. `v2026.5.0`) und leite daraus den Branch-Namen ab: `upstream/2026.5.0` (ohne führendes `v`).
---
## Schritt 3 Branch erstellen
```bash
git checkout -b upstream/<VERSION> tags/<UPSTREAM_TAG>
# Beispiel: git checkout -b upstream/2026.5.0 tags/v2026.5.0
```
---
## Schritt 4 Master mergen (immer, nicht optional)
```bash
git merge master
```
Bei Merge-Konflikten: Konflikte anzeigen und gemeinsam mit dem Nutzer lösen, bevor es weitergeht.
---
## Schritt 5 Branch pushen & MR anlegen
Push mit Push Option, damit Gitea direkt einen MR anlegt:
```bash
git push --set-upstream origin upstream/<VERSION> -o merge_request.create
```
---
## Schritt 6 Gender-Prüfung in den deutschen Übersetzungen
**Erst nach dem Merge mit master** die folgenden vier Dateien auf gegenderte Sprache prüfen:
- `src/pretix/locale/de/LC_MESSAGES/django.po`
- `src/pretix/locale/de/LC_MESSAGES/djangojs.po`
- `src/pretix/locale/de_Informal/LC_MESSAGES/django.po`
- `src/pretix/locale/de_Informal/LC_MESSAGES/djangojs.po`
Suche in den `msgstr`-Zeilen (und den zugehörigen `msgid`-Zeilen für Kontext) nach folgenden Mustern:
| Muster | Beispiele |
|--------|-----------|
| Binneni mit `*` | `Teilnehmer*in`, `Käufer*innen` |
| Binneni mit `:` | `Teilnehmer:in`, `Käufer:innen` |
| Binneni mit `_` | `Teilnehmer_in`, `Käufer_innen` |
| Binneni mit `(in)` | `Teilnehmer(in)`, `Käufer(innen)` |
| Partizip-Plural auf `-enden` | `Teilnehmenden`, `Helfenden`, `Organisierenden`, `Bestellenden`, `Buchenden`, `Zahlenden` |
**Ausgabe der Befunde:** Zeige für jeden Treffer:
1. Datei + Zeilennummer
2. Den vollständigen `msgstr`-Block (mehrzeilig wenn nötig) als Kontext
3. Den konkreten Vorschlag für die Ersetzung (z.B. `Teilnehmer*in``Teilnehmer`, `Teilnehmenden``Teilnehmer`)
Warte dann auf Freigabe durch den Nutzer, bevor Änderungen vorgenommen werden.
Nach Freigabe: Korrekturen direkt in den Dateien vornehmen und committen:
```bash
git add src/pretix/locale/de/ src/pretix/locale/de_Informal/
git commit -m "Remove gendered language from de/de_Informal translations"
git push
```
---
## Schritt 7 Warten auf Merge-Bestätigung
**Hier pausieren.** Den Nutzer auffordern zu bestätigen, sobald der MR auf Gitea gemergt wurde.
---
## Schritt 8 Master aktualisieren und neuen Tag vergeben
Nach Bestätigung:
```bash
git checkout master
git pull
```
Den neuen Tag ableiten: `<UPSTREAM_TAG>.0` **ohne führendes `v`** (z.B. `v2026.5.0``2026.5.0.0`):
```bash
git tag <VERSION>.0
# Beispiel: git tag 2026.5.0.0
git push origin <VERSION>.0
```
Ausgabe: Bestätigung mit dem neuen Tag und kurzem Status.
---
## Schritt 9 Warten auf Build-Pipeline
Nutze den Gitea MCP Server, um den Pipeline-Lauf für den soeben gepushten Tag zu überwachen:
- **Repo:** `CGM_Public` / `pretix_cgo`
- **Methode:** `mcp__gitea__actions_run_read` mit `method: list_runs`
- Warte, bis ein Run mit dem passenden Ref (`refs/tags/<NEUER_TAG>`) den Status `success` erreicht.
- Prüfe alle ~30 Sekunden erneut und zeige dem Nutzer den aktuellen Status (running / success / failure).
- Bei `failure`: sofort stoppen und dem Nutzer melden.
Erst wenn der Build erfolgreich (`success`) abgeschlossen ist, weitermachen.
---
## Schritt 10 cgm72 GitOps-Update
Im GitOps-Repo unter `../OrtlerstrasseGitOps/` (liegt immer neben dem pretix_cgo-Repo):
1. In `pretix-cgm72/pretix-deployment.yaml` den Image-Tag aktualisieren:
- Suche: `cr.ortlerstrasse.de/cgo/pretix:<ALTER_TAG>`
- Ersetze mit: `cr.ortlerstrasse.de/cgo/pretix:<NEUER_TAG>` (= der in Schritt 8 gesetzte Git-Tag, z.B. `2026.5.0.0`)
2. Committen und pushen (im OrtlerstrasseGitOps-Verzeichnis):
```bash
git -C ../OrtlerstrasseGitOps add pretix-cgm72/pretix-deployment.yaml
git -C ../OrtlerstrasseGitOps commit -m "Update pretix-cgm72 to <NEUER_TAG>"
git -C ../OrtlerstrasseGitOps push
```
Der GitOps-Operator wendet das YAML automatisch via `kubectl apply` an und rollt den Container neu aus.
---
## Schritt 11 cgm72: Container-Neustart abwarten und Loginseite prüfen
Warte, bis der Container neu gestartet ist, und prüfe dann die Loginseite:
- **URL:** `https://anmeldung.christliche-gemeinde.de/`
- **Prüfung:** HTTP 200 und die Seite enthält typische Pretix-Inhalte (z.B. Stichwort „pretix" oder ein Login-Formular im Body)
Methode: Nutze `mcp__claude_ai_Simon_Fetch__fetch_page` (oder `curl -sI`), um die URL alle ~30 Sekunden abzurufen. Erwarte einen kurzen Ausfall während des Neustarts warte danach auf eine erfolgreiche Antwort.
Zeige dem Nutzer das Ergebnis (Status-Code + kurzen Seitenausschnitt als Bestätigung).
---
## Schritt 12 Manuelle Bestätigung für cgm72
**Hier pausieren.** Den Nutzer auffordern zu bestätigen, dass cgm72 korrekt funktioniert (`https://anmeldung.christliche-gemeinde.de/`), bevor mit sola fortgefahren wird.
---
## Schritt 13 sola GitOps-Update
Im GitOps-Repo unter `../OrtlerstrasseGitOps/` (liegt immer neben dem pretix_cgo-Repo):
1. In `pretix-sola/pretix-deployment.yaml` den Image-Tag aktualisieren:
- Suche: `cr.ortlerstrasse.de/cgo/pretix:<ALTER_TAG>`
- Ersetze mit: `cr.ortlerstrasse.de/cgo/pretix:<NEUER_TAG>`
2. Committen und pushen:
```bash
git -C ../OrtlerstrasseGitOps add pretix-sola/pretix-deployment.yaml
git -C ../OrtlerstrasseGitOps commit -m "Update pretix-sola to <NEUER_TAG>"
git -C ../OrtlerstrasseGitOps push
```
---
## Schritt 14 sola: Container-Neustart abwarten und Loginseite prüfen
Warte, bis der Container neu gestartet ist, und prüfe dann die Loginseite:
- **URL:** `https://anmeldung.sola-muenchen.de`
- **Prüfung:** HTTP 200 und die Seite enthält typische Pretix-Inhalte
Methode: wie in Schritt 11.
Ausgabe: Bestätigung mit kurzem Abschluss-Status des gesamten Workflows.
-31
View File
@@ -1,31 +0,0 @@
name: Build Deploy email notification tool
run-name: ${{ gitea.actor }} building new version of the email notification tool
on:
push: # Baut bei jedem Push (Branches + Tags)
workflow_dispatch:
jobs:
Apply-Kubernetes-Resources:
runs-on: podman
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Login to Docker Registry
run: podman login -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_TOKEN }} cr.ortlerstrasse.de
- name: Set Docker Image Tag
run: |
if [[ "${{ gitea.ref }}" == refs/tags/* ]]; then
echo "TAG_NAME=${{ gitea.ref_name }}" >> $GITHUB_ENV
else
echo "TAG_NAME=latest" >> $GITHUB_ENV
fi
- name: Build Docker image
run: podman build -t cr.ortlerstrasse.de/cgo/pretix:${{ env.TAG_NAME }} .
- name: Push Docker image
run: |
podman push cr.ortlerstrasse.de/cgo/pretix:${{ env.TAG_NAME }}
echo "Image pushed successfully: cr.ortlerstrasse.de/cgo/pretix:${{ env.TAG_NAME }}"
+1 -2
View File
@@ -61,8 +61,7 @@ RUN pip3 install -U \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached]" \
gunicorn django-extensions ipython \
pretix-oidc && \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
RUN chmod +x /usr/local/bin/pretix && \
+2
View File
@@ -71,6 +71,8 @@ Checking a ticket in
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
**Example request**:
+2 -2
View File
@@ -2038,7 +2038,7 @@ Manipulating individual positions
* ``order`` (mandatory, specified as a string mapping to a ``code``)
* ``addon_to`` (optional, specified as an integer mapping to the ``positionid`` of the parent position)
* ``addon_to`` (optional, specified as an integer mapping to ``positionid`` - the number of the position within the order, see :ref:`_order-position-resource` - of the parent position)
* ``item`` (mandatory)
@@ -2348,7 +2348,7 @@ otherwise, such as splitting an order or changing fees.
"subevent": 562,
"seat": "seat-guid-2",
"price": "99.99",
"addon_to": 12374,
"addon_to": 1,
"attendee_name": "Peter",
}
],
+4 -4
View File
@@ -33,12 +33,12 @@ dependencies = [
"bleach==6.4.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=49.0.0",
"cryptography>=50.0.0",
"css-inline==0.21.*",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
"django-bootstrap3==26.2",
"django-compressor==4.6.0",
"django-countries==9.0.*",
"django-filter==26.1",
@@ -48,7 +48,7 @@ dependencies = [
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
"django-libsass==0.9",
"django-localflavor==5.0",
"django-localflavor==5.1",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.7.*",
@@ -67,7 +67,7 @@ dependencies = [
"kombu==5.6.*",
"libsass==0.23.*",
"lxml",
"markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.10.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.3.*",
+1 -1
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__ = "2026.7.0"
__version__ = "2026.8.0.dev0"
+1
View File
@@ -104,6 +104,7 @@ ALL_LANGUAGES = [
('gl', _('Galician')),
('el', _('Greek')),
('he', _('Hebrew')),
('hu', _('Hungarian')),
('id', _('Indonesian')),
('it', _('Italian')),
('ja', _('Japanese')),
+25 -1
View File
@@ -20,8 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
import logging
from datetime import timedelta
from django.contrib.auth.models import AnonymousUser
from django.db import DatabaseError
from django.utils.timezone import now
from django_scopes import scopes_disabled
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
@@ -30,6 +33,7 @@ from pretix.api.auth.devicesecurity import (
FullAccessSecurityProfile, get_all_security_profiles,
)
from pretix.base.models import Device
from pretix.base.models.devices import DeviceLastSeen
logger = logging.getLogger(__name__)
@@ -42,7 +46,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
model = self.get_model()
try:
with scopes_disabled():
device = model.objects.select_related('organizer').get(api_token=key)
device = model.objects.select_related('organizer', 'last_seen').get(api_token=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')
@@ -53,6 +57,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
logging.warning(f'Connection attempt of revoked device {device.pk}.')
raise exceptions.AuthenticationFailed('Device access has been revoked.')
self._update_last_seen(device)
return AnonymousUser(), device
def authenticate(self, request):
@@ -63,3 +68,22 @@ class DeviceTokenAuthentication(TokenAuthentication):
if not profile.is_allowed(request):
raise exceptions.PermissionDenied('Request denied by device security profile.')
return r
def _update_last_seen(self, device: Device):
try:
try:
last_seen_obj = device.last_seen
except DeviceLastSeen.DoesNotExist:
# First request from device, create model, ignore result. Use get_or_create to be safe
# against concurrent create requests
DeviceLastSeen.objects.get_or_create(device=device, last_seen=now())
else:
if now() - last_seen_obj.last_seen < timedelta(seconds=10):
# We don't need to know the last seen info of a device to more precision than this,
# so we can avoid some database writes if the device is bursting a lot of requests.
return
last_seen_obj.last_seen = now()
last_seen_obj.save(update_fields=["last_seen"])
except DatabaseError:
# Do not stop the request from happening
logger.exception("Database error while updating last_seen")
+3 -3
View File
@@ -115,7 +115,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'),
('POST', 'api-v1:checkinrpc.annul'),
('GET', 'api-v1:checkinrpc.search'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-lookup'),
@@ -154,7 +154,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'),
('POST', 'api-v1:checkinrpc.annul'),
('GET', 'api-v1:checkinrpc.search'),
)
@@ -191,7 +191,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'),
('POST', 'api-v1:checkinrpc.annul'),
('GET', 'api-v1:checkinrpc.search'),
)
+1
View File
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
answers = serializers.JSONField(required=False, allow_null=True)
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
exchange_medium_identifier = serializers.CharField(required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+8
View File
@@ -139,6 +139,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
)
return qs
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -153,6 +154,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -837,6 +839,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
)
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
if simulate:
raise CheckInError(
gettext('You cannot simulate a medium exchange.'),
'error'
)
with transaction.atomic():
# Do exchange and check-in atomically, i.e. both succeed or both fail
medium = perform_media_exchange(
@@ -1064,6 +1071,7 @@ class CheckinRPCRedeemView(views.APIView):
legacy_url_support=False,
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
simulate=s.validated_data.get('simulate'),
)
+4
View File
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
@@ -64,6 +65,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
'limit_sales_channels',
)
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -78,6 +80,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -87,6 +90,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('You cannot delete this discount because it already has '
+11
View File
@@ -257,6 +257,7 @@ class EventViewSet(viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_create(self, serializer):
copy_from = None
if 'clone_from' in self.request.GET:
@@ -320,6 +321,7 @@ class EventViewSet(viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
@@ -355,6 +357,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
@@ -513,6 +516,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
resp['X-Page-Generated'] = date
return resp
@transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
super().perform_update(serializer)
@@ -529,6 +533,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -538,6 +543,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
@@ -566,6 +572,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.tax_rules.all()
@transaction.atomic()
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.log_action(
@@ -575,6 +582,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -584,6 +592,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
@@ -757,6 +766,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
}
return ctx
@transaction.atomic()
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.event.log_action(
@@ -766,6 +776,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
data={"seats": [serializer.instance.pk]},
)
@transaction.atomic()
def bulk_change_blocked(self, blocked):
s = SeatBulkBlockInputSerializer(
data=self.request.data,
+19 -5
View File
@@ -23,6 +23,7 @@ from datetime import timedelta
from celery.result import AsyncResult
from django.conf import settings
from django.db import transaction
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
@@ -45,7 +46,8 @@ from pretix.base.models import (
)
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.services.export import (
export, init_event_exporters, init_organizer_exporters, multiexport,
ExportError, export, init_event_exporters, init_organizer_exporters,
multiexport,
)
from pretix.helpers.http import ChunkBasedFileResponse
@@ -149,8 +151,11 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
))
exporters = []
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
try:
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
except ExportError:
pass
return exporters
def do_export(self, cf, instance, data):
@@ -180,8 +185,11 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
))
exporters = []
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
try:
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
except ExportError:
pass
return exporters
def do_export(self, cf, instance, data):
@@ -220,6 +228,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
qs = self.request.event.scheduled_exports
return qs.select_related("owner")
@transaction.atomic()
def perform_create(self, serializer):
if not self.request.user.is_authenticated:
raise PermissionDenied('Creation of exports requires user-specific API access.')
@@ -250,6 +259,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
))
return {e.identifier: e for e in exporters}
@transaction.atomic()
def perform_update(self, serializer):
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
@@ -275,6 +285,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
self.request.event.log_action(
'pretix.event.export.schedule.deleted',
@@ -302,6 +313,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
qs = self.request.organizer.scheduled_exports
return qs.select_related("owner")
@transaction.atomic()
def perform_create(self, serializer):
if not self.request.user.is_authenticated:
raise PermissionDenied('Creation of exports requires user-specific API access.')
@@ -332,6 +344,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
))
return {e.identifier: e for e in exporters}
@transaction.atomic()
def perform_update(self, serializer):
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
@@ -382,6 +395,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.organizer.export.schedule.deleted',
+28
View File
@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import django_filters
from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
@@ -109,6 +110,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
).all()
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -123,6 +125,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
@@ -139,6 +142,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
@@ -183,6 +187,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_create(self, serializer):
item = self.item
if not item.has_variations:
@@ -197,6 +202,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
{'value': serializer.instance.value})
)
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
@@ -207,6 +213,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
{'value': serializer.instance.value})
)
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
@@ -249,6 +256,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
ctx['item'] = self.item
return ctx
@transaction.atomic()
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(base_item=item)
@@ -259,6 +267,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.base_item.log_action(
@@ -268,6 +277,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.base_item.log_action(
@@ -303,6 +313,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
ctx['item'] = self.item
return ctx
@transaction.atomic()
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(item=item)
@@ -313,6 +324,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
@@ -322,6 +334,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.item.log_action(
@@ -354,6 +367,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
ctx['item'] = self.item
return ctx
@transaction.atomic()
def perform_create(self, serializer):
item = self.item
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
@@ -365,6 +379,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.base_item.log_action(
@@ -374,6 +389,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.base_item.log_action(
@@ -403,6 +419,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.categories.all()
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -417,6 +434,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -426,6 +444,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
for item in instance.items.all():
item.category = None
@@ -458,6 +477,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -472,6 +492,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -481,6 +502,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.question.deleted',
@@ -509,6 +531,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
return ctx
@transaction.atomic()
def perform_create(self, serializer):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
serializer.save(question=q)
@@ -519,6 +542,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.question.log_action(
@@ -528,6 +552,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@transaction.atomic()
def perform_destroy(self, instance):
instance.question.log_action(
'pretix.event.question.option.deleted',
@@ -586,6 +611,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -608,6 +634,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['request'] = self.request
return ctx
@transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
@@ -663,6 +690,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
)
serializer.instance.rebuild_cache()
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.quota.deleted',
+3
View File
@@ -394,6 +394,7 @@ class TeamViewSet(viewsets.ModelViewSet):
)
return inst
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
instance.delete()
@@ -693,6 +694,7 @@ class MembershipTypeViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied("Can only be deleted if unused.")
@@ -833,6 +835,7 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
)
return inst
@transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied("Can only be deleted if unused.")
+4
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import django_filters
from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
@@ -62,6 +63,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -70,6 +72,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
)
@transaction.atomic()
def perform_update(self, serializer):
if serializer.instance.voucher:
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
@@ -80,6 +83,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
)
@transaction.atomic()
def perform_destroy(self, instance):
if instance.voucher:
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
+4
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import django_filters
from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
@@ -48,6 +49,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
@@ -57,6 +59,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
@@ -67,6 +70,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
)
return inst
@transaction.atomic()
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.webhook.changed',
-1
View File
@@ -28,6 +28,5 @@ from .items import * # noqa
from .json import * # noqa
from .mail import * # noqa
from .orderlist import * # noqa
from .relevant_orderlist import * # noqa
from .reusablemedia import * # noqa
from .waitinglist import * # noqa
+1 -1
View File
@@ -89,7 +89,7 @@ class OrderListExporter(MultiSheetListExporter):
description = gettext_lazy('Download a spreadsheet of all orders. The spreadsheet will include three sheets, one '
'with a line for every order, one with a line for every order position, and one with '
'a line for every additional fee charged in an order.')
featured = False
featured = True
repeatable_read = False
@cached_property
File diff suppressed because it is too large Load Diff
+15
View File
@@ -314,3 +314,18 @@ class BusinessBooleanRadio(forms.RadioSelect):
'False': False,
False: False,
}.get(value)
class OptionAttrsSelect(forms.Select):
def __init__(self, *args, option_attrs=None, **kwargs):
super().__init__(*args, **kwargs)
self.option_attrs = option_attrs or {}
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
option = super().create_option(
name, value, label, selected, index, subindex=subindex, attrs=attrs
)
extra = self.option_attrs.get(str(value))
if extra:
option["attrs"].update(extra)
return option
@@ -0,0 +1,43 @@
# Generated by Django 5.2.16 on 2026-08-05 08:00
import django.db.models.deletion
from django.db import migrations, models
import pretix.helpers.database
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0306_alter_eventmetaproperty_unique_together"),
]
operations = [
migrations.CreateModel(
name="DeviceLastSeen",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("last_seen", models.DateTimeField(auto_now=True)),
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.device",
),
),
],
),
migrations.AddIndex(
model_name="devicelastseen",
index=pretix.helpers.database.BrinIndexIgnoredOnSQLite(
models.F("last_seen"),
autosummarize=True,
name="pretixbase_device_last_seen",
),
),
]
+27
View File
@@ -20,11 +20,13 @@
# <https://www.gnu.org/licenses/>.
#
import string
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Max
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
@@ -32,6 +34,7 @@ from pretix.base.models import LoggedModel
from pretix.base.permissions import (
AnyPermissionOf, assert_valid_event_permission,
)
from pretix.helpers import BrinIndexIgnoredOnSQLite
@scopes_disabled()
@@ -287,3 +290,27 @@ class Device(LoggedModel):
return self.get_events_with_any_permission()
else:
return self.organizer.events.none()
class DeviceLastSeen(models.Model):
# This is a separate model since we expect it to get A LOT of writes and PostgreSQL always
# writes full rows and then needs to update all indexes on the row, so this is going to save a
# lot of write traffic on the databse
device = models.OneToOneField("Device", on_delete=models.CASCADE, related_name="last_seen")
last_seen = models.DateTimeField(auto_now=True)
class Meta:
indexes = [
BrinIndexIgnoredOnSQLite(
# BRIN indexes are highly efficient on lots of updates, especially of chronological data
# and especially if we later want to query them by range, as we likely want to.
"last_seen",
name="pretixbase_device_last_seen",
autosummarize=True
)
]
@property
def is_recent(self):
# pretixSCAN/pretixPOS sync every 5 minutes, so 7 minutes can be considered "offline"
return now() - self.last_seen < timedelta(minutes=7)
+373 -155
View File
@@ -19,30 +19,245 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import copy
import datetime
import os
import warnings
from collections import namedtuple
from typing import Union
from dataclasses import dataclass
from typing import (
TYPE_CHECKING, Iterable, List, Literal, Optional, Tuple, Union, cast,
)
from zoneinfo import ZoneInfo
from dateutil import parser
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.forms.widgets import Widget
from django.utils.formats import get_format
from django.utils.functional import lazy
from django.utils.functional import Promise, lazy
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
BASE_CHOICES = (
('date_from', _('Event start')),
('date_to', _('Event end')),
('date_admission', _('Event admission')),
('presale_start', _('Presale start')),
('presale_end', _('Presale end')),
)
from pretix.base.forms.widgets import OptionAttrsSelect
RelativeDate = namedtuple('RelativeDate', ['days', 'minutes', 'time', 'is_after', 'base_date_name'], defaults=(0, None, None, False, 'date_from'))
if TYPE_CHECKING:
from .models import Event, Order, SubEvent
@dataclass(frozen=True)
class BaseChoice:
base: Literal["event", "order"]
attribute: str
text: Promise
supports_before: bool
supports_after: bool
@property
def key(self) -> str:
key = f"{self.base}__{self.attribute}"
return key
@staticmethod
def find(objects: Iterable["BaseChoice"], key: str) -> "BaseChoice":
if "__" in key:
choice = next((obj for obj in objects if obj.key == key), None)
else:
# fallback for RelativeDateFields stored, before support for bases other than event was added
choice = next((obj for obj in objects if obj.attribute == key and obj.base == "event"), None)
if choice is None:
raise TypeError(f"key {key} must be a valid key in BASE_CHOICES")
return choice
BASE_CHOICES: List[BaseChoice] = [
BaseChoice('event', 'date_from', _('Event start'), True, True),
BaseChoice('event', 'date_to', _('Event end'), True, True),
BaseChoice('event', 'date_admission', _('Event admission'), True, True),
BaseChoice('event', 'presale_start', _('Presale start'), True, True),
BaseChoice('event', 'presale_end', _('Presale end'), True, True),
BaseChoice('order', 'datetime', _('Order creation'), False, True),
BaseChoice('order', 'expires', _('Order expiry'), True, True),
]
EVENT_BASE_CHOICES = [
x for x in BASE_CHOICES if x.base == 'event'
]
ORDER_BASE_CHOICES = [
x for x in BASE_CHOICES if x.base == 'order'
]
@dataclass(frozen=True)
class RelativeDate:
"""
This contains information on a date that is defined in relation to a fixed base point.
This means that the underlying data is a fixed date as the base point and a number of days or a time interval
to calculate the date.
The list of valid base date choices is defined in BASE_CHOICES.
If the base_date_name is not set, the date_from attribute of Event is used.
"""
days: int = 0
minutes: Optional[int] = None
time: Optional[datetime.time] = None
is_after: bool = False
base_date_name: str = 'event__date_from__'
def __post_init__(self) -> None:
if self.is_after and not self._choice.supports_after:
raise ValueError(
"The selected base date and attribute combination does not support relative dates placed after the base date"
)
if not self.is_after and not self._choice.supports_before:
raise ValueError(
"The selected base date and attribute combination does not support relative dates placed before the base date")
@property
def _choice(self):
return BaseChoice.find(BASE_CHOICES, self.base_date_name)
@property
def key(self):
return self._choice.key
def __eq__(self, o: object) -> bool:
if not isinstance(o, RelativeDate):
return False
return self.to_string() == o.to_string()
def _resolve_date(self, base: "Event | Order | SubEvent") -> Tuple[datetime.datetime, ZoneInfo]:
"""
Resolves the datetime and timezone information of this RelativeDate object in relation to the provided reference.
:param base: the reference which should be used to resolve the relative date
:return:
"""
from .models import Event, Order, SubEvent
choice = self._choice
if choice.base == "order" and isinstance(base, Order):
event = base.event
base_date = getattr(base, choice.attribute)
elif choice.base == "event" and isinstance(base, SubEvent):
event = base.event
base_date = (getattr(base, choice.attribute) or
getattr(base.event, choice.attribute) or
base.date_from)
elif choice.base == "event" and isinstance(base, Event):
event = base
base_date = getattr(base, choice.attribute) or event.date_from
else:
raise TypeError("The base defined by data does not match the passed in base")
tz = ZoneInfo(event.settings.timezone)
return base_date, tz
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
"""
Resolves the effective date of this RelativeDate object in relation to the provided reference.
:param base: the reference which should be used to resolve the date
:return: datetime.date
"""
if self.minutes is not None:
raise ValueError('A minute-based relative datetime can not be used as a date')
base_date, tz = self._resolve_date(base)
if self.is_after:
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.days)
else:
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.days)
return new_date.date()
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
"""
Resolves the effective datetime of this RelativeDate object in relation to the provided reference.
:param base: the reference which should be used to resolve the datetime
:return: datetime.datetime
"""
base_date, tz = self._resolve_date(base)
if self.minutes is not None:
if self.is_after:
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.minutes)
else:
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.minutes)
else:
if self.is_after:
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.days)).astimezone(tz)
else:
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.days)).astimezone(tz)
if self.time:
new_date = new_date.replace(
hour=self.time.hour,
minute=self.time.minute,
second=self.time.second
)
new_date = new_date.astimezone(tz)
return new_date
def to_string(self) -> str:
if self.minutes is not None:
return 'RELDATE/minutes/{}/{}/{}'.format( #
self.minutes,
self._choice.key,
'after' if self.is_after else '',
)
return 'RELDATE/{}/{}/{}/{}'.format( #
self.days,
self.time.strftime('%H:%M:%S') if self.time else '-',
self._choice.key,
'after' if self.is_after else '',
)
@classmethod
def from_string(cls, input: str):
if not input.startswith('RELDATE/'):
raise TypeError("Invalid input for RelativeDate.from_string()")
parts = input.split('/')
if parts[1] == 'minutes':
data = RelativeDate(
days=0,
minutes=int(parts[2]),
base_date_name=parts[3],
time=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
else:
if parts[2] == '-':
time = None
else:
timeparts = parts[2].split(':')
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
try:
data = RelativeDate(
days=int(parts[1] or 0),
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
except ValueError:
data = RelativeDate(
days=0,
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
return data
class RelativeDateWrapper:
@@ -50,130 +265,53 @@ class RelativeDateWrapper:
This contains information on a date that might be relative to an event. This means
that the underlying data is either a fixed date or a number of days and a wall clock
time to calculate the date based on a base point.
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
attribute of an event or subevent. If the respective attribute is not set, ``date_from``
will be used.
"""
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
self.data = data
def date(self, event) -> datetime.date:
from .models import SubEvent
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
"""
If the RelativeDateWrapper wraps a RelativeDate object:
Resolves the effective date of this object in relation to the provided reference.
If the RelativeDateWrapper wraps an absolute date or datetime:
Returns the wrapped absolute date.
:param base: the reference which should be used to resolve the date in case it is a relative date.
:return: datetime.date
"""
if isinstance(self.data, datetime.datetime):
return self.data.date()
elif isinstance(self.data, datetime.date):
return self.data
else:
if self.data.minutes is not None:
raise ValueError('A minute-based relative datetime can not be used as a date')
return self.data.date(base)
tz = ZoneInfo(event.settings.timezone)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
or getattr(event.event, self.data.base_date_name)
or event.date_from
)
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
if self.data.is_after:
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)
else:
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)
return new_date.date()
def datetime(self, event) -> datetime.datetime:
from .models import SubEvent
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
"""
If the RelativeDateWrapper wraps a RelativeDate object:
Resolves the effective datetime of this object in relation to the provided reference.
If the RelativeDateWrapper wraps an absolute date or datetime:
Returns the wrapped absolute datetime.
:param base: the reference which should be used to resolve the datetime in case it is a relative datetime.
:return: datetime.datetime
"""
if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data
else:
tz = ZoneInfo(event.settings.timezone)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
or getattr(event.event, self.data.base_date_name)
or event.date_from
)
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
if self.data.minutes is not None:
if self.data.is_after:
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.data.minutes)
else:
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes)
else:
if self.data.is_after:
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)).astimezone(tz)
else:
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)).astimezone(tz)
if self.data.time:
new_date = new_date.replace(
hour=self.data.time.hour,
minute=self.data.time.minute,
second=self.data.time.second
)
new_date = new_date.astimezone(tz)
return new_date
return self.data.datetime(base)
def to_string(self) -> str:
if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data.isoformat()
else:
if self.data.minutes is not None:
return 'RELDATE/minutes/{}/{}/{}'.format( #
self.data.minutes,
self.data.base_date_name,
'after' if self.data.is_after else '',
)
return 'RELDATE/{}/{}/{}/{}'.format( #
self.data.days,
self.data.time.strftime('%H:%M:%S') if self.data.time else '-',
self.data.base_date_name,
'after' if self.data.is_after else '',
)
return self.data.to_string()
@classmethod
def from_string(cls, input: str):
if input.startswith('RELDATE/'):
parts = input.split('/')
if parts[1] == 'minutes':
data = RelativeDate(
days=0,
minutes=int(parts[2]),
base_date_name=parts[3],
time=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
else:
if parts[2] == '-':
time = None
else:
timeparts = parts[2].split(':')
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
try:
data = RelativeDate(
days=int(parts[1] or 0),
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
except ValueError:
data = RelativeDate(
days=0,
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
data = RelativeDate.from_string(input)
else:
data = parser.parse(input)
return RelativeDateWrapper(data)
@@ -187,7 +325,6 @@ BEFORE_AFTER_CHOICE = (
('after', _('after')),
)
reldatetimeparts = namedtuple('reldatetimeparts', (
"status", # 0
"absolute", # 1
@@ -202,6 +339,14 @@ reldatetimeparts = namedtuple('reldatetimeparts', (
reldatetimeparts.indizes = reldatetimeparts(*range(9))
def _get_choices(base_choices: List[BaseChoice]) -> List[Tuple[str, Promise]]:
return [(c.key, c.text) for c in base_choices]
def _get_choice_validation_obj(choices: List[BaseChoice]):
return {c.key: {"data-supports-before": c.supports_before, "data-supports-after": c.supports_after} for c in choices}
class RelativeDateTimeWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/reldatetime.html'
parts = reldatetimeparts
@@ -209,6 +354,7 @@ class RelativeDateTimeWidget(forms.MultiWidget):
def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices')
base_choices = kwargs.pop('base_choices')
choices = _get_choices(base_choices)
def placeholder_datetime_format():
df = get_format('DATETIME_INPUT_FORMATS')[0]
@@ -220,21 +366,29 @@ class RelativeDateTimeWidget(forms.MultiWidget):
tf = get_format('TIME_INPUT_FORMATS')[0]
return datetime.time(8, 30, 0).strftime(tf)
widgets = reldatetimeparts(
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldatetimeparts(
status=forms.RadioSelect(choices=self.status_choices),
absolute=forms.DateTimeInput(
attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'}
),
rel_days_number=forms.NumberInput(),
rel_mins_relationto=forms.Select(choices=base_choices),
rel_mins_relationto=OptionAttrsSelect(
attrs={'data-relative-choice': True},
choices=choices,
option_attrs=_get_choice_validation_obj(base_choices)
),
rel_days_timeofday=forms.TimeInput(
attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'}
),
rel_mins_number=forms.NumberInput(),
rel_days_relationto=forms.Select(choices=base_choices),
rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
)
rel_days_relationto=OptionAttrsSelect(
attrs={'data-relative-choice': True},
choices=choices,
option_attrs=_get_choice_validation_obj(base_choices)
),
rel_mins_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
rel_days_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
)))
super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value):
@@ -247,10 +401,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="unset",
absolute=None,
rel_days_number=1,
rel_mins_relationto="date_from",
rel_mins_relationto="event__date_from",
rel_days_timeofday=None,
rel_mins_number=0,
rel_days_relationto="date_from",
rel_days_relationto="event__date_from",
rel_mins_relation="before",
rel_days_relation="before"
)
@@ -259,10 +413,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="absolute",
absolute=value.data,
rel_days_number=1,
rel_mins_relationto="date_from",
rel_mins_relationto="event__date_from",
rel_days_timeofday=None,
rel_mins_number=0,
rel_days_relationto="date_from",
rel_days_relationto="event__date_from",
rel_mins_relation="before",
rel_days_relation="before"
)
@@ -271,10 +425,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="relative_minutes",
absolute=None,
rel_days_number=None,
rel_mins_relationto=value.data.base_date_name,
rel_mins_relationto=value.data.key,
rel_days_timeofday=None,
rel_mins_number=value.data.minutes,
rel_days_relationto=value.data.base_date_name,
rel_days_relationto=value.data.key,
rel_mins_relation="after" if value.data.is_after else "before",
rel_days_relation="after" if value.data.is_after else "before"
)
@@ -282,10 +436,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="relative",
absolute=None,
rel_days_number=value.data.days,
rel_mins_relationto=value.data.base_date_name,
rel_mins_relationto=value.data.key,
rel_days_timeofday=value.data.time,
rel_mins_number=0,
rel_days_relationto=value.data.base_date_name,
rel_days_relationto=value.data.key,
rel_mins_relation="after" if value.data.is_after else "before",
rel_days_relation="after" if value.data.is_after else "before"
)
@@ -309,17 +463,41 @@ class RelativeDateTimeField(forms.MultiValueField):
('relative', _('Relative date:')),
('relative_minutes', _('Relative time:')),
]
self.relative_to_order = kwargs.pop('relative_to_order', False)
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
if self.relative_to_order:
possible_choices.extend(ORDER_BASE_CHOICES)
if kwargs.get('limit_choices'):
limit = kwargs.pop('limit_choices')
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
else:
choices = BASE_CHOICES
if any(["__" not in l for l in limit]):
_warn_skips = (str(os.path.dirname(__file__)),)
warnings.warn(
message="Please prefix limit_choices with the base the attributes refer to, for example event__date_from",
category=DeprecationWarning,
skip_file_prefixes=_warn_skips
)
possible_choices = [
c for c in possible_choices if
# new base case as we want limit_choices to be expressed as base__attribute
(c.key in limit) or
# fallback for old event based entries
# if the base is an event, then using only attribute is fine
(c.base == "event" and c.attribute in limit)
]
if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set')))
choices = _get_choices(possible_choices)
fields = reldatetimeparts(
status=forms.ChoiceField(
choices=status_choices,
required=True
required=True,
),
absolute=forms.DateTimeField(
required=False
@@ -329,7 +507,7 @@ class RelativeDateTimeField(forms.MultiValueField):
),
rel_mins_relationto=forms.ChoiceField(
choices=choices,
required=False
required=False,
),
rel_days_timeofday=forms.TimeField(
required=False,
@@ -339,7 +517,7 @@ class RelativeDateTimeField(forms.MultiValueField):
),
rel_days_relationto=forms.ChoiceField(
choices=choices,
required=False
required=False,
),
rel_mins_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE,
@@ -350,8 +528,9 @@ class RelativeDateTimeField(forms.MultiValueField):
required=False
),
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices)
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=possible_choices)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(
@@ -359,21 +538,24 @@ class RelativeDateTimeField(forms.MultiValueField):
)
def set_event(self, event):
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
]
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
]
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
if self.relative_to_order:
possible_choices.extend(ORDER_BASE_CHOICES)
possible_choices = possible_choices
choices = _get_choices(possible_choices)
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = choices
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = choices
def compress(self, data_list):
if not data_list:
return None
data = reldatetimeparts(*data_list)
if data.status == 'absolute':
return RelativeDateWrapper(data.absolute)
elif data.status == 'unset':
if data.status == 'unset':
return None
elif data.status == 'absolute':
return RelativeDateWrapper(data.absolute)
elif data.status == 'relative_minutes':
return RelativeDateWrapper(RelativeDate(
days=0,
@@ -404,6 +586,18 @@ class RelativeDateTimeField(forms.MultiValueField):
raise ValidationError(self.error_messages['incomplete'])
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
raise ValidationError(self.error_messages['incomplete'])
elif data.status == 'relative':
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
if data.rel_days_relation == "before" and not choice.supports_before:
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
elif data.status == 'relative' and data.rel_days_relation == "after" and not choice.supports_after:
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
elif data.status == 'relative_minutes':
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
if data.rel_days_relation == "before" and not choice.supports_before:
raise ValidationError(_('A relative time cannot be expressed as "before" for "{}"'.format(choice.text)))
elif data.rel_days_relation == "after" and not choice.supports_after:
raise ValidationError(_('A relative time cannot be expressed as "after" for "{}"'.format(choice.text)))
return super().clean(value)
@@ -424,15 +618,20 @@ class RelativeDateWidget(RelativeDateTimeWidget):
def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices')
widgets = reldateparts(
self.base_choices = kwargs.pop('base_choices')
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldateparts(
status=forms.RadioSelect(choices=self.status_choices),
absolute=forms.DateInput(
attrs={'class': 'datepickerfield'}
),
rel_days_number=forms.NumberInput(),
rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')),
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
)
rel_days_relationto=OptionAttrsSelect(
choices=self.base_choices,
option_attrs=_get_choice_validation_obj(self.base_choices),
attrs={'data-relative-choice': True},
),
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE, attrs={'data-relation-choice': True},),
)))
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
def decompress(self, value):
@@ -460,7 +659,7 @@ class RelativeDateWidget(RelativeDateTimeWidget):
status="relative",
absolute=None,
rel_days_number=value.data.days,
rel_days_relationto=value.data.base_date_name,
rel_days_relationto=value.data.key,
rel_days_relation="after" if value.data.is_after else "before"
)
@@ -474,6 +673,15 @@ class RelativeDateField(RelativeDateTimeField):
]
if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set')))
self.relative_to_order = kwargs.pop('relative_to_order', False)
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
if self.relative_to_order:
possible_choices.extend(ORDER_BASE_CHOICES)
choices = _get_choices(possible_choices)
fields = reldateparts(
status=forms.ChoiceField(
choices=status_choices,
@@ -486,33 +694,37 @@ class RelativeDateField(RelativeDateTimeField):
required=False
),
rel_days_relationto=forms.ChoiceField(
choices=BASE_CHOICES,
required=False
choices=choices,
required=False,
),
rel_days_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE,
required=False
),
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=possible_choices)
forms.MultiValueField.__init__(
self, fields=fields, require_all_fields=False, *args, **kwargs
)
def set_event(self, event):
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
choices = [
(c.key, c.text) for c in EVENT_BASE_CHOICES if getattr(event, c.attribute, None)
]
if self.relative_to_order:
choices += [(c.key, c.text) for c in ORDER_BASE_CHOICES]
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = choices
def compress(self, data_list):
if not data_list:
return None
data = reldateparts(*data_list)
if data.status == 'absolute':
return RelativeDateWrapper(data.absolute)
elif data.status == 'unset':
if data.status == 'unset':
return None
elif data.status == 'absolute':
return RelativeDateWrapper(data.absolute)
else:
return RelativeDateWrapper(RelativeDate(
days=data.rel_days_number,
@@ -525,8 +737,14 @@ class RelativeDateField(RelativeDateTimeField):
data = reldateparts(*value)
if data.status == 'absolute' and not data.absolute:
raise ValidationError(self.error_messages['incomplete'])
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
raise ValidationError(self.error_messages['incomplete'])
if data.status == 'relative':
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
if data.rel_days_number is None or not data.rel_days_relationto:
raise ValidationError(self.error_messages['incomplete'])
elif data.rel_days_relation == "before" and not choice.supports_before:
raise ValidationError(_("A relative date cannot be expressed as 'before' for '{}'".format(choice.text)))
elif data.rel_days_relation == "after" and not choice.supports_after:
raise ValidationError(_("A relative date cannot be expressed as 'after' for '{}'".format(choice.text)))
return forms.MultiValueField.clean(self, value)
+5
View File
@@ -53,6 +53,7 @@ from django.utils.translation import (
)
from django_scopes import scopes_disabled
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
@@ -916,6 +917,8 @@ class CartManager:
if custom_price > 99_999_999_999:
raise CartError(error_messages['price_too_high'])
custom_price = round_decimal(custom_price, currency=self.event.currency)
op = self.AddOperation(
count=i['count'],
item=item,
@@ -1038,6 +1041,8 @@ class CartManager:
if custom_price > 99_999_999_999:
raise CartError(error_messages['price_too_high'])
custom_price = round_decimal(custom_price, currency=self.event.currency)
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
for ca in current_addons[cp][a['item'], a['variation']]:
if ca.listed_price != listed_price:
+4 -3
View File
@@ -40,7 +40,7 @@ import dateutil
import dateutil.parser
from dateutil.tz import datetime_exists
from django.core.files import File
from django.db import IntegrityError, transaction
from django.db import IntegrityError
from django.db.models import (
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, TextField, Value, When,
@@ -59,6 +59,7 @@ from pretix.base.models import (
)
from pretix.base.signals import checkin_created, periodic_task
from pretix.helpers import OF_SELF
from pretix.helpers.database import conditional_atomic
from pretix.helpers.jsonlogic import Logic
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
from pretix.helpers.jsonlogic_query import (
@@ -1043,10 +1044,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if not simulate:
_save_answers(op, answers, given_answers)
with transaction.atomic():
with conditional_atomic(not simulate):
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
opqs = OrderPosition.all.select_related("order", "item")
if type != Checkin.TYPE_EXIT:
if type != Checkin.TYPE_EXIT and not simulate:
opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk)
+92 -71
View File
@@ -48,12 +48,12 @@ from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db import models, transaction
from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
Value,
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
Sum, Value,
)
from django.db.models.functions import Coalesce, Greatest
from django.db.models.functions import Cast, Greatest
from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
@@ -71,7 +71,7 @@ from pretix.base.models import (
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.event import Event_SettingsStore, SubEvent
from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret,
@@ -1494,83 +1494,104 @@ def send_expiry_warnings(sender, **kwargs):
@scopes_disabled()
def send_download_reminders(sender, **kwargs):
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
qs = Order.objects.annotate(
first_date=Coalesce(
Min('all_positions__subevent__date_from'),
F('event__date_from')
events = Event.objects.filter(
Q(has_subevents=False, date_from__gte=now()) |
(Q(has_subevents=True) & Q(Exists(
SubEvent.objects.filter(event_id=OuterRef('id'), date_from__gte=now())
)))
).annotate(
reminder_days=Subquery(
Event_SettingsStore.objects.filter(
object=OuterRef('id'),
key='mail_days_download_reminder'
).exclude(
value="None"
).annotate(
val=Cast(F("value"), output_field=models.IntegerField()),
).values("val")
)
).filter(
download_reminder_sent=False,
datetime__lte=now() - timedelta(hours=2),
first_date__gte=today,
).only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by('event_id')
event_id = None
days = None
event = None
reminder_days__isnull=False,
).order_by()
for o in qs:
if o.event_id != event_id:
days = o.event.settings.get('mail_days_download_reminder', as_type=int)
event = o.event
event_id = o.event_id
for event in events.iterator(chunk_size=10_000):
qs = event.orders.filter(
download_reminder_sent=False,
datetime__lte=now() - timedelta(hours=2),
)
if days is None:
continue
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
if now() < reminder_date or o.datetime > reminder_date:
continue
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
if event.has_subevents:
qs = qs.annotate(
first_date=Min('all_positions__subevent__date_from')
).filter(
Q(first_date__gte=today)
)
else:
event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
if now() < event_reminder_date:
continue
if not o.ticket_download_available:
qs = qs.only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by()
for o in qs:
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
if event.has_subevents:
reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
else:
reminder_date = event_reminder_date
if now() < reminder_date or o.datetime > reminder_date:
continue
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
continue
with language(o.locale, o.event.settings.region):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
if not o.ticket_download_available:
continue
if event.settings.mail_send_download_reminder_attendee:
for p in positions:
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0
)
if now() < reminder_date:
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
continue
with language(o.locale, o.event.settings.region):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
if event.settings.mail_send_download_reminder_attendee:
for p in positions:
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=event.reminder_days)).replace(
hour=0, minute=0, second=0, microsecond=0
)
if now() < reminder_date:
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
+11 -3
View File
@@ -801,11 +801,10 @@ def get_available_placeholders(event, base_parameters, rich=False):
return params
def get_sample_context(event, context_parameters, rich=True):
def prepare_sample_context_for_preview(placeholder_to_sample):
context_dict = {}
lbl = _('This value will be replaced based on dynamic parameters.')
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
sample = v.render_sample(event)
for k, sample in placeholder_to_sample.items():
if isinstance(sample, PlainHtmlAlternativeString):
context_dict[k] = PlainHtmlAlternativeString(
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
@@ -830,3 +829,12 @@ def get_sample_context(event, context_parameters, rich=True):
escape(sample)
))
return context_dict
def get_sample_context(event, context_parameters, rich=True):
return prepare_sample_context_for_preview(
{
k: v.render_sample(event)
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items()
}
)
+1 -1
View File
@@ -2930,7 +2930,7 @@ Your {event} team""")) # noqa: W291
},
'mail_days_download_reminder': {
'type': int,
'default': None
'default': None # when this default is changed, send_download_reminders needs to change
},
'mail_send_download_reminder_attendee': {
'type': bool,
+6 -6
View File
@@ -94,14 +94,14 @@ class SubEventBulkForm(SubEventForm):
label=_('Start of presale'),
help_text=_('Optional. No products will be sold before this date.'),
required=False,
limit_choices=('date_from', 'date_to'),
limit_choices=('event__date_from', 'event__date_to'),
)
rel_presale_end = RelativeDateTimeField(
label=_('End of presale'),
help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
'will end after the end date of your event.'),
required=False,
limit_choices=('date_from', 'date_to'),
limit_choices=('event__date_from', 'event__date_to'),
)
skip_if_overlap = forms.BooleanField(
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
@@ -333,12 +333,12 @@ class BulkSubEventItemForm(SubEventItemForm):
rel_available_from = RelativeDateTimeField(
label=_('Available from'),
required=False,
limit_choices=('date_from', 'date_to'),
limit_choices=('event__date_from', 'event__date_to'),
)
rel_available_until = RelativeDateTimeField(
label=_('Available until'),
required=False,
limit_choices=('date_from', 'date_to'),
limit_choices=('event__date_from', 'event__date_to'),
)
def __init__(self, *args, **kwargs):
@@ -355,12 +355,12 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm):
rel_available_from = RelativeDateTimeField(
label=_('Available from'),
required=False,
limit_choices=('date_from', 'date_to'),
limit_choices=('event__date_from', 'event__date_to'),
)
rel_available_until = RelativeDateTimeField(
label=_('Available_until'),
required=False,
limit_choices=('date_from', 'date_to'),
limit_choices=('event__date_from', 'event__date_to'),
)
def __init__(self, *args, **kwargs):
@@ -58,6 +58,7 @@
<script type="text/javascript" src="{% static "leaflet/leaflet.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/geo.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/reldate.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script>
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
@@ -118,6 +118,10 @@
</td>
{% endif %}
<td>
<span class="fa fa-fw {% if d.last_seen and d.last_seen.is_recent %}fa-check-circle text-success{% else %}fa-circle text-danger{% endif %}"
data-toggle="tooltip"
title="{% if d.last_seen %}{% blocktrans with time=d.last_seen.last_seen|date:"SHORT_DATETIME_FORMAT" %}Last seen: {{ time }}{% endblocktrans %}{% else %}{% trans "No recent contact" %}{% endif %}"
></span>
{{ d.device_id }}
</td>
<td>
@@ -125,6 +129,7 @@
<del>{% endif %}
{{ d.name }}
{% if d.revoked %}</del>{% endif %}
{% if d.gate %}
<br>
<small class="text-muted">{{ d.gate.name }}</small>
+2 -2
View File
@@ -269,8 +269,8 @@ class MailSettingsSetupView(TemplateView):
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
dmarc_record = get_dmarc_record(hostname)
if not dmarc_record:
spf_warning = _(
'We did not find DMARC record for your domain. This means that there is a very high chance '
dmarc_warning = _(
'We did not find a DMARC record for your domain. This means that there is a very high chance '
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
'of your domain.'
)
+1 -1
View File
@@ -382,7 +382,7 @@ class OrderOverpaidRefundBulkActionView(BaseOrderBulkActionView):
'provider': refund.provider,
}, user=self.request.user)
payment.payment_provider.execute_refund(refund)
return True
return bool(proposals)
except (ValueError, PaymentException):
return False
+9 -11
View File
@@ -108,6 +108,9 @@ from pretix.base.services.export import (
init_organizer_exporters, multiexport, scheduled_organizer_export,
)
from pretix.base.services.mail import mail, prefix_subject
from pretix.base.services.placeholders import (
prepare_sample_context_for_preview,
)
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.exports import ScheduledOrganizerExportForm
@@ -345,16 +348,11 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
# get all supported placeholders with dummy values
def placeholders(self, item):
ctx = {}
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(
MailSettingsForm.base_context[item]).items():
if s.strip().startswith('*'):
ctx[p] = s
else:
ctx[p] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
s
)
ctx = prepare_sample_context_for_preview(
MailSettingsForm(obj=self.request.organizer)._get_sample_context(
MailSettingsForm.base_context[item]
)
)
return self.SafeDict(ctx)
def post(self, request, *args, **kwargs):
@@ -1210,7 +1208,7 @@ class DeviceQueryMixin:
def get_queryset(self):
qs = self.request.organizer.devices.prefetch_related(
'limit_events', 'gate',
).order_by('revoked', '-device_id')
).select_related('last_seen').order_by('revoked', '-device_id')
if 'device' in self.request_data and '__ALL' not in self.request_data:
qs = qs.filter(
+1 -1
View File
@@ -431,7 +431,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
days = (self.copy_from.date_from.astimezone(tz).date() - value.astimezone(tz).date()).days
return RelativeDateWrapper(RelativeDate(
days=abs(days),
base_date_name='date_from',
base_date_name='event__date_from',
time=value.astimezone(tz).time(),
minutes=None,
is_after=days < 0,
+28
View File
@@ -22,6 +22,7 @@
import contextlib
from django.conf import settings
from django.contrib.postgres.indexes import BrinIndex
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db import connection, transaction
from django.db.models import (
@@ -285,3 +286,30 @@ def get_deterministic_ordering(model, ordering):
# on the primary key to provide total ordering.
ordering.append("-pk")
return ordering
@contextlib.contextmanager
def conditional_atomic(do_atomic, **kwargs):
if do_atomic:
with transaction.atomic(**kwargs):
yield
else:
yield
class IgnoreOnSQLiteMixin:
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
# on SQLite. SQLite is supported for testing only anyways!
def create_sql(self, model, schema_editor, *args, **kwargs):
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
return ""
return super().create_sql(model, schema_editor, *args, **kwargs)
def remove_sql(self, model, schema_editor, **kwargs):
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
return ""
return super().remove_sql(model, schema_editor, **kwargs)
class BrinIndexIgnoredOnSQLite(IgnoreOnSQLiteMixin, BrinIndex):
pass
+133 -211
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
"PO-Revision-Date: 2026-07-27 17:00+0000\n"
"PO-Revision-Date: 2026-07-31 18:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
">\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
"da/>\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -1381,10 +1381,8 @@ msgid "Membership type"
msgstr "Medlemskabstype"
#: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "Purchase time"
msgid "Purchase ticket"
msgstr "Købsdato"
msgstr "Køb billet"
#: pretix/base/exporters/customers.py pretix/base/exporters/orderlist.py
#: pretix/base/exporters/waitinglist.py pretix/base/forms/questions.py
@@ -1401,10 +1399,8 @@ msgid "Start date"
msgstr "Starttidspunkt"
#: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "Start time from"
msgid "Start time"
msgstr "Starttidspunkt fra"
msgstr "Starttidspunkt"
#: pretix/base/exporters/customers.py pretix/base/exporters/orderlist.py
#: pretix/base/exporters/waitinglist.py pretix/base/models/memberships.py
@@ -1417,10 +1413,8 @@ msgid "End date"
msgstr "Sluttidspunkt"
#: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "End: %(time)s"
msgid "End time"
msgstr "Slut: %(time)s"
msgstr "Sluttidspunkt"
#: pretix/base/exporters/dekodi.py pretix/base/exporters/invoices.py
msgctxt "export_category"
@@ -4641,16 +4635,12 @@ msgid "This event is remote or partially remote."
msgstr "Dette arrangement afholdes online eller delvist online."
#: pretix/base/models/event.py
#, fuzzy
#| msgid ""
#| "This will be used to let users know if the event is in a different "
#| "timezone and lets us calculate users local times."
msgid ""
"This will be used to let users know if the event is in a different timezone, "
"and to let us calculate the local time of a user."
msgstr ""
"Dette vil blive brugt til at informere brugerne om, hvorvidt begivenheden "
"finder sted i en anden tidszone, og til at beregne brugernes lokale tid."
"finder sted i en anden tidszone, og til at beregne brugerens lokale tid."
#: pretix/base/models/event.py pretix/base/models/organizer.py
#: pretix/control/navigation.py
@@ -5472,14 +5462,6 @@ msgid "Reusable media policy"
msgstr "Politik for genanvendelige medier"
#: pretix/base/models/items.py
#, fuzzy
#| msgid ""
#| "If this product should be stored on a re-usable physical medium, you can "
#| "attach a physical media policy. This is not required for regular tickets, "
#| "which just use a one-time barcode, but only for products like renewable "
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
#| "feature that also requires specific configuration of ticketing and "
#| "printing settings."
msgid ""
"If this product should be stored on a reusable physical medium, you can "
"attach a physical media policy. This is not required for regular tickets, "
@@ -5488,12 +5470,12 @@ msgid ""
"feature that also requires specific configuration of ticketing and printing "
"settings."
msgstr ""
"Hvis dette produkt skal opbevares på et genanvendeligt fysisk medie, kan du "
"vedhæfte en politik for fysiske medier. Dette er ikke nødvendigt for "
"almindelige billetter, som bare bruger en engangs-stregkode, men kun for "
"produkter som f.eks. fornyelige sæsonkort eller genopladelige gavekort-"
"armbånd. Dette er en avanceret funktion, som også kræver en specifik "
"konfiguration af billet- og printindstillinger."
"Hvis dette produkt skal opbevares på et genanvendeligt fysisk medium, kan du "
"knytte en politik for fysiske medier til det. Dette er ikke påkrævet for "
"almindelige billetter, som blot bruger en engangsstregkode, men gælder kun "
"for produkter som sæsonkort, der kan fornyes, eller gavekortarmbånd, som kan "
"blive genopladet. Dette er en avanceret funktion, der også kræver en "
"specifik konfiguration af billet- og udskrivningsindstillingerne."
#: pretix/base/models/items.py
msgid "Reusable media type"
@@ -5537,6 +5519,9 @@ msgid ""
"prior to their usage. Therefore, the selected media policy does not make "
"sense for this media type."
msgstr ""
"Den valgte medietype kræver, at alle medier registreres i systemet, inden de "
"tages i brug. Derfor giver den valgte mediepolitik ikke mening for denne "
"medietype."
#: pretix/base/models/items.py
msgid ""
@@ -6090,18 +6075,16 @@ msgstr "afvist"
#: pretix/base/models/media.py
msgctxt "reusable_medium"
msgid "Claim token"
msgstr ""
msgstr "Hent token"
#: pretix/base/models/media.py
msgctxt "reusable_medium"
msgid "Label"
msgstr ""
msgstr "Etiket"
#: pretix/base/models/media.py
#, fuzzy
#| msgid "Linked ticket"
msgid "Linked tickets"
msgstr "Forbundet billet"
msgstr "Sammenkædede billetter"
#: pretix/base/models/media.py
msgid ""
@@ -6109,6 +6092,9 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Hvis du linker til mere end én billet, skal du sikre dig, at der ikke er "
"nogen overlapning i gyldighedsperioden. Hvis flere billetter er gyldige på "
"samme tid, vil det medføre, at check-in mislykkes."
#: pretix/base/models/memberships.py
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html
@@ -7568,6 +7554,8 @@ msgid ""
"This payment provider exists for historical purposes only and is no longer "
"usable."
msgstr ""
"Denne betalingsudbyder findes udelukkende af historiske årsager og kan ikke "
"længere benyttes."
#: pretix/base/pdf.py
msgid "Ticket code (barcode content)"
@@ -7802,15 +7790,12 @@ msgid "Atlantis"
msgstr "Eksempelland"
#: pretix/base/pdf.py
#, fuzzy
msgid "Invoice custom recipient field"
msgstr "Fakturamodtager:"
msgstr "Brugerdefineret modtagerfelt på fakturaen"
#: pretix/base/pdf.py
#, fuzzy
#| msgid "Custom recipient field label"
msgid "Custom recipient field"
msgstr "Brugerdefineret etiket til modtagerfelt"
msgstr "Brugerdefineret modtagerfelt"
#: pretix/base/pdf.py
msgid "List of Add-Ons"
@@ -8252,7 +8237,7 @@ msgstr "Arrangement aflyst"
#: pretix/base/services/cancelevent.py
msgid "Confirm event cancellation and bulk refund"
msgstr ""
msgstr "Bekræft aflysning af begivenhed og samlet refusion"
#: pretix/base/services/cart.py pretix/base/services/modelimport.py
#: pretix/base/services/orders.py
@@ -8781,13 +8766,15 @@ msgstr "Du skal svare på spørgsmål for at gennemføre dette tjek-ind."
#: pretix/base/services/checkin.py
msgid "Ticket needs to be exchanged to a suitable medium."
msgstr ""
msgstr "Billetten skal ombyttes til et passende medium."
#: pretix/base/services/checkin.py
msgid ""
"This ticket has already been exchanged for a reusable medium that now needs "
"to be used instead."
msgstr ""
"Denne billet er allerede blevet ombyttet til et genanvendeligt medium, som "
"nu skal bruges i stedet."
#: pretix/base/services/checkin.py
msgid "This ticket has already been redeemed."
@@ -8818,9 +8805,8 @@ msgid "Your export did not contain any data."
msgstr "Din eksport indeholdt ingen data."
#: pretix/base/services/export.py
#, fuzzy
msgid "Scheduled export failed"
msgstr "Start eksport"
msgstr "Den planlagte eksport mislykkedes"
#: pretix/base/services/export.py
msgid "Permission denied."
@@ -8947,60 +8933,44 @@ msgid "You are receiving this email because you placed an order for {event}."
msgstr "Du modtager denne e-mail, fordi du har afgivet en ordre til {event}."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "Invalid input type."
msgid "Invalid medium type."
msgstr "Ugyldig indtastning."
msgstr "Ugyldig medietype."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid "Medium type is not enabled for organizer."
msgstr "Den valgte medietype er ikke aktiveret i dine arrangør-indstillinger."
msgstr "Medie-typen er ikke aktiveret for arrangøren."
#: pretix/base/services/media.py
msgid "Incorrect medium type for product."
msgstr ""
msgstr "Forkert medietype for produktet."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "Ticket is already exchanged for reusable medium."
msgstr "Denne billet er allerede blevet indløst."
msgstr "Billetten er allerede ombyttet til et genanvendeligt medium."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "Reusable Medium ID"
msgid "Reusable medium not found."
msgstr "ID for genanvendeligt medie"
msgstr "Genanvendeligt medie blev ikke fundet."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium is inactive or expired."
msgstr "Det genanvendelige medie er blevet oprettet."
msgstr "Det genanvendelige medium er inaktivt eller udløbet."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium not found and could not be created."
msgstr "Det genanvendelige medie er blevet oprettet."
msgstr "Genanvendeligt medie blev ikke fundet og kunne ikke oprettes."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists."
msgstr "Genanvendelig medietype"
msgstr "Det genanvendelige medie findes allerede."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium could not be created."
msgstr "Det genanvendelige medie er blevet oprettet."
msgstr "Det var ikke muligt at oprette det genanvendelige medie."
#: pretix/base/services/media.py
msgid "Product does not support medium exchange."
msgstr ""
msgstr "Produktet understøtter ikke udskiftning af medier."
#: pretix/base/services/memberships.py
#, python-brace-format
@@ -9164,6 +9134,7 @@ msgstr "En voucher kan ikke oprettes uden kode."
msgid ""
"Voucher codes must be unique. Code \"{code}\" already exists in this import."
msgstr ""
"Kuponkoder skal være unikke. Koden \"{code}\" findes allerede i denne import."
#: pretix/base/services/modelimport.py
#, python-brace-format
@@ -9172,13 +9143,19 @@ msgid ""
msgid_plural ""
"Voucher codes must be unique. Import contains existing voucher codes {code}."
msgstr[0] ""
"Rabatkoder skal være unikke. Importen indeholder den eksisterende rabatkode "
"{code}."
msgstr[1] ""
"Rabatkoder skal være unikke. Importen indeholder de eksisterende rabatkoder "
"{code}."
#: pretix/base/services/modelimport.py
msgid ""
"Vouchers could not be imported, probably due to a voucher code already being "
"in use."
msgstr ""
"Rabatkoder kunne ikke importeres, sandsynligvis fordi en kuponkode allerede "
"var i brug."
#: pretix/base/services/orders.py
msgid ""
@@ -9462,16 +9439,12 @@ msgstr ""
"gavekort."
#: pretix/base/services/orders.py
#, fuzzy
#| msgid ""
#| "You cannot change the price of a position that has been used to issue a "
#| "gift card."
msgid ""
"You cannot change the ticket secret of a position that has been used to "
"issue a gift card."
msgstr ""
"Du kan ikke ændre prisen af en post, som er blevet brugt til at udstede et "
"gavekort."
"Du kan ikke ændre billetkoden for en position, der er blevet brugt til at "
"udstede et gavekort."
#: pretix/base/services/orders.py
#, python-brace-format
@@ -9558,10 +9531,9 @@ msgid "Something happened in your event after the export, please try again."
msgstr "Der skete noget i din begivenhed efter eksporten, prøv venligst igen."
#: pretix/base/services/shredder.py
#, fuzzy, python-format
#| msgid "Data shredding completed"
#, python-format
msgid "Data shredding completed for %(event)s"
msgstr "Makulering af data afsluttet"
msgstr "Datadestruktion for %(event)s er afsluttet"
#: pretix/base/services/stats.py
msgid "Uncategorized"
@@ -9738,29 +9710,24 @@ msgstr ""
"ind under købet."
#: pretix/base/settings.py
#, fuzzy
#| msgid "Activate re-usable media"
msgid "Activate reusable media"
msgstr "Aktiver genanvendelige medier"
#: pretix/base/settings.py
#, fuzzy
#| msgid ""
#| "The re-usable media feature allows you to connect tickets and gift cards "
#| "with physical media such as wristbands or chip cards that may be re-used "
#| "for different tickets or gift cards later."
msgid ""
"The reusable media feature allows you to connect tickets and gift cards with "
"physical media such as wristbands or chip cards that may be reused for "
"different tickets or gift cards later."
msgstr ""
"Funktionen \"Genanvendelige medier\" gør det muligt at forbinde billetter og "
"voucher med fysiske medier som armbånd eller chipkort, der kan genbruges til "
"andre billetter eller voucher senere."
"Med funktionen \"Genanvendelige medier\" kan du knytte billetter og gavekort "
"til fysiske medier som armbånd eller chipkort, som senere kan genbruges "
"til andre billetter eller gavekort."
#: pretix/base/settings.py
msgid "Enforce the usage of issued reusable media for check-in"
msgstr ""
"Gennemtving, at der udelukkende anvendes de udleverede genanvendelige medier "
"ved check-in"
#: pretix/base/settings.py
msgid ""
@@ -9768,6 +9735,10 @@ msgid ""
"medium has been created and linked to a ticket. Keeping this option turned "
"off will treat the reusable medium and ticket as equals."
msgstr ""
"Hvis denne indstilling er aktiveret, accepteres en billetstregkode ikke "
"længere, hvis der er oprettet et genanvendeligt medie, som er knyttet til en "
"billet. Hvis denne indstilling forbliver deaktiveret, behandles det "
"genanvendelige medie og billetten på samme måde."
#: pretix/base/settings.py
msgid "Length of barcodes"
@@ -11336,10 +11307,8 @@ msgid "We'll show this publicly to allow attendees to contact you."
msgstr "Vi vil vise dette offentligt, så deltagerne kan kontakte dig."
#: pretix/base/settings.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "Kontakt"
msgstr "Kontakt-URL"
#: pretix/base/settings.py
msgid ""
@@ -11347,6 +11316,9 @@ msgid ""
"the email address above. Please note that you still need to add a contact "
"email address that will be shared with all emails you send."
msgstr ""
"Hvis du indstiller dette, vil kontaktlinket i sidefoden pege hertil i stedet "
"for at bruge ovenstående e-mailadresse. Bemærk, at du stadig skal tilføje en "
"kontakt-e-mailadresse, som vil blive angivet i alle de e-mails, du sender."
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -13213,30 +13185,19 @@ msgstr ""
"os."
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#, fuzzy
#| msgid ""
#| "You have requested us to cancel an event which includes a larger bulk-"
#| "refund:"
msgid "You requested to cancel an event that involves a large bulk refund:"
msgstr ""
"Du har bedt os om at aflyse et arrangement, der indebærer en større samlet "
"Du har anmodet om at annullere en begivenhed, der medfører en stor samlet "
"refusion:"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#, fuzzy
#| msgid "Estimated refund amount"
msgid "Estimated refund"
msgstr "Anslået refusionsbeløb"
msgstr "Anslået tilbagebetaling"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#, fuzzy
#| msgid ""
#| "Please confirm that you want to proceed by coping the following "
#| "confirmation code into the cancellation form:"
msgid "To confirm, paste the following code into the cancellation form:"
msgstr ""
"Bekræft venligst, at du ønsker at fortsætte ved at indsætte følgende "
"bekræftelseskode i afmeldingsformularen:"
"For at bekræfte skal du indsætte følgende kode i afbestillingsformularen:"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#, python-format
@@ -13244,6 +13205,8 @@ msgid ""
"Don't share this code with anyone. The %(instance)s team will never ask you "
"for it."
msgstr ""
"Del ikke denne kode med nogen. %(instance)s-teamet vil aldrig bede dig om "
"den."
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#: pretix/base/templates/pretixbase/email/export_failed.txt
@@ -13252,6 +13215,8 @@ msgid ""
"Thanks, \n"
"The %(instance)s Team"
msgstr ""
"Tak, \n"
"%(instance)s-teamet"
#: pretix/base/templates/pretixbase/email/email_footer.html
#, python-format
@@ -13259,51 +13224,38 @@ msgid "powered by <a %(a_attr)s>pretix</a>"
msgstr "drevet af <a %(a_attr)s>pretix</a>"
#: pretix/base/templates/pretixbase/email/export_failed.txt
#, fuzzy
#| msgid "Your export failed."
msgid "Your scheduled export failed."
msgstr "Din eksport mislykkedes."
msgstr "Din planlagte eksport mislykkedes."
#: pretix/base/templates/pretixbase/email/export_failed.txt
#: pretix/control/templates/pretixcontrol/event/tax_edit.html
#, fuzzy
msgid "Reason"
msgstr "Tilbagebetal bestilling"
msgstr "Begrundelse"
#: pretix/base/templates/pretixbase/email/export_failed.txt
#, fuzzy
#| msgid "If your export fails five times in a row, it will no longer be sent."
msgid "If an export fails five times in a row, we'll stop sending it."
msgstr ""
"Hvis din eksport mislykkes fem gange i træk, vil den ikke længere blive "
"sendt."
"Hvis en eksport mislykkes fem gange i træk, vil vi stoppe med at sende den."
#: pretix/base/templates/pretixbase/email/export_failed.txt
#, fuzzy
#| msgid "You can request to cancel this order."
msgid "You can adjust or remove this export here:"
msgstr "Du kan anmode om at annullere denne bestilling."
msgstr "Du kan ændre eller fjerne denne eksport her:"
#: pretix/base/templates/pretixbase/email/notification.html
#: pretix/base/templates/pretixbase/email/notification.txt
#, fuzzy
#| msgid "You receive these emails based on your notification settings."
msgid "You're receiving this email based on your notification settings."
msgstr ""
"Du modtager disse e-mails i henhold til dine indstillinger for "
"notifikationer."
"Du modtager denne e-mail i henhold til dine indstillinger for meddelelser."
#: pretix/base/templates/pretixbase/email/notification.html
#: pretix/base/templates/pretixbase/email/notification.txt
#, fuzzy
msgid "Manage settings"
msgstr "Basisindstillinger"
msgstr "Administrer indstillinger"
#: pretix/base/templates/pretixbase/email/notification.html
#: pretix/base/templates/pretixbase/email/notification.txt
#, fuzzy
msgid "Disable all notifications"
msgstr "E-mailnotifikationer"
msgstr "Deaktiver alle notifikationer"
#: pretix/base/templates/pretixbase/email/order_details.html
msgid ""
@@ -13362,25 +13314,7 @@ msgid "Contact"
msgstr "Kontakt"
#: pretix/base/templates/pretixbase/email/shred_completed.txt
#, fuzzy, python-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we hereby confirm that the following data shredding job has been "
#| "completed:\n"
#| "\n"
#| "Organizer: %(organizer)s\n"
#| "\n"
#| "Event: %(event)s\n"
#| "\n"
#| "Data selection: %(shredders)s\n"
#| "\n"
#| "Start time: %(start_time)s (new data added after this time might not have "
#| "been deleted)\n"
#| "\n"
#| "Best regards,\n"
#| "\n"
#| "Your %(instance)s team\n"
#, python-format
msgid ""
"Hello,\n"
"\n"
@@ -13398,20 +13332,19 @@ msgid ""
msgstr ""
"Hej,\n"
"\n"
"vi bekræfter hermed, at følgende datadestruktionsopgave er afsluttet:\n"
"Følgende datadestruktionsopgave er afsluttet:\n"
"\n"
"Arrangør: %(organizer)s\n"
"- Arrangør: %(organizer)s\n"
"- Begivenhed: %(event)s\n"
"- Dataudvælgelse: %(shredders)s\n"
"- Starttidspunkt: %(start_time)s\n"
"\n"
"Begivenhed: %(event)s\n"
"Data, der er tilføjet til begivenheden efter starttidspunktet, er muligvis "
"ikke blevet slettet.\n"
"\n"
"Dataudvælgelse: %(shredders)s\n"
"Med venlig hilsen, \n"
"\n"
"Starttidspunkt: %(start_time)s (nye data tilføjet efter dette tidspunkt er "
"muligvis ikke blevet slettet)\n"
"\n"
"Med venlig hilsen,\n"
"\n"
"Dit %(instance)s-team\n"
"%(instance)s-teamet\n"
#: pretix/base/templates/pretixbase/forms/widgets/checkbox_sales_channel_option.html
msgid ""
@@ -13443,22 +13376,23 @@ msgstr "%(number)s dage %(relation)s %(relation_to)s på %(time_of_day)s"
#: pretix/base/templates/pretixbase/framebreak.html
#: pretix/presale/templates/pretixpresale/event/cookies.html
msgid "Please continue in a new tab"
msgstr ""
msgstr "Fortsæt venligst i en ny fane"
#: pretix/base/templates/pretixbase/framebreak.html
msgid "For security reasons, the following step is only possible in a new tab."
msgstr ""
"Af sikkerhedsmæssige årsager kan det følgende trin kun udføres i en ny fane."
#: pretix/base/templates/pretixbase/framebreak.html
msgid ""
"If the new tab did not open automatically, please click the following button:"
msgstr ""
"Hvis den nye fane ikke åbnede automatisk, skal du klikke på følgende knap:"
#: pretix/base/templates/pretixbase/framebreak.html
#: pretix/presale/templates/pretixpresale/event/cookies.html
#, fuzzy
msgid "Continue in new tab"
msgstr "Opret gruppe"
msgstr "Fortsæt i en ny fane"
#: pretix/base/templates/pretixbase/redirect.html
msgid "Redirect"
@@ -15184,13 +15118,11 @@ msgstr "Søg efter e-mailadresse eller emne"
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/refunds.html
msgid "Source"
msgstr ""
msgstr "Kilde"
#: pretix/control/forms/filter.py
#, fuzzy
#| msgid "All vouchers"
msgid "All sources"
msgstr "Alle vouchere"
msgstr "Alle kilder"
#: pretix/control/forms/filter.py
msgid "Team actions"
@@ -15201,21 +15133,16 @@ msgid "Customer actions"
msgstr "Kundehandlinger"
#: pretix/control/forms/filter.py
#, fuzzy
#| msgid "Device status"
msgid "Device actions"
msgstr "Enhedsstatus"
msgstr "Enhedshandlinger"
#: pretix/control/forms/filter.py
#, fuzzy
#| msgid "Order email"
msgid "User email"
msgstr "Bestillings-e-mail"
msgstr "Brugerens e-mailadresse"
#: pretix/control/forms/filter.py pretix/control/navigation.py
#, fuzzy
msgid "All users"
msgstr "Alle vouchere"
msgstr "Alle brugere"
#: pretix/control/forms/global_settings.py
msgid "Additional footer text"
@@ -16724,40 +16651,34 @@ msgid ""
"because at least one of the selected vouchers has already been redeemed "
"%(max_redeemed)s times."
msgstr ""
"Du kan ikke reducere det maksimale antal indløsninger til %(max_usages)s, da "
"mindst én af de valgte kuponer allerede er blevet indløst %(max_redeemed)s "
"gange."
#: pretix/control/forms/vouchers.py
#, fuzzy
#| msgid ""
#| "You cannot create a voucher that blocks quota as the selected product or "
#| "quota is currently sold out or completely reserved."
msgid ""
"You cannot create a voucher that allows selection of a quota but has no date "
"selected."
msgstr ""
"Du kan ikke oprette en rabatkode der blokerer en kvote idet det valgte "
"produkt eller kvote pt. er udsolgt eller fuldt reserveret."
"Du kan ikke oprette en kupon, hvor det er muligt at vælge en kvote, men hvor "
"der ikke er valgt nogen dato."
#: pretix/control/forms/vouchers.py
#, fuzzy
#| msgid "The selected product does not allow to select a seat."
msgid "The selected quota does not match the selected subevent."
msgstr "Det valgte produkt tillader ikke, at du vælger en plads."
msgstr "Den valgte kvote stemmer ikke overens med den valgte underbegivenhed."
#: pretix/control/forms/vouchers.py
#, fuzzy
#| msgid ""
#| "There is not enough quota available on quota \"{}\" to perform the "
#| "operation."
msgid "There is no sufficient quota available to perform this change."
msgstr ""
"Der er ikke enheder nok tilbage på kvoten \"{}\" til at udføre denne "
"operation."
"Der er ikke tilstrækkelig kvote til rådighed til at gennemføre denne ændring."
#: pretix/control/forms/vouchers.py
msgid ""
"Changing the maximum number of usages in bulk is not supported if any of the "
"selected vouchers is assigned a seat."
msgstr ""
"Det er ikke muligt at ændre det maksimale antal anvendelser samlet, hvis en "
"af de valgte kuponer er tildelt en plads."
#: pretix/control/forms/vouchers.py
msgctxt "subevent"
@@ -16765,18 +16686,24 @@ msgid ""
"Changing the date in bulk is not supported if any of the selected vouchers "
"is assigned a seat."
msgstr ""
"Det er ikke muligt at ændre datoen for flere billetter på én gang, hvis en "
"af de valgte billetter er tildelt en plads."
#: pretix/control/forms/vouchers.py
msgid ""
"Changing the product to a quota is not supported if any of the selected "
"vouchers is assigned a seat."
msgstr ""
"Det er ikke muligt at ændre produktet til en kvote, hvis en af de valgte "
"kuponer er tildelt en plads."
#: pretix/control/forms/vouchers.py
msgid ""
"This change cannot be completed because not all assigned seats of the "
"vouchers are still available"
msgstr ""
"Denne ændring kan ikke gennemføres, da ikke alle de tildelte pladser på "
"billetterne stadig er ledige"
#: pretix/control/forms/vouchers.py
msgid "Codes"
@@ -17897,16 +17824,12 @@ msgid "The reusable medium has been changed."
msgstr "Det genanvendelige medie er blevet udskiftet."
#: pretix/control/logdisplay.py
#, fuzzy
#| msgid "The new member has been added to the team."
msgid "A new ticket has been added to the medium."
msgstr "Det nye medlem er blevet føjet til gruppen."
msgstr "Der er blevet tilføjet en ny billet til mediet."
#: pretix/control/logdisplay.py
#, fuzzy
#| msgid "{user} has been removed from the team."
msgid "A ticket has been removed from the medium."
msgstr "{user} er fjernet fra gruppen."
msgstr "En billet er blevet fjernet fra mediet."
#: pretix/control/logdisplay.py
msgid "The medium has been connected to a new ticket."
@@ -17918,6 +17841,8 @@ msgid ""
"The ticket #{positionid} was exchanged for reusable medium "
"{medium_identifier}."
msgstr ""
"Billetten #{positionid} blev ombyttet til et genanvendeligt medie "
"{medium_identifier}."
#: pretix/control/logdisplay.py
msgid "The medium has been connected to a new gift card."
@@ -18328,14 +18253,14 @@ msgid "Payment {local_id} has been confirmed."
msgstr "Betalingen {local_id} er blevet bekræftet."
#: pretix/control/logdisplay.py
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Payment {local_id} has been canceled."
msgstr "Bestillingen er blevet annulleret."
msgstr "Betalingen {local_id} er blevet annulleret."
#: pretix/control/logdisplay.py
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Canceling payment {local_id} has failed."
msgstr "Bestillingen er blevet annulleret."
msgstr "Det lykkedes ikke at annullere betalingen {local_id}."
#: pretix/control/logdisplay.py
#, fuzzy, python-brace-format
@@ -21795,54 +21720,51 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "January"
msgstr ""
msgstr "januar"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "February"
msgstr ""
msgstr "februar"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
#, fuzzy
msgid "March"
msgstr "Marts"
msgstr "marts"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "April"
msgstr ""
msgstr "april"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
#, fuzzy
#| msgid "Day"
msgid "May"
msgstr "Dag"
msgstr "maj"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "June"
msgstr ""
msgstr "juni"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "July"
msgstr ""
msgstr "juli"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "August"
msgstr ""
msgstr "august"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "September"
msgstr "September"
msgstr "september"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "October"
msgstr ""
msgstr "oktober"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "November"
msgstr "November"
msgstr "november"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "December"
msgstr "December"
msgstr "december"
#: pretix/control/templates/pretixcontrol/global_sysreport.html
#, fuzzy
+14 -14
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-04-22 18:00+0000\n"
"PO-Revision-Date: 2026-07-31 18:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
"da/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.17\n"
"X-Generator: Weblate 2026.7.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -1306,62 +1306,62 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "January"
msgstr "Januar"
msgstr "januar"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "February"
msgstr "Februar"
msgstr "februar"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "March"
msgstr "Marts"
msgstr "marts"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "April"
msgstr "April"
msgstr "april"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "May"
msgstr "Maj"
msgstr "maj"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "June"
msgstr "Juni"
msgstr "juni"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "July"
msgstr "Juli"
msgstr "juli"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "August"
msgstr "August"
msgstr "august"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "September"
msgstr "September"
msgstr "september"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "October"
msgstr "Oktober"
msgstr "oktober"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "November"
msgstr "November"
msgstr "november"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "December"
msgstr "December"
msgstr "december"
#~ msgid "iDEAL"
#~ msgstr "iDEAL"
+87 -106
View File
@@ -843,7 +843,7 @@ msgstr "Details der Bestellposition"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee details"
msgstr "Details Teilnehmer"
msgstr "Details Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/answers.py
#: pretix/base/models/items.py pretix/control/navigation.py
@@ -891,11 +891,11 @@ msgstr "Veranstaltungs- oder Termininformationen"
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html
msgid "Attendee name"
msgstr "Name Teilnehmer"
msgstr "Name Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee"
msgstr "Teilnehmer"
msgstr "Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/models/customers.py
@@ -904,11 +904,11 @@ msgstr "Teilnehmer"
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email"
msgstr "E-Mail Teilnehmer"
msgstr "E-Mail Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee or order email"
msgstr "E-Mail Teilnehmer oder Bestellung"
msgstr "E-Mail Teilnehmer*in oder Bestellung"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/orderlist.py
#: pretix/base/pdf.py pretix/control/templates/pretixcontrol/order/index.html
@@ -916,23 +916,23 @@ msgstr "E-Mail Teilnehmer oder Bestellung"
#: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee company"
msgstr "Teilnehmer-Firma"
msgstr "Teilnehmer*innen-Firma"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address street"
msgstr "Teilnehmer-Adresse: Straße"
msgstr "Teilnehmer*innen-Adresse: Straße"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address ZIP code"
msgstr "Teilnehmer-Adresse: PLZ"
msgstr "Teilnehmer*innen-Adresse: PLZ"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address city"
msgstr "Teilnehmer-Adresse: Stadt"
msgstr "Teilnehmer*innen-Adresse: Stadt"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address country"
msgstr "Teilnehmer-Adresse: Land"
msgstr "Teilnehmer*innen-Adresse: Land"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/orderlist.py
#: pretix/base/pdf.py
@@ -1166,7 +1166,7 @@ msgid ""
"Download a ZIP file including all files that have been uploaded by your "
"customers while creating an order."
msgstr ""
"ZIP-Datei mit allen Dateien, die von Kunden im Bestellprozess als "
"ZIP-Datei mit allen Dateien, die von Kund*innen im Bestellprozess als "
"Antwort auf eine Frage hochgeladen wurden."
#: pretix/base/exporters/answers.py pretix/base/exporters/orderlist.py
@@ -2020,11 +2020,7 @@ msgstr "Ohne gültige Mitgliedschaft verstecken"
msgid "Order data"
msgstr "Bestelldaten"
#: pretix/base/exporters/relevant_orderlist.py:86
msgid "Order data (sorted by relevance)"
msgstr "Bestelldaten (nach Relevanz sortiert)"
#: pretix/base/exporters/json.py:53
#: pretix/base/exporters/json.py
msgid ""
"Download a structured JSON representation of all orders. This might be "
"useful for the import in third-party systems."
@@ -2041,8 +2037,8 @@ msgid ""
"Download a text file with all email addresses collected either from buyers "
"or from ticket holders."
msgstr ""
"Textdatei mit allen E-Mail-Adressen, die von Käufern und "
"Ticketinhaber eingesammelt wurden."
"Textdatei mit allen E-Mail-Adressen, die von Käufer*innen und "
"Ticketinhaber*innen eingesammelt wurden."
#: pretix/base/exporters/mail.py pretix/plugins/reports/exporters.py
msgid "Filter by status"
@@ -2059,30 +2055,15 @@ msgstr ""
"Bestellposition und das dritte eine Zeile für jede zusätzlich erhobene "
"Gebühr."
#: pretix/base/exporters/relevant_orderlist.py:88
msgid ""
"Download a spreadsheet of all orders. The spreadsheet will include three "
"sheets, one with a line for every order, one with a line for every order "
"position, and one with a line for every additional fee charged in an "
"order. The most relevant data is in the first columns of the tables."
msgstr ""
"Tabelle (Excel oder CSV) mit allen Bestellungen. Das erste Tabellenblatt "
"enthält eine Zeile für jede Bestellung, das zweite eine Zeile für jede "
"Bestellposition und das dritte eine Zeile für jede zusätzlich erhobene "
"Gebühr. Die relevantesten Daten sind in den ersten Spalten der Tabellen."
#: pretix/base/exporters/orderlist.py:102 pretix/base/models/orders.py:336
#: pretix/base/permissions.py:228 pretix/control/navigation.py:267
#: pretix/control/navigation.py:387
#: pretix/control/templates/pretixcontrol/orders/index.html:8
#: pretix/control/templates/pretixcontrol/orders/index.html:10
#: pretix/control/templates/pretixcontrol/organizers/customer.html:205
#: pretix/control/templates/pretixcontrol/subevents/detail.html:203
#: pretix/plugins/reports/accountingreport.py:673
#: pretix/plugins/reports/accountingreport.py:873
#: pretix/plugins/reports/exporters.py:673 pretix/plugins/statistics/apps.py:39
#: pretix/presale/templates/pretixpresale/organizers/customer_orders.html:13
#: pretix/presale/views/customer.py:364
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
#: pretix/base/permissions.py pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/orders/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/subevents/detail.html
#: pretix/plugins/reports/accountingreport.py
#: pretix/plugins/reports/exporters.py pretix/plugins/statistics/apps.py
#: pretix/presale/templates/pretixpresale/organizers/customer_orders.html
#: pretix/presale/views/customer.py
msgid "Orders"
msgstr "Bestellungen"
@@ -3859,13 +3840,13 @@ msgstr "Bitte wählen Sie einen gültigen Staat aus."
#: pretix/base/modelimport_orders.py pretix/control/forms/filter.py
msgid "Attendee email address"
msgstr "Teilnehmer-E-Mail-Adresse"
msgstr "Teilnehmer*innen-E-Mail-Adresse"
#: pretix/base/modelimport_orders.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee address"
msgstr "Teilnehmer-Adresse"
msgstr "Teilnehmer*innen-Adresse"
#: pretix/base/modelimport_orders.py
msgid "Calculate from product"
@@ -5369,8 +5350,8 @@ msgid ""
"needs to be checked."
msgstr ""
"Wenn diese Option gesetzt ist, wird die Check-in-Anwendung melden, dass "
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studenten "
"nützlich, deren Studentenausweis noch geprüft werden soll."
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studierende "
"nützlich, deren Studierendenausweis noch geprüft werden soll."
#: pretix/base/models/items.py
msgid ""
@@ -6246,8 +6227,8 @@ msgid ""
"cases."
msgstr ""
"Wenn diese Option gesetzt ist, wird die Check-in-Anwendung melden, dass "
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studenten "
"nützlich, deren Studentenausweis noch geprüft werden soll."
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studierende "
"nützlich, deren Studierendenausweis noch geprüft werden soll."
#: pretix/base/models/orders.py
msgid ""
@@ -7723,7 +7704,7 @@ msgstr "Musterfirma GmbH"
#: pretix/base/pdf.py
msgid "Full attendee address"
msgstr "Volle Teilnehmer-Adresse"
msgstr "Volle Teilnehmer*innen-Adresse"
#: pretix/base/pdf.py
msgid ""
@@ -7741,23 +7722,23 @@ msgstr ""
#: pretix/base/pdf.py
msgid "Attendee street"
msgstr "Teilnehmer-Straße"
msgstr "Teilnehmer*innen-Straße"
#: pretix/base/pdf.py
msgid "Attendee ZIP code"
msgstr "Teilnehmer-PLZ"
msgstr "Teilnehmer*innen-PLZ"
#: pretix/base/pdf.py
msgid "Attendee city"
msgstr "Teilnehmer-Stadt"
msgstr "Teilnehmer*innen-Stadt"
#: pretix/base/pdf.py
msgid "Attendee state"
msgstr "Teilnehmer-Bundesstaat"
msgstr "Teilnehmer*innen-Bundesstaat"
#: pretix/base/pdf.py
msgid "Attendee country"
msgstr "Teilnehmer-Land"
msgstr "Teilnehmer*innen-Land"
#: pretix/base/pdf.py
msgid "Pseudonymization ID (lead scanning)"
@@ -8052,7 +8033,7 @@ msgstr "Herr Mustermann"
#: pretix/plugins/ticketoutputpdf/exporters.py
#, python-brace-format
msgid "Attendee name: {part}"
msgstr "Teilnehmername: {part}"
msgstr "Teilnehmer*innenname: {part}"
#: pretix/base/pdf.py
msgid "Invoice address name for salutation"
@@ -8954,7 +8935,7 @@ msgstr "Veranstaltungsort: {location}"
#, python-brace-format
msgctxt "invoice"
msgid "Attendee: {name}"
msgstr "Teilnehmer: {name}"
msgstr "Teilnehmer*in: {name}"
#: pretix/base/services/invoices.py pretix/plugins/reports/exporters.py
#, python-brace-format
@@ -9911,7 +9892,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Hide prices on attendee ticket page"
msgstr "Preise auf Teilnehmer-Ticket-Seite verstecken"
msgstr "Preise auf Teilnehmer*innen-Ticket-Seite verstecken"
#: pretix/base/settings.py
msgid ""
@@ -9921,13 +9902,13 @@ msgid ""
"price."
msgstr ""
"Wenn eine Person mehrere Tickets erwirbt und E-Mails an alle "
"Teilnehmer verschickt werden, wird mit dieser Option der Ticketpreis "
"auf der Ticket-Seite der einzelnen Teilnehmer versteckt. Nur die "
"Teilnehmer*innen verschickt werden, wird mit dieser Option der Ticketpreis "
"auf der Ticket-Seite der einzelnen Teilnehmer*innen versteckt. Nur die "
"Person, welche die Tickets kauft, sieht den Preis."
#: pretix/base/settings.py
msgid "Ask for attendee names"
msgstr "Namen der Teilnehmer erfragen"
msgstr "Namen der Teilnehmer*innen erfragen"
#: pretix/base/settings.py
msgid "Ask for a name for all personalized tickets."
@@ -9936,11 +9917,11 @@ msgstr ""
#: pretix/base/settings.py
msgid "Require attendee names"
msgstr "Namen der Teilnehmer erfordern"
msgstr "Namen der Teilnehmer*innen erfordern"
#: pretix/base/settings.py
msgid "Require customers to fill in the names of all attendees."
msgstr "Erfordere die Eingabe aller Teilnehmer-Namen."
msgstr "Erfordere die Eingabe aller Teilnehmer*innen-Namen."
#: pretix/base/settings.py
msgid "Ask for email addresses per ticket"
@@ -10470,8 +10451,8 @@ msgid ""
"but no indication of missing payment will be visible on the ticket pages of "
"attendees who did not buy the ticket themselves."
msgstr ""
"Die Box mit Zahlungsinstruktionen wird Ticketkäufern weiter angezeigt, "
"aber Teilnehmern, die ihr Ticket nicht selbst gekauft haben, werden "
"Die Box mit Zahlungsinstruktionen wird Ticketkäufer*innen weiter angezeigt, "
"aber Teilnehmer*innen, die ihr Ticket nicht selbst gekauft haben, werden "
"keine Anzeichen des fehlenden Zahlungseingangs sehen."
#: pretix/base/settings.py
@@ -10967,7 +10948,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Show number of check-ins to customer"
msgstr "Zeige Anzahl der Check-ins für Kunden an"
msgstr "Zeige Anzahl der Check-ins für Kund*innen an"
#: pretix/base/settings.py
msgid ""
@@ -10978,12 +10959,12 @@ msgid ""
"failed scans will not be counted, and the user will not see the different "
"check-in lists."
msgstr ""
"Wenn diese Option aktiv ist, können Kunden selbst sehen, wie oft sie die "
"Wenn diese Option aktiv ist, können Kund*innen selbst sehen, wie oft sie die "
"Veranstaltung betreten haben. Das ist normalerweise nicht nötig, aber kann "
"nützlich sein, wenn es Tickets gibt, die eine bestimmte Anzahl an Eintritten "
"erlauben, sodass Kunden die bisherige Nutzung des Tickets einsehen "
"erlauben, sodass Kund*innen die bisherige Nutzung des Tickets einsehen "
"können. Ausgänge oder fehlgeschlagene Scans werden nicht angezeigt und die "
"Kunden sehen keine Aufschlüsselung verschiedener Check-in-Listen."
"Kund*innen sehen keine Aufschlüsselung verschiedener Check-in-Listen."
#: pretix/base/settings.py
msgid "Allow users to download tickets"
@@ -11164,7 +11145,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Both the attendee and the person who ordered can make changes"
msgstr ""
"Sowohl Besteller als auch Teilnehmer können Änderungen vornehmen"
"Sowohl Besteller*in als auch Teilnehmer*innen können Änderungen vornehmen"
#: pretix/base/settings.py
msgid "Allow customers to modify their information"
@@ -11272,7 +11253,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Allow individual attendees to change their ticket"
msgstr "Erlaubt einzelnen Teilnehmern ihr Ticket zu ändern"
msgstr "Erlaubt einzelnen Teilnehmer*innen ihr Ticket zu ändern"
#: pretix/base/settings.py
msgid ""
@@ -11284,7 +11265,7 @@ msgid ""
msgstr ""
"Standardmäßig kann nur die Person, welche die Tickets gekauft hat, "
"Änderungen an der Bestellung vornehmen. Wenn diese Option aktiv ist, können "
"auch einzelne Teilnehmer Änderungen vornehmen. Teilnehmer können "
"auch einzelne Teilnehmer*innen Änderungen vornehmen. Teilnehmer*innen können "
"jedoch immer nur Änderungen vornehmen, welche die Gesamtkosten der "
"Bestellung nicht verändern. Solche Änderungen können nur vom Ticketkäufer "
"vorgenommen werden."
@@ -11593,7 +11574,7 @@ msgid ""
"people."
msgstr ""
"Sie können dieses Feld benutzen, um zusätzliche Informationen mit Ihren "
"Teilnehmer zu teilen, wie z.B. Anreise-Informationen oder den Link zu "
"Teilnehmer*innen zu teilen, wie z.B. Anreise-Informationen oder den Link zu "
"einer digitalen Veranstaltung. Wenn das Feld leer ist, fügen wir automatisch "
"einen Link zum Ticketshop, die Einlass-Uhrzeit und den Veranstalter hier "
"ein. Es sind keine Platzhalter mit sensiblen personenbezogenen Daten "
@@ -11829,8 +11810,8 @@ msgstr ""
"Diese Datei wird an die erste E-Mail angehängt, die wir beim Eingang einer "
"neuen Bestellung verschicken. Sie kann daher mit den Textvorlagen "
"\"Getätigte Bestellung\", \"Kostenlose Bestellung\" oder \"Erhaltene "
"Bestellung\" von oben auftreten. Sie wird ggf. sowohl an Besteller als "
"auch Teilnehmer verschickt. Nicht geeignet zum Versand nicht-"
"Bestellung\" von oben auftreten. Sie wird ggf. sowohl an Besteller*innen als "
"auch Teilnehmer*innen verschickt. Nicht geeignet zum Versand nicht-"
"öffentlicher Informationen, da die Datei unabhängig davon verschickt wird, "
"ob die Bestellung bezahlt oder freigegeben ist. Um zu vermeiden, dass diese "
"wichtige E-Mail nicht ankommt, können nur PDF-Dateien mit maximal {size} MB "
@@ -15118,7 +15099,7 @@ msgstr "Check-in-Status"
#: pretix/control/forms/filter.py pretix/plugins/checkinlists/exporters.py
msgid "All attendees"
msgstr "Alle Teilnehmer"
msgstr "Alle Teilnehmer*innen"
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/checkin/index.html
@@ -15721,7 +15702,7 @@ msgid ""
"people over 65. This ticket includes access to all parts of the event, "
"except the VIP area."
msgstr ""
"z.B. Dieses reduzierte Ticket ist erhältlich für Vollzeitstudent, "
"z.B. Dieses reduzierte Ticket ist erhältlich für Vollzeitstudent*innen, "
"Arbeitslose und Menschen über 65. Das Ticket enthält Zugang zu allen Teilen "
"der Veranstaltung außer des VIP-Bereiches."
@@ -16032,7 +16013,7 @@ msgstr ""
#: pretix/control/forms/orders.py
msgid "Notify customer by email"
msgstr "Kunde per E-Mail benachrichtigen"
msgstr "Kund*in per E-Mail benachrichtigen"
#: pretix/control/forms/orders.py
msgid "Keep a cancellation fee of"
@@ -17638,7 +17619,7 @@ msgstr "Eine individuelle E-Mail wurde verschickt."
#: pretix/control/logdisplay.py
msgid "A custom email has been sent to an attendee."
msgstr "Eine individuelle E-Mail wurde an einen Teilnehmer verschickt."
msgstr "Eine individuelle E-Mail wurde an eine Teilnehmer*in verschickt."
#: pretix/control/logdisplay.py
msgid ""
@@ -19340,7 +19321,7 @@ msgstr "Terminal-ID"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html
msgid "Card holder"
msgstr "Karteninhaber"
msgstr "Karteninhaber*in"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html
msgid "Card expiration"
@@ -19620,7 +19601,7 @@ msgstr "CSV"
#: pretix/control/templates/pretixcontrol/checkin/index.html
msgid "No attendee record was found."
msgstr "Keine passenden Teilnehmer gefunden."
msgstr "Keine passenden Teilnehmer*innen gefunden."
#: pretix/control/templates/pretixcontrol/checkin/index.html
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
@@ -22380,7 +22361,7 @@ msgid ""
"Only purchases of such products will be considered \"attendees\" for most "
"statistical purposes or within some plugins."
msgstr ""
"Nur Käufe eines solchen Produkts werden als \"Teilnehmer\" gewertet, "
"Nur Käufe eines solchen Produkts werden als \"Teilnehmer*innen\" gewertet, "
"z.B. in Statistiken oder in Funktionen von Erweiterungen."
#: pretix/control/templates/pretixcontrol/item/create.html
@@ -22459,7 +22440,7 @@ msgid ""
"The system will not ask for a name or other attendee details. This only "
"affects system-provided fields, you can still add your own questions."
msgstr ""
"Das System wird nicht nach einem Namen oder anderen Teilnehmer-Daten "
"Das System wird nicht nach einem Namen oder anderen Teilnehmer*innen-Daten "
"fragen. Dies betrifft nur vom System bereitgestellte Felder, eigene Fragen "
"können trotzdem hinzugefügt werden."
@@ -24711,7 +24692,7 @@ msgstr "Sonstige Datenexporte"
#: pretix/control/templates/pretixcontrol/orders/export.html
#: pretix/control/templates/pretixcontrol/organizers/export.html
msgid "Recommended for new users"
msgstr "Empfohlen für neue Benutzer"
msgstr "Empfohlen für neue Benutzer*innen"
#: pretix/control/templates/pretixcontrol/orders/export.html
#: pretix/control/templates/pretixcontrol/organizers/export.html
@@ -25644,7 +25625,7 @@ msgstr "Domains"
#: pretix/control/templates/pretixcontrol/organizers/edit.html
msgid "This dialog is intended for advanced users."
msgstr "Dieser Dialog ist für fortgeschrittene Anwender gedacht."
msgstr "Dieser Dialog ist für fortgeschrittene Anwender*innen gedacht."
#: pretix/control/templates/pretixcontrol/organizers/edit.html
msgid ""
@@ -28024,7 +28005,7 @@ msgid ""
"customers. This way, customers will not be able to discover the waiting list."
msgstr ""
"Entsprechend Ihrer Veranstaltungseinstellungen werden ausverkaufte Produkte "
"nicht angezeigt. Dies führt dazu, dass Kunden die Warteliste nicht "
"nicht angezeigt. Dies führt dazu, dass Kund*innen die Warteliste nicht "
"finden können."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html
@@ -28322,11 +28303,11 @@ msgstr "Das ausgewählte List wurde gelöscht."
#: pretix/control/views/dashboards.py
msgid "Attendees (ordered)"
msgstr "Teilnehmer (bestellt)"
msgstr "Teilnehmende (bestellt)"
#: pretix/control/views/dashboards.py
msgid "Attendees (paid)"
msgstr "Teilnehmer (bezahlt)"
msgstr "Teilnehmende (bezahlt)"
#: pretix/control/views/dashboards.py
#, python-brace-format
@@ -30389,7 +30370,7 @@ msgstr "PDF-Sammlungen"
#: pretix/plugins/badges/exporters.py
msgid "Download all attendee badges as one large PDF for printing."
msgstr "Alle Teilnehmer-Badges in einer großen PDF-Datei für den Druck."
msgstr "Alle Teilnehmer*innen-Badges in einer großen PDF-Datei für den Druck."
#: pretix/plugins/badges/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py
@@ -30640,7 +30621,7 @@ msgstr "Anderes Bankkonto"
#: pretix/plugins/banktransfer/payment.py
msgid "Name of account holder"
msgstr "Kontoinhaber"
msgstr "Kontoinhaber*in"
#: pretix/plugins/banktransfer/payment.py
msgid ""
@@ -30776,7 +30757,7 @@ msgstr "Bitte überweisen Sie den vollen Betrag auf das folgende Bankkonto:"
#: pretix/plugins/banktransfer/views.py pretix/plugins/stripe/payment.py
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html
msgid "Account holder"
msgstr "Kontoinhaber"
msgstr "Kontoinhaber*in"
#: pretix/plugins/banktransfer/payment.py
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html
@@ -31354,7 +31335,7 @@ msgid ""
"Download a spreadsheet with all attendees that are included in a check-in "
"list."
msgstr ""
"Tabelle (Excel oder CSV) mit allen Teilnehmer, die in einer Check-in-"
"Tabelle (Excel oder CSV) mit allen Teilnehmer*innen, die in einer Check-in-"
"Liste zutrittsberechtigt sind."
#: pretix/plugins/checkinlists/exporters.py
@@ -32470,7 +32451,7 @@ msgstr "Geplante E-Mails"
#: pretix/plugins/sendmail/signals.py
msgid "Mass email was sent to customers or attendees."
msgstr "Rundmail wurde an Kunden oder Teilnehmer verschickt."
msgstr "Rundmail wurde an Kunden oder Teilnehmer*innen verschickt."
#: pretix/plugins/sendmail/signals.py
msgid "Mass email was sent to waiting list entries."
@@ -32502,7 +32483,7 @@ msgstr "Eine automatisierte E-Mail wurde an den Besteller verschickt"
#: pretix/plugins/sendmail/signals.py
msgid "A scheduled email was sent to a ticket holder"
msgstr "Eine automatisierte E-Mail wurde an eine Teilnehmer verschickt."
msgstr "Eine automatisierte E-Mail wurde an eine Teilnehmer*in verschickt."
#: pretix/plugins/sendmail/signals.py
msgid "An email rule was deleted"
@@ -32535,7 +32516,7 @@ msgstr "Alle nicht eingecheckten Kunden"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html
msgid "Attendee contact addresses"
msgstr "Teilnehmer-E-Mail-Adressen"
msgstr "Teilnehmer*innen-E-Mail-Adressen"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html
msgid "All contact addresses"
@@ -32687,14 +32668,14 @@ msgstr ""
#: pretix/plugins/sendmail/views.py
msgid "Orders or attendees"
msgstr "Bestellungen oder Teilnehmer"
msgstr "Bestellungen oder Teilnehmer*innen"
#: pretix/plugins/sendmail/views.py
msgid ""
"Send an email to every customer, or to every person a ticket has been "
"purchased for, or a combination of both."
msgstr ""
"Senden Sie eine E-Mail an alle Ticketkäufer, alle Ticketinhaber "
"Senden Sie eine E-Mail an alle Ticketkäufer*innen, alle Ticketinhaber*innen "
"oder eine Kombination aus beiden Gruppen."
#: pretix/plugins/sendmail/views.py
@@ -33257,23 +33238,23 @@ msgstr "SEPA-Lastschrift"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Name"
msgstr "Kontoinhaber"
msgstr "Kontoinhaber*in"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Street"
msgstr "Straße (Kontoinhaber)"
msgstr "Straße (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Postal Code"
msgstr "PLZ (Kontoinhaber)"
msgstr "PLZ (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder City"
msgstr "Stadt (Kontoinhaber)"
msgstr "Stadt (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Country"
msgstr "Land (Kontoinhaber)"
msgstr "Land (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Affirm via Stripe"
@@ -33713,7 +33694,7 @@ msgstr "MOTO"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html
msgid "Payer name"
msgstr "Name des Zahlers"
msgstr "Name des Zahlenden"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html
msgid "Payment receipt"
@@ -35006,7 +34987,7 @@ msgstr[1] "Das Ticket wurde %(count)s-mal eingelöst."
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "No attendee name provided"
msgstr "Name des Teilnehmenrs nicht angegeben"
msgstr "Name der teilnehmenden Person nicht angegeben"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
@@ -36783,12 +36764,12 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html
#: pretix/presale/views/customer.py
msgid "Attendee profiles"
msgstr "Teilnehmer-Profile"
msgstr "Teilnehmer*innen-Profile"
#: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html
msgid "You dont have any attendee profiles in your account yet."
msgstr ""
"In Ihrem Kundenkonto sind noch keine Teilnehmer-Profile gespeichert."
"In Ihrem Kundenkonto sind noch keine Teilnehmer*innen-Profile gespeichert."
#: pretix/presale/templates/pretixpresale/organizers/customer_registration.html
msgid "Registration"
@@ -38543,7 +38524,7 @@ msgstr "Kosovo"
#~ "This plugin allows you to generate badges or name tags for your attendees."
#~ msgstr ""
#~ "Diese Erweiterung erlaubt, Namensschilder oder Badges für die "
#~ "Teilnehmer zu erstellen."
#~ "Teilnehmer*innen zu erstellen."
#~ msgid "This plugin allows you to receive payments via PayPal"
#~ msgstr "Dieses Plugin erlaubt, Zahlungen über PayPal anzunehmen"
+2 -2
View File
@@ -153,11 +153,11 @@ msgstr "Bezahlte Bestellungen"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (ordered)"
msgstr "Teilnehmer (bestellt)"
msgstr "Teilnehmende (bestellt)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (paid)"
msgstr "Teilnehmer (bezahlt)"
msgstr "Teilnehmende (bezahlt)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Total revenue"
@@ -845,7 +845,7 @@ msgstr "Details der Bestellposition"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee details"
msgstr "Details Teilnehmer"
msgstr "Details Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/answers.py
#: pretix/base/models/items.py pretix/control/navigation.py
@@ -893,11 +893,11 @@ msgstr "Veranstaltungs- oder Termininformationen"
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html
msgid "Attendee name"
msgstr "Name Teilnehmer"
msgstr "Name Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee"
msgstr "Teilnehmer"
msgstr "Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/models/customers.py
@@ -906,11 +906,11 @@ msgstr "Teilnehmer"
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email"
msgstr "E-Mail Teilnehmer"
msgstr "E-Mail Teilnehmer*in"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee or order email"
msgstr "E-Mail Teilnehmer oder Bestellung"
msgstr "E-Mail Teilnehmer*in oder Bestellung"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/orderlist.py
#: pretix/base/pdf.py pretix/control/templates/pretixcontrol/order/index.html
@@ -918,23 +918,23 @@ msgstr "E-Mail Teilnehmer oder Bestellung"
#: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee company"
msgstr "Teilnehmer-Firma"
msgstr "Teilnehmer*innen-Firma"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address street"
msgstr "Teilnehmer-Adresse: Straße"
msgstr "Teilnehmer*innen-Adresse: Straße"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address ZIP code"
msgstr "Teilnehmer-Adresse: PLZ"
msgstr "Teilnehmer*innen-Adresse: PLZ"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address city"
msgstr "Teilnehmer-Adresse: Stadt"
msgstr "Teilnehmer*innen-Adresse: Stadt"
#: pretix/base/datasync/sourcefields.py
msgid "Attendee address country"
msgstr "Teilnehmer-Adresse: Land"
msgstr "Teilnehmer*innen-Adresse: Land"
#: pretix/base/datasync/sourcefields.py pretix/base/exporters/orderlist.py
#: pretix/base/pdf.py
@@ -1168,7 +1168,7 @@ msgid ""
"Download a ZIP file including all files that have been uploaded by your "
"customers while creating an order."
msgstr ""
"ZIP-Datei mit allen Dateien, die von Kunden im Bestellprozess als "
"ZIP-Datei mit allen Dateien, die von Kund*innen im Bestellprozess als "
"Antwort auf eine Frage hochgeladen wurden."
#: pretix/base/exporters/answers.py pretix/base/exporters/orderlist.py
@@ -2039,8 +2039,8 @@ msgid ""
"Download a text file with all email addresses collected either from buyers "
"or from ticket holders."
msgstr ""
"Textdatei mit allen E-Mail-Adressen, die von Käufer und "
"Ticketinhaber eingesammelt wurden."
"Textdatei mit allen E-Mail-Adressen, die von Käufer*innen und "
"Ticketinhaber*innen eingesammelt wurden."
#: pretix/base/exporters/mail.py pretix/plugins/reports/exporters.py
msgid "Filter by status"
@@ -3842,13 +3842,13 @@ msgstr "Bitte wähle einen gültigen Staat aus."
#: pretix/base/modelimport_orders.py pretix/control/forms/filter.py
msgid "Attendee email address"
msgstr "Teilnehmer-E-Mail-Adresse"
msgstr "Teilnehmer*innen-E-Mail-Adresse"
#: pretix/base/modelimport_orders.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee address"
msgstr "Teilnehmer-Adresse"
msgstr "Teilnehmer*innen-Adresse"
#: pretix/base/modelimport_orders.py
msgid "Calculate from product"
@@ -5347,8 +5347,8 @@ msgid ""
"needs to be checked."
msgstr ""
"Wenn diese Option gesetzt ist, wird die Check-in-Anwendung melden, dass "
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studenten "
"nützlich, deren Studentenausweis noch geprüft werden soll."
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studierende "
"nützlich, deren Studierendenausweis noch geprüft werden soll."
#: pretix/base/models/items.py
msgid ""
@@ -6224,8 +6224,8 @@ msgid ""
"cases."
msgstr ""
"Wenn diese Option gesetzt ist, wird die Check-in-Anwendung melden, dass "
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studenten "
"nützlich, deren Studentenausweis noch geprüft werden soll."
"dieses Ticket besondere Behandlung braucht. Das ist z.B. für Studierende "
"nützlich, deren Studierendenausweis noch geprüft werden soll."
#: pretix/base/models/orders.py
msgid ""
@@ -7697,7 +7697,7 @@ msgstr "Musterfirma"
#: pretix/base/pdf.py
msgid "Full attendee address"
msgstr "Volle Teilnehmer-Adresse"
msgstr "Volle Teilnehmer*innen-Adresse"
#: pretix/base/pdf.py
msgid ""
@@ -7715,23 +7715,23 @@ msgstr ""
#: pretix/base/pdf.py
msgid "Attendee street"
msgstr "Teilnehmer-Straße"
msgstr "Teilnehmer*innen-Straße"
#: pretix/base/pdf.py
msgid "Attendee ZIP code"
msgstr "Teilnehmer-PLZ"
msgstr "Teilnehmer*innen-PLZ"
#: pretix/base/pdf.py
msgid "Attendee city"
msgstr "Teilnehmer-Stadt"
msgstr "Teilnehmer*innen-Stadt"
#: pretix/base/pdf.py
msgid "Attendee state"
msgstr "Teilnehmer-Bundesstaat"
msgstr "Teilnehmer*innen-Bundesstaat"
#: pretix/base/pdf.py
msgid "Attendee country"
msgstr "Teilnehmer-Land"
msgstr "Teilnehmer*innen-Land"
#: pretix/base/pdf.py
msgid "Pseudonymization ID (lead scanning)"
@@ -8026,7 +8026,7 @@ msgstr "Herr Mustermann"
#: pretix/plugins/ticketoutputpdf/exporters.py
#, python-brace-format
msgid "Attendee name: {part}"
msgstr "Teilnehmer: {part}"
msgstr "Teilnehmer*innenname: {part}"
#: pretix/base/pdf.py
msgid "Invoice address name for salutation"
@@ -8925,7 +8925,7 @@ msgstr "Veranstaltungsort: {location}"
#, python-brace-format
msgctxt "invoice"
msgid "Attendee: {name}"
msgstr "Teilnehmer: {name}"
msgstr "Teilnehmer*in: {name}"
#: pretix/base/services/invoices.py pretix/plugins/reports/exporters.py
#, python-brace-format
@@ -9880,7 +9880,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Hide prices on attendee ticket page"
msgstr "Preise auf Teilnehmer-Ticket-Seite verstecken"
msgstr "Preise auf Teilnehmer*innen-Ticket-Seite verstecken"
#: pretix/base/settings.py
msgid ""
@@ -9890,13 +9890,13 @@ msgid ""
"price."
msgstr ""
"Wenn eine Person mehrere Tickets erwirbt und E-Mails an alle "
"Teilnehmer verschickt werden, wird mit dieser Option der Ticketpreis "
"auf der Ticket-Seite der einzelnen Teilnehmer versteckt. Nur die "
"Teilnehmer*innen verschickt werden, wird mit dieser Option der Ticketpreis "
"auf der Ticket-Seite der einzelnen Teilnehmer*innen versteckt. Nur die "
"Person, welche die Tickets kauft, sieht den Preis."
#: pretix/base/settings.py
msgid "Ask for attendee names"
msgstr "Namen der Teilnehmer erfragen"
msgstr "Namen der Teilnehmer*innen erfragen"
#: pretix/base/settings.py
msgid "Ask for a name for all personalized tickets."
@@ -9905,11 +9905,11 @@ msgstr ""
#: pretix/base/settings.py
msgid "Require attendee names"
msgstr "Namen der Teilnehmer erfordern"
msgstr "Namen der Teilnehmer*innen erfordern"
#: pretix/base/settings.py
msgid "Require customers to fill in the names of all attendees."
msgstr "Erfordere die Eingabe aller Teilnehmer-Namen."
msgstr "Erfordere die Eingabe aller Teilnehmer*innen-Namen."
#: pretix/base/settings.py
msgid "Ask for email addresses per ticket"
@@ -9931,7 +9931,7 @@ msgstr ""
"aktivierst, fragt das System zusätzlich nach einzelnen E-Mail-Adressen für "
"jedes personalisierte Ticket in der Bestellung. Dies könnte z.B. nützlich "
"sein, wenn du auch im Falle von Gruppenbestellungen individuelle Adressen "
"von jedem Teilnehmer benötigst. pretix sendet die Bestellbestätigung "
"von jeder Teilnehmer*in benötigst. pretix sendet die Bestellbestätigung "
"standardmäßig nach wie vor nur an die primäre Adresse, dies kann jedoch in "
"den E-Mail-Einstellungen angepasst werden."
@@ -10438,8 +10438,8 @@ msgid ""
"but no indication of missing payment will be visible on the ticket pages of "
"attendees who did not buy the ticket themselves."
msgstr ""
"Die Box mit Zahlungsinstruktionen wird Ticketkäufern weiter angezeigt, "
"aber Teilnehmer, die ihr Ticket nicht selbst gekauft haben, werden "
"Die Box mit Zahlungsinstruktionen wird Ticketkäufer*innen weiter angezeigt, "
"aber Teilnehmer*innen, die ihr Ticket nicht selbst gekauft haben, werden "
"keine Anzeichen des fehlenden Zahlungseingangs sehen."
#: pretix/base/settings.py
@@ -10935,7 +10935,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Show number of check-ins to customer"
msgstr "Zeige Anzahl der Check-ins für Kunden an"
msgstr "Zeige Anzahl der Check-ins für Kund*innen an"
#: pretix/base/settings.py
msgid ""
@@ -10946,12 +10946,12 @@ msgid ""
"failed scans will not be counted, and the user will not see the different "
"check-in lists."
msgstr ""
"Wenn diese Option aktiv ist, können Kunden selbst sehen, wie oft sie die "
"Wenn diese Option aktiv ist, können Kund*innen selbst sehen, wie oft sie die "
"Veranstaltung betreten haben. Das ist normalerweise nicht nötig, aber kann "
"nützlich sein, wenn es Tickets gibt, die eine bestimmte Anzahl an Eintritten "
"erlauben, sodass Kunden die bisherige Nutzung des Tickets einsehen "
"erlauben, sodass Kund*innen die bisherige Nutzung des Tickets einsehen "
"können. Ausgänge oder fehlgeschlagene Scans werden nicht angezeigt und die "
"Kunden sehen keine Aufschlüsselung verschiedener Check-in-Listen."
"Kund*innen sehen keine Aufschlüsselung verschiedener Check-in-Listen."
#: pretix/base/settings.py
msgid "Allow users to download tickets"
@@ -11132,7 +11132,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Both the attendee and the person who ordered can make changes"
msgstr ""
"Sowohl Besteller als auch Teilnehmer können Änderungen vornehmen"
"Sowohl Besteller*in als auch Teilnehmer*innen können Änderungen vornehmen"
#: pretix/base/settings.py
msgid "Allow customers to modify their information"
@@ -11240,7 +11240,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Allow individual attendees to change their ticket"
msgstr "Erlaubt einzelnen Teilnehmer ihr Ticket zu ändern"
msgstr "Erlaubt einzelnen Teilnehmer*innen ihr Ticket zu ändern"
#: pretix/base/settings.py
msgid ""
@@ -11252,7 +11252,7 @@ msgid ""
msgstr ""
"Standardmäßig kann nur die Person, welche die Tickets gekauft hat, "
"Änderungen an der Bestellung vornehmen. Wenn diese Option aktiv ist, können "
"auch einzelne Teilnehmer Änderungen vornehmen. Teilnehmer können "
"auch einzelne Teilnehmer*innen Änderungen vornehmen. Teilnehmer*innen können "
"jedoch immer nur Änderungen vornehmen, welche die Gesamtkosten der "
"Bestellung nicht verändern. Solche Änderungen können nur vom Ticketkäufer "
"vorgenommen werden."
@@ -11435,7 +11435,7 @@ msgstr "Kontakt-E-Mail"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "We'll show this publicly to allow attendees to contact you."
msgstr ""
"Wir werden diese Adresse veröffentlichen um Teilnehmern zu ermöglichen, "
"Wir werden diese Adresse veröffentlichen um Teilnehmer*innen zu ermöglichen, "
"dich zu kontaktieren."
#: pretix/base/settings.py
@@ -11560,7 +11560,7 @@ msgid ""
"people."
msgstr ""
"Du kannst dieses Feld benutzen um zusätzliche Informationen mit deinen "
"Teilnehmer zu teilen, wie z.B. Anreise-Informationen oder den Link zu "
"Teilnehmer*innen zu teilen, wie z.B. Anreise-Informationen oder den Link zu "
"einer digitalen Veranstaltung. Wenn das Feld leer ist, fügen wir automatisch "
"einen Link zum Ticketshop, die Einlass-Uhrzeit und den Veranstalter hier "
"ein. Es sind keine Platzhalter mit sensiblen personenbezogenen Daten "
@@ -11795,8 +11795,8 @@ msgstr ""
"Diese Datei wird an die erste E-Mail angehängt, die wir beim Eingang einer "
"neuen Bestellung verschicken. Sie kann daher mit den Textvorlagen "
"\"Getätigte Bestellung\", \"Kostenlose Bestellung\" oder \"Erhaltene "
"Bestellung\" von oben auftreten. Sie wird ggf. sowohl an Besteller als "
"auch Teilnehmer verschickt. Nicht geeignet zum Versand nicht-"
"Bestellung\" von oben auftreten. Sie wird ggf. sowohl an Besteller*innen als "
"auch Teilnehmer*innen verschickt. Nicht geeignet zum Versand nicht-"
"öffentlicher Informationen, da die Datei unabhängig davon verschickt wird, "
"ob die Bestellung bezahlt oder freigegeben ist. Um zu vermeiden, dass diese "
"wichtige E-Mail nicht ankommt, können nur PDF-Dateien mit maximal {size} MB "
@@ -13039,7 +13039,7 @@ msgstr ""
msgid ""
"You cannot require specifying attendee names if you do not ask for them."
msgstr ""
"Du kannst die Angabe von Teilnehmernamen nur erfordern, wenn auch nach "
"Du kannst die Angabe von Teilnehmer*innennamen nur erfordern, wenn auch nach "
"Namen gefragt wird."
#: pretix/base/settings.py
@@ -14712,11 +14712,11 @@ msgstr "Ticket-Downloads"
#: pretix/control/forms/event.py
msgid "Your customers will be able to download their tickets in PDF format."
msgstr ""
"Die Teilnehmer werden ihre Tickets im PDF-Format herunterladen können."
"Die Teilnehmer*innen werden ihre Tickets im PDF-Format herunterladen können."
#: pretix/control/forms/event.py
msgid "Require all attendees to fill in their names"
msgstr "Erfordere, dass alle Teilnehmer ihre Namen ausfüllen"
msgstr "Erfordere, dass alle Teilnehmer*innen ihre Namen ausfüllen"
#: pretix/control/forms/event.py
msgid ""
@@ -14752,7 +14752,7 @@ msgid ""
"then import your bank statements to process the payments within pretix, or "
"mark them as paid manually."
msgstr ""
"Deine Teilnehmer werden angewiesen, das Geld direkt auf dein Bankkonto "
"Deine Teilnehmer*innen werden angewiesen, das Geld direkt auf dein Bankkonto "
"zu überweisen. Du kannst dann deinen Kontoauszug in pretix importieren, um "
"Zahlungen zuzuweisen, oder die Bestellungen manuell als bezahlt markieren."
@@ -15075,7 +15075,7 @@ msgstr "Check-in-Status"
#: pretix/control/forms/filter.py pretix/plugins/checkinlists/exporters.py
msgid "All attendees"
msgstr "Alle Teilnehmer"
msgstr "Alle Teilnehmer*innen"
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/checkin/index.html
@@ -15678,7 +15678,7 @@ msgid ""
"people over 65. This ticket includes access to all parts of the event, "
"except the VIP area."
msgstr ""
"z.B. Dieses reduzierte Ticket ist erhältlich für Vollzeitstudenten, "
"z.B. Dieses reduzierte Ticket ist erhältlich für Vollzeitstudent*innen, "
"Arbeitslose und Menschen über 65. Das Ticket enthält Zugang zu allen Teilen "
"der Veranstaltung außer des VIP-Bereiches."
@@ -17595,7 +17595,7 @@ msgstr "Eine individuelle E-Mail wurde verschickt."
#: pretix/control/logdisplay.py
msgid "A custom email has been sent to an attendee."
msgstr "Eine individuelle E-Mail wurde an eine Teilnehmer verschickt."
msgstr "Eine individuelle E-Mail wurde an eine Teilnehmer*in verschickt."
#: pretix/control/logdisplay.py
msgid ""
@@ -19295,7 +19295,7 @@ msgstr "Terminal-ID"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html
msgid "Card holder"
msgstr "Karteninhaber"
msgstr "Karteninhaber*in"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html
msgid "Card expiration"
@@ -19575,7 +19575,7 @@ msgstr "CSV"
#: pretix/control/templates/pretixcontrol/checkin/index.html
msgid "No attendee record was found."
msgstr "Keine passenden Teilnehmer gefunden."
msgstr "Keine passenden Teilnehmer*innen gefunden."
#: pretix/control/templates/pretixcontrol/checkin/index.html
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
@@ -21354,7 +21354,7 @@ msgid ""
"provide ways for your attendees to contact you:"
msgstr ""
"Wenn irgendetwas schiefgeht oder unklar ist, empfehlen wir, dass du deinen "
"Teilnehmer die Möglichkeit gibst, dich zu benachrichtigen:"
"Teilnehmer*innen die Möglichkeit gibst, dich zu benachrichtigen:"
#: pretix/control/templates/pretixcontrol/event/settings.html
msgid "Basics"
@@ -22331,7 +22331,7 @@ msgid ""
"Only purchases of such products will be considered \"attendees\" for most "
"statistical purposes or within some plugins."
msgstr ""
"Nur Käufe eines solchen Produkts werden als \"Teilnehmer\" gewertet, "
"Nur Käufe eines solchen Produkts werden als \"Teilnehmer*innen\" gewertet, "
"z.B. in Statistiken oder in Funktionen von Erweiterungen."
#: pretix/control/templates/pretixcontrol/item/create.html
@@ -22410,7 +22410,7 @@ msgid ""
"The system will not ask for a name or other attendee details. This only "
"affects system-provided fields, you can still add your own questions."
msgstr ""
"Das System wird nicht nach einem Namen oder anderen Teilnehmer-Daten "
"Das System wird nicht nach einem Namen oder anderen Teilnehmer*innen-Daten "
"fragen. Dies betrifft nur vom System bereitgestellte Felder, eigene Fragen "
"können trotzdem hinzugefügt werden."
@@ -23081,9 +23081,9 @@ msgid ""
"ticket. If you provide food, one example might be to ask your users about "
"dietary requirements."
msgstr ""
"Fragen erlauben deinen Besucher, zusätzliche Informationen zu ihrem "
"Fragen erlauben deinen Besucher*innen, zusätzliche Informationen zu ihrem "
"Ticket auszufüllen. Wenn deine Veranstaltung Verpflegung beinhaltet, "
"könntest du z.B. nach Allergien deiner Teilnehmer fragen."
"könntest du z.B. nach Allergien deiner Teilnehmer*innen fragen."
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question"
@@ -23245,7 +23245,7 @@ msgstr ""
"Um deine Produkte verfügbar zu machen, musst du Kontingente anlegen. "
"Kontingente definieren, wie oft ein Produkt verkauft werden darf. Auf diese "
"Art kannst du konfigurieren, ob deine Veranstaltung unbegrenzt viele "
"Teilnehmer aufnehmen kann oder ob die Anzahl begrenzt ist. Du kannst "
"Teilnehmer*innen aufnehmen kann oder ob die Anzahl begrenzt ist. Du kannst "
"ein Produkt zu mehreren Kontingenten hinzufügen, um komplexere Anforderungen "
"abzubilden, z.B. wenn du die Gesamtzahl der Tickets begrenzen willst, aber "
"einen speziellen Ticket-Typ noch stärker begrenzen willst."
@@ -24659,7 +24659,7 @@ msgstr "Sonstige Datenexporte"
#: pretix/control/templates/pretixcontrol/orders/export.html
#: pretix/control/templates/pretixcontrol/organizers/export.html
msgid "Recommended for new users"
msgstr "Empfohlen für neue Benutzer"
msgstr "Empfohlen für neue Benutzer*innen"
#: pretix/control/templates/pretixcontrol/orders/export.html
#: pretix/control/templates/pretixcontrol/organizers/export.html
@@ -25591,7 +25591,7 @@ msgstr "Domains"
#: pretix/control/templates/pretixcontrol/organizers/edit.html
msgid "This dialog is intended for advanced users."
msgstr "Dieser Dialog ist für fortgeschrittene Anwender gedacht."
msgstr "Dieser Dialog ist für fortgeschrittene Anwender*innen gedacht."
#: pretix/control/templates/pretixcontrol/organizers/edit.html
msgid ""
@@ -27966,7 +27966,7 @@ msgid ""
"customers. This way, customers will not be able to discover the waiting list."
msgstr ""
"Entsprechend deiner Veranstaltungseinstellungen werden ausverkaufte Produkte "
"nicht angezeigt. Dies führt dazu, dass Kunden die Warteliste nicht "
"nicht angezeigt. Dies führt dazu, dass Kund*innen die Warteliste nicht "
"finden können."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html
@@ -28263,11 +28263,11 @@ msgstr "Das ausgewählte List wurde gelöscht."
#: pretix/control/views/dashboards.py
msgid "Attendees (ordered)"
msgstr "Teilnehmer (bestellt)"
msgstr "Teilnehmende (bestellt)"
#: pretix/control/views/dashboards.py
msgid "Attendees (paid)"
msgstr "Teilnehmer (bezahlt)"
msgstr "Teilnehmende (bezahlt)"
#: pretix/control/views/dashboards.py
#, python-brace-format
@@ -30326,7 +30326,7 @@ msgstr "PDF-Sammlungen"
#: pretix/plugins/badges/exporters.py
msgid "Download all attendee badges as one large PDF for printing."
msgstr "Alle Teilnehmer-Badges in einer großen PDF-Datei für den Druck."
msgstr "Alle Teilnehmer*innen-Badges in einer großen PDF-Datei für den Druck."
#: pretix/plugins/badges/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py
@@ -30576,7 +30576,7 @@ msgstr "Anderes Bankkonto"
#: pretix/plugins/banktransfer/payment.py
msgid "Name of account holder"
msgstr "Kontoinhaber"
msgstr "Kontoinhaber*in"
#: pretix/plugins/banktransfer/payment.py
msgid ""
@@ -30712,7 +30712,7 @@ msgstr "Bitte überweise den vollen Betrag auf das folgende Bankkonto:"
#: pretix/plugins/banktransfer/views.py pretix/plugins/stripe/payment.py
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html
msgid "Account holder"
msgstr "Kontoinhaber"
msgstr "Kontoinhaber*in"
#: pretix/plugins/banktransfer/payment.py
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html
@@ -31291,7 +31291,7 @@ msgid ""
"Download a spreadsheet with all attendees that are included in a check-in "
"list."
msgstr ""
"Tabelle (Excel oder CSV) mit allen Teilnehmer, die in einer Check-in-"
"Tabelle (Excel oder CSV) mit allen Teilnehmer*innen, die in einer Check-in-"
"Liste zutrittsberechtigt sind."
#: pretix/plugins/checkinlists/exporters.py
@@ -32404,7 +32404,7 @@ msgstr "Geplante E-Mails"
#: pretix/plugins/sendmail/signals.py
msgid "Mass email was sent to customers or attendees."
msgstr "Rundmail wurde an Kunden oder Teilnehmer verschickt."
msgstr "Rundmail wurde an Kunden oder Teilnehmer*innen verschickt."
#: pretix/plugins/sendmail/signals.py
msgid "Mass email was sent to waiting list entries."
@@ -32436,7 +32436,7 @@ msgstr "Eine automatisierte E-Mail wurde an den Besteller verschickt"
#: pretix/plugins/sendmail/signals.py
msgid "A scheduled email was sent to a ticket holder"
msgstr "Eine automatisierte E-Mail wurde an einen Teilnehmer verschickt."
msgstr "Eine automatisierte E-Mail wurde an eine Teilnehmer*in verschickt."
#: pretix/plugins/sendmail/signals.py
msgid "An email rule was deleted"
@@ -32469,7 +32469,7 @@ msgstr "Alle nicht eingecheckten Kunden"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html
msgid "Attendee contact addresses"
msgstr "Teilnehmer-E-Mail-Adressen"
msgstr "Teilnehmer*innen-E-Mail-Adressen"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html
msgid "All contact addresses"
@@ -32623,14 +32623,14 @@ msgstr ""
#: pretix/plugins/sendmail/views.py
msgid "Orders or attendees"
msgstr "Bestellungen oder Teilnehmer"
msgstr "Bestellungen oder Teilnehmer*innen"
#: pretix/plugins/sendmail/views.py
msgid ""
"Send an email to every customer, or to every person a ticket has been "
"purchased for, or a combination of both."
msgstr ""
"Sende eine E-Mail an alle Ticketkäufer, alle Ticketinhaber oder "
"Sende eine E-Mail an alle Ticketkäufer*innen, alle Ticketinhaber*innen oder "
"eine Kombination aus beiden Gruppen."
#: pretix/plugins/sendmail/views.py
@@ -33191,23 +33191,23 @@ msgstr "SEPA-Lastschrift"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Name"
msgstr "Kontoinhaber"
msgstr "Kontoinhaber*in"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Street"
msgstr "Straße (Kontoinhaber)"
msgstr "Straße (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Postal Code"
msgstr "PLZ (Kontoinhaber)"
msgstr "PLZ (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder City"
msgstr "Stadt (Kontoinhaber)"
msgstr "Stadt (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Account Holder Country"
msgstr "Land (Kontoinhaber)"
msgstr "Land (Kontoinhaber*in)"
#: pretix/plugins/stripe/payment.py
msgid "Affirm via Stripe"
@@ -33645,7 +33645,7 @@ msgstr "MOTO"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html
msgid "Payer name"
msgstr "Name des Zahlers"
msgstr "Name des Zahlenden"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html
msgid "Payment receipt"
@@ -34933,7 +34933,7 @@ msgstr[1] "Das Ticket wurde %(count)s-mal eingelöst."
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "No attendee name provided"
msgstr "Name des Teilnehmenrs nicht angegeben"
msgstr "Name der teilnehmenden Person nicht angegeben"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
@@ -36708,12 +36708,12 @@ msgstr "Möchtest das folgende Profil wirklich aus deinem Kundenkonto löschen?"
#: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html
#: pretix/presale/views/customer.py
msgid "Attendee profiles"
msgstr "Teilnehmer-Adresse"
msgstr "Teilnehmer*innen-Adresse"
#: pretix/presale/templates/pretixpresale/organizers/customer_profiles.html
msgid "You dont have any attendee profiles in your account yet."
msgstr ""
"In deinem Kundenkonto sind noch keine Teilnehmer-Profile gespeichert."
"In deinem Kundenkonto sind noch keine Teilnehmer*innen-Profile gespeichert."
#: pretix/presale/templates/pretixpresale/organizers/customer_registration.html
msgid "Registration"
@@ -38413,7 +38413,7 @@ msgstr "Kosovo"
#~ "This plugin allows you to generate badges or name tags for your attendees."
#~ msgstr ""
#~ "Diese Erweiterung erlaubt, Namensschilder oder Badges für die "
#~ "Teilnehmer zu erstellen."
#~ "Teilnehmer*innen zu erstellen."
#~ msgid "This plugin allows you to receive payments via PayPal"
#~ msgstr "Dieses Plugin erlaubt, Zahlungen über PayPal anzunehmen"
@@ -38991,7 +38991,7 @@ msgstr "Kosovo"
#~ msgstr "Biete Ticket-Download bereits vor Bezahlung einer Bestellung an"
#~ msgid "Attendee names"
#~ msgstr "Teilnehmername"
#~ msgstr "Teilnehmer*innennamen"
#~ msgid "Enable output"
#~ msgstr "Aktivieren"
@@ -39268,7 +39268,7 @@ msgstr "Kosovo"
#~ "If checked, users can cancel orders by themselves as long as they are not "
#~ "yet paid."
#~ msgstr ""
#~ "Wenn diese Option aktiviert ist, können Teilnehmer selbstständig "
#~ "Wenn diese Option aktiviert ist, können Teilnehmer*innen selbstständig "
#~ "Bestellungen stornieren solange sie nicht bezahlt wurden."
#~ msgid "Sales overview"
@@ -153,11 +153,11 @@ msgstr "Bezahlte Bestellungen"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (ordered)"
msgstr "Teilnehmer (bestellt)"
msgstr "Teilnehmende (bestellt)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (paid)"
msgstr "Teilnehmer (bezahlt)"
msgstr "Teilnehmende (bezahlt)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Total revenue"
+5 -19
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
"PO-Revision-Date: 2026-07-23 15:35+0000\n"
"PO-Revision-Date: 2026-08-01 21:00+0000\n"
"Last-Translator: szurofkamarciidfbe08444ef04788 <szurofkamarcii@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix/"
"hu/>\n"
@@ -1377,10 +1377,8 @@ msgid "Membership type"
msgstr "Tagságtípus"
#: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "Purchase time"
msgid "Purchase ticket"
msgstr "Vásárlás időpontja"
msgstr "Jegy vásárlása"
#: pretix/base/exporters/customers.py pretix/base/exporters/orderlist.py
#: pretix/base/exporters/waitinglist.py pretix/base/forms/questions.py
@@ -1397,10 +1395,8 @@ msgid "Start date"
msgstr "Kezdő dátum"
#: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "Start time from"
msgid "Start time"
msgstr "Kezdési időpont ettől"
msgstr "Kezdési időpont"
#: pretix/base/exporters/customers.py pretix/base/exporters/orderlist.py
#: pretix/base/exporters/waitinglist.py pretix/base/models/memberships.py
@@ -1413,10 +1409,8 @@ msgid "End date"
msgstr "Záró dátum"
#: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "End: %(time)s"
msgid "End time"
msgstr "Vége: %(time)s"
msgstr "Záró időpont"
#: pretix/base/exporters/dekodi.py pretix/base/exporters/invoices.py
msgctxt "export_category"
@@ -4641,10 +4635,6 @@ msgid "This event is remote or partially remote."
msgstr "Ez az rendezvény távoli vagy részben távoli."
#: pretix/base/models/event.py
#, fuzzy
#| msgid ""
#| "This will be used to let users know if the event is in a different "
#| "timezone and lets us calculate users local times."
msgid ""
"This will be used to let users know if the event is in a different timezone, "
"and to let us calculate the local time of a user."
@@ -7578,15 +7568,11 @@ msgid "This gift card was used in the meantime. Please try again."
msgstr "Ezt az ajándékkártyát időközben felhasználták. Kérjük, próbálja újra."
#: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment provider does not exist or the respective plugin is disabled."
msgid ""
"This payment provider exists for historical purposes only and is no longer "
"usable."
msgstr ""
"Ez a fizetési szolgáltató nem létezik, vagy a hozzá tartozó bővítmény le van "
"tiltva."
"Ez a fizetési szolgáltató egy korábbi funkció volt, már nem használható."
#: pretix/base/pdf.py
msgid "Ticket code (barcode content)"
+96 -44
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
"PO-Revision-Date: 2026-05-12 04:00+0000\n"
"Last-Translator: Stefano Campus <stefano.campus@regione.piemonte.it>\n"
"PO-Revision-Date: 2026-08-03 23:00+0000\n"
"Last-Translator: \"Luca Sorace \\\"Stranck\\\"\" <strdjn@gmail.com>\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 5.17.1\n"
"X-Generator: Weblate 2026.7.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -149,7 +149,7 @@ msgstr "Spagnolo (America Latina)"
#: pretix/_base_settings.py
msgid "Thai"
msgstr ""
msgstr "Tailandese"
#: pretix/_base_settings.py
msgid "Turkish"
@@ -413,7 +413,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format
msgid "You've been invited to join %(organizer)s"
msgstr ""
msgstr "Sei stato invitato ad entrare in %(organizer)s"
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team."
@@ -605,6 +605,9 @@ msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"Questo include gli eventi relativi quali la creazione, cancellazione, "
"apertura o chiusura delle quote. Nessun webhook verrà inviato per modifiche "
"alla disponibilità."
#: pretix/api/webhooks.py
msgid "Shop taken live"
@@ -1234,8 +1237,9 @@ msgstr "Indirizzi Email (file di testo)"
#: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html
#: pretix/presale/views/customer.py
#, fuzzy
msgid "Memberships"
msgstr ""
msgstr "Membership"
#: pretix/base/exporters/customers.py pretix/base/models/customers.py
#: pretix/control/templates/pretixcontrol/organizers/customer.html
@@ -3030,11 +3034,12 @@ msgid ""
"The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"."
msgstr ""
"Il campo \"%(label)s\" non può contenere i caratteri speciali \"%(chars)s\"."
#: pretix/base/forms/questions.py
#, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr ""
msgstr "Il campo \"%(label)s\" non può contenere un URL (%(url)s)."
#: pretix/base/forms/questions.py
msgctxt "phonenumber"
@@ -4272,8 +4277,9 @@ msgid "Ticket type not allowed here"
msgstr "Biglietto non consentito qui"
#: pretix/base/models/checkin.py
#, fuzzy
msgid "Ticket code is ambiguous on list"
msgstr "Codice biglietto ambiguo nella lista"
msgstr "Il codice del biglietto è ambiguo sulla lista"
#: pretix/base/models/checkin.py
msgid "Server error"
@@ -6167,14 +6173,15 @@ msgid "bounced"
msgstr "rimbalzato"
#: pretix/base/models/media.py
#, fuzzy
msgctxt "reusable_medium"
msgid "Claim token"
msgstr ""
msgstr "Applica token"
#: pretix/base/models/media.py
msgctxt "reusable_medium"
msgid "Label"
msgstr ""
msgstr "Etichetta"
#: pretix/base/models/media.py
#, fuzzy
@@ -6188,6 +6195,9 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Se colleghi più di un biglietto, assicurati che la loro validità non si "
"sovrapponga. Se più biglietti sono validi contemporaneamente i check-in "
"andranno in errore."
#: pretix/base/models/memberships.py
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html
@@ -7676,6 +7686,8 @@ msgid ""
"This payment provider exists for historical purposes only and is no longer "
"usable."
msgstr ""
"Questo provider di pagamenti esiste per ragioni storiche e non ne è più "
"possibile l'utilizzo."
#: pretix/base/pdf.py
msgid "Ticket code (barcode content)"
@@ -7843,7 +7855,7 @@ msgstr "Giorno di inizio evento"
#: pretix/base/pdf.py pretix/base/services/checkin.py
#: pretix/control/forms/filter.py
msgid "Friday"
msgstr "venerdì"
msgstr "Venerdì"
#: pretix/base/pdf.py
msgid "Event end date and time"
@@ -8387,7 +8399,7 @@ msgstr "Evento annullato"
#: pretix/base/services/cancelevent.py
msgid "Confirm event cancellation and bulk refund"
msgstr ""
msgstr "Conferma la cancellazione dell'evento e i rimborsi di tutti gli ordini"
#: pretix/base/services/cart.py pretix/base/services/modelimport.py
#: pretix/base/services/orders.py
@@ -9333,6 +9345,8 @@ msgstr "Non è possibile creare un buono senza un codice."
msgid ""
"Voucher codes must be unique. Code \"{code}\" already exists in this import."
msgstr ""
"Il codice del voucher deve essere univoco. \"{code}\" esiste già in questo "
"import."
#: pretix/base/services/modelimport.py
#, python-brace-format
@@ -9341,13 +9355,19 @@ msgid ""
msgid_plural ""
"Voucher codes must be unique. Import contains existing voucher codes {code}."
msgstr[0] ""
"Il codice del voucher deve essere univoco. \"{code}\" esiste già in questo "
"import."
msgstr[1] ""
"Il codice dei voucher deve essere univoco. \"{code}\" esiste già in questo "
"import."
#: pretix/base/services/modelimport.py
msgid ""
"Vouchers could not be imported, probably due to a voucher code already being "
"in use."
msgstr ""
"Non è stato possibile importare i voucher, probabilmente perché il codice di "
"un voucher è già in uso."
#: pretix/base/services/orders.py
msgid ""
@@ -10267,6 +10287,10 @@ msgid ""
"ID in all countries. VAT ID will be required for all business addresses in "
"the selected countries."
msgstr ""
"La partita IVA è opzionale di default, perché non tutte le aziende la "
"posseggono in tutte le nazioni. Il codice della partita IVA sarà "
"obbligatorio per tutti gli indirizzi aziendali locati nelle nazioni "
"selezionate."
#: pretix/base/settings.py
msgid "Invoice address explanation"
@@ -11316,50 +11340,60 @@ msgstr ""
#: pretix/base/settings.py
msgid "Customers can change the variation of the products they purchased"
msgstr ""
msgstr "I clienti potranno cambiare la variation dei prodotti già acquistati"
#: pretix/base/settings.py
#, fuzzy
msgid "Customers can change their selected add-on products"
msgstr "I clienti non possono più modificare i loro ordini"
msgstr "I clienti non potranno più modificare i gli add-on selezionati"
#: pretix/base/settings.py
msgid ""
"Only allow changes if the resulting price is higher or equal than the "
"previous price."
msgstr ""
"Permetti le modifiche solamente se il prezzo finale è maggiore o uguale al "
"prezzo precedente."
#: pretix/base/settings.py
msgid ""
"Only allow changes if the resulting price is higher than the previous price."
msgstr ""
"Permetti le modifiche solamente se il prezzo finale è maggiore del prezzo "
"precedente."
#: pretix/base/settings.py
msgid ""
"Only allow changes if the resulting price is equal to the previous price."
msgstr ""
"Permetti le modifiche solamente se il prezzo finale è uguale al prezzo "
"precedente."
#: pretix/base/settings.py
msgid ""
"Allow changes regardless of price, as long as no refund is required (i.e. "
"the resulting price is not lower than what has already been paid)."
msgstr ""
"Permetti le modifiche a prescindere del prezzo, a meno che non sia "
"necessario un rimborso (Il prezzo finale non è inferiore a quanto il cliente "
"ha già pagato)."
#: pretix/base/settings.py
msgid "Allow changes regardless of price, even if this results in a refund."
msgstr ""
"Permetti le modifiche a prescindere del prezzo, anche se portano ad un "
"rimborso."
#: pretix/base/settings.py
msgid "Requirement for changed prices"
msgstr ""
msgstr "Requisiti per il cambiamento di prezzi"
#: pretix/base/settings.py
msgid "Do not allow changes after"
msgstr ""
msgstr "Non permettere modifiche dopo il"
#: pretix/base/settings.py
msgid "Allow change even though the ticket has already been checked in"
msgstr ""
msgstr "Permetti modifiche anche se il biglietto ha già effettuato il check-in"
#: pretix/base/settings.py
msgid ""
@@ -11369,10 +11403,16 @@ msgid ""
"in individually. Use with care, and preferably only in combination with a "
"limitation on price changes above."
msgstr ""
"Di default, le modifiche degli ordini sono disabilitate dopo che è stato "
"effettuato il check-in con qualsiasi biglietto contenuto nell'ordine. "
"Abilitando questa opzione, disabiliterai questo controllo. Non sarà comunque "
"possibile rimuovere un add-on con cui è già stato effettuato un check-in. "
"Usa questa opzione con attenzione e, preferibilmente, solamente in "
"combinazione con una delle limitazioni sul prezzo elencate sopra."
#: pretix/base/settings.py
msgid "Allow individual attendees to change their ticket"
msgstr ""
msgstr "Permetti ai singoli partecipanti di modificare il proprio biglietto"
#: pretix/base/settings.py
msgid ""
@@ -11382,15 +11422,20 @@ msgid ""
"total price of the order. Such changes can always only be made by the main "
"customer."
msgstr ""
"Di default, solo la persona che ha effettuato l'ordine di questo biglietto "
"può eseguire modifiche. Selezionando questa opzione, anche i singoli "
"partecipanti potranno fare cambiamenti. In ogni caso, i singoli partecipanti "
"non potranno effettuare operazioni che cambiano il costo dell'ordine. Queste "
"operazioni sono possibili solamente per chi originariamente ha creato "
"l'ordine."
#: pretix/base/settings.py
msgid "Customers can cancel their unpaid orders"
msgstr ""
msgstr "I clienti possono annullare i loro ordini non pagati"
#: pretix/base/settings.py
#, fuzzy
msgid "Charge a fixed cancellation fee"
msgstr "Cancellazione di"
msgstr "Addebita una quota di cancellazione fissa"
#: pretix/base/settings.py
msgid ""
@@ -11398,50 +11443,58 @@ msgid ""
"never charged. Note that it will be your responsibility to claim the "
"cancellation fee from the user."
msgstr ""
"Interessa solamente gli ordini con pagamenti in attesa, la quota di "
"cancellazione non viene mai addebitata sugli ordini gratuiti. Nota: è tua "
"responsabilità collezionare la quota di cancellazione dall'utente."
#: pretix/base/settings.py
msgid "Charge payment, shipping and service fees"
msgstr ""
msgstr "Addebita la quota di spedizione, pagamento e servizio"
#: pretix/base/settings.py
msgid "Charge a percentual cancellation fee"
msgstr ""
msgstr "Addebita una quota di quota di cancellazione in percentuale"
#: pretix/base/settings.py
msgid "Do not allow cancellations after"
msgstr ""
msgstr "Non permettere di annullare gli ordine dopo il"
#: pretix/base/settings.py
msgid "Customers can cancel their paid orders"
msgstr ""
msgstr "I clienti possono annullare i loro ordini già pagati"
#: pretix/base/settings.py
msgid ""
"Paid money will be automatically paid back if the payment method allows it. "
"Otherwise, a manual refund will be created for you to process manually."
msgstr ""
"Il denaro già incassato sarà automaticamente restituito se il metodo di "
"pagamento lo permette. Altrimenti, un rimborso manuale verrà creato così che "
"tu possa processarlo manualmente."
#: pretix/base/settings.py pretix/control/forms/orders.py
msgid "Keep a fixed cancellation fee"
msgstr ""
msgstr "Trattieni una quota di cancellazione fissa"
#: pretix/base/settings.py
msgid "Keep payment, shipping and service fees"
msgstr ""
msgstr "Trattieni la quota di spedizione, pagamento e servizio"
#: pretix/base/settings.py pretix/control/forms/orders.py
msgid "Keep a percentual cancellation fee"
msgstr ""
msgstr "Trattieni una quota di cancellazione in percentuale"
#: pretix/base/settings.py
msgid "Allow customers to voluntarily choose a lower refund"
msgstr ""
msgstr "Permetti si clienti di scegliere volontariamente un rimborso minore"
#: pretix/base/settings.py
msgid ""
"With this option enabled, your customers can choose to get a smaller refund "
"to support you."
msgstr ""
"Con questa opzione attiva, i tuoi clienti potranno scegliere di ottenere un "
"rimborso minore al dovuto, per effettuare una donazione nei vostri confronti."
#: pretix/base/settings.py
msgid ""
@@ -20421,7 +20474,7 @@ msgstr "Abilita webhook"
#: pretix/control/templates/pretixcontrol/organizers/device_logs.html
#: pretix/control/templates/pretixcontrol/organizers/logs.html
msgid "No results"
msgstr ""
msgstr "Nessun risultato"
#: pretix/control/templates/pretixcontrol/event/mail.html
#: pretix/control/templates/pretixcontrol/organizers/mail.html
@@ -20462,7 +20515,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html
msgid "Edit"
msgstr ""
msgstr "Modifica"
#: pretix/control/templates/pretixcontrol/event/mail.html
#, fuzzy
@@ -25479,7 +25532,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/pdf/index.html
msgid "Text box"
msgstr ""
msgstr "Riquadro testo"
#: pretix/control/templates/pretixcontrol/pdf/index.html
#, fuzzy
@@ -25520,7 +25573,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/pdf/index.html
msgid "Duplicate"
msgstr ""
msgstr "Duplicato"
#: pretix/control/templates/pretixcontrol/pdf/index.html
msgid "Undo"
@@ -27145,11 +27198,11 @@ msgstr ""
#: pretix/control/views/dashboards.py
msgid "Attendees (ordered)"
msgstr ""
msgstr "Partecipanti (ordini effettuati)"
#: pretix/control/views/dashboards.py
msgid "Attendees (paid)"
msgstr ""
msgstr "Partecipanti (ordini pagati)"
#: pretix/control/views/dashboards.py
#, python-brace-format
@@ -31650,11 +31703,11 @@ msgstr ""
#: pretix/plugins/stripe/payment.py
msgid "Bancontact"
msgstr ""
msgstr "Bancontact"
#: pretix/plugins/stripe/payment.py
msgid "SEPA Direct Debit"
msgstr ""
msgstr "Addebito diretto SEPA"
#: pretix/plugins/stripe/payment.py
msgid ""
@@ -31684,7 +31737,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py
msgid "Przelewy24"
msgstr ""
msgstr "Przelewy24"
#: pretix/plugins/stripe/payment.py
#, fuzzy
@@ -31700,7 +31753,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py
msgid "WeChat Pay"
msgstr ""
msgstr "WeChat Pay"
#: pretix/plugins/stripe/payment.py
msgid "Swish"
@@ -31853,7 +31906,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py
msgid "giropay"
msgstr ""
msgstr "giropay"
#: pretix/plugins/stripe/payment.py
msgid ""
@@ -31877,7 +31930,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py
msgid "iDEAL | Wero"
msgstr ""
msgstr "iDEAL | Wero"
#: pretix/plugins/stripe/payment.py
msgid ""
@@ -33621,9 +33674,8 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/fragment_modals.html
#, fuzzy
msgid "Renew reservation"
msgstr "Descrizione"
msgstr "Rinnova scadenza"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#, fuzzy
+98 -135
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-03-25 14:14+0000\n"
"Last-Translator: Pietro Isotti <isottipietro@gmail.com>\n"
"PO-Revision-Date: 2026-08-02 22:00+0000\n"
"Last-Translator: \"Luca Sorace \\\"Stranck\\\"\" <strdjn@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/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 5.16.2\n"
"X-Generator: Weblate 2026.7.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -42,93 +42,91 @@ msgstr "Apple Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Itaú"
msgstr ""
msgstr "Ita"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#, fuzzy
msgid "PayPal Credit"
msgstr "PayPal"
msgstr "PayPal (Pagamento a rate)"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Credit Card"
msgstr ""
msgstr "Carta di credito"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal Pay Later"
msgstr ""
msgstr "PayPal (Acquista ora, paga dopo)"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "iDEAL | Wero"
msgstr ""
msgstr "iDEAL | Wero"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "SEPA Direct Debit"
msgstr ""
msgstr "Addebito diretto SEPA"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Bancontact"
msgstr ""
msgstr "Bancontact"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "giropay"
msgstr ""
msgstr "giropay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "SOFORT"
msgstr ""
msgstr "SOFORT"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#, fuzzy
msgid "eps"
msgstr "Si"
msgstr "eps"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "MyBank"
msgstr ""
msgstr "MyBank"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Przelewy24"
msgstr ""
msgstr "Przelewy24"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Verkkopankki"
msgstr ""
msgstr "Verkkopankki"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayU"
msgstr ""
msgstr "PayU"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "BLIK"
msgstr ""
msgstr "BLIK"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Trustly"
msgstr ""
msgstr "Trustly"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Zimpler"
msgstr ""
msgstr "Zimpler"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Maxima"
msgstr ""
msgstr "Maxima"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "OXXO"
msgstr ""
msgstr "OXXO"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Boleto"
msgstr ""
msgstr "Boleto"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "WeChat Pay"
msgstr ""
msgstr "WeChat Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Mercado Pago"
msgstr ""
msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
@@ -143,7 +141,7 @@ msgstr "Stiamo processando il tuo pagamento …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Payment method unavailable"
msgstr ""
msgstr "Metodo di pagamento non disponibile"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Placed orders"
@@ -155,11 +153,11 @@ msgstr "Ordini pagati"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (ordered)"
msgstr ""
msgstr "Partecipanti (ordini effettuati)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (paid)"
msgstr ""
msgstr "Partecipanti (ordini pagati)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Total revenue"
@@ -239,11 +237,11 @@ msgstr "Eliminato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Confirmed"
msgstr ""
msgstr "Confermato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Approval pending"
msgstr ""
msgstr "In attesa di approvazione"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Redeemed"
@@ -310,12 +308,13 @@ msgid "Order canceled"
msgstr "Ordine cancellato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
msgid "Ticket code is ambiguous on list"
msgstr ""
msgstr "Il codice del biglietto è ambiguo sulla lista"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Order not approved"
msgstr ""
msgstr "Ordine non approvato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Checked-in Tickets"
@@ -430,11 +429,11 @@ msgstr "Usa i tasti Ctrl-C per copiare!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Edit"
msgstr ""
msgstr "Modifica"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Visualize"
msgstr ""
msgstr "Visualizza"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid ""
@@ -442,10 +441,13 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"La tua regola filtra sempre per prodotto o variation, ma i seguenti prodotti "
"o variation non sono presenti in nessuna parte delle tue regole. Questo "
"impedirà alle persone con questi biglietti di poter accedere all'evento:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Please double-check if this was intentional."
msgstr ""
msgstr "Per favore, controlla se è stato intenzionale."
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "All of the conditions below (AND)"
@@ -489,17 +491,17 @@ msgstr "minuti"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Duplicate"
msgstr ""
msgstr "Duplicato"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgctxt "entry_status"
msgid "present"
msgstr ""
msgstr "Presente"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgctxt "entry_status"
msgid "absent"
msgstr ""
msgstr "Assente"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "is one of"
@@ -515,7 +517,7 @@ msgstr "è dopo"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "="
msgstr ""
msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Product"
@@ -535,63 +537,55 @@ msgstr "Data e orario corrente"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
msgstr "Giorno corrente della settimana (1 = Lunedì, 7 = Domenica)"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current entry status"
msgstr ""
msgstr "Stato dell'ingresso corrente"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of previous entries"
msgstr "Numero di inserimenti precedenti"
msgstr "Numero di precedenti ingressi"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of previous entries since midnight"
msgstr "Numero di inserimenti precedenti fino a mezzanotte"
msgstr "Numero di precedenti ingressi fino a mezzanotte"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries since"
msgstr "Numero di inserimenti precedenti"
msgstr "Numero di precedenti ingressi dal"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries before"
msgstr "Numero di inserimenti precedenti"
msgstr "Numero di precedenti ingressi prima del"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of days with a previous entry"
msgstr "Nunmero di giorni con un inserimento precedente"
msgstr "Numero di giorni con un precedente ingresso"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of days with a previous entry"
msgid "Number of days with a previous entry since"
msgstr "Nunmero di giorni con un inserimento precedente"
msgstr "Numero di giorni con un precedente ingresso dal"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of days with a previous entry"
msgid "Number of days with a previous entry before"
msgstr "Nunmero di giorni con un inserimento precedente"
msgstr "Numero di giorni con un precedente ingresso prima del"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Minutes since last entry (-1 on first entry)"
msgstr ""
msgstr "Minuti dall'ultimo ingresso (-1 al primo ingresso)"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Minutes since first entry (-1 on first entry)"
msgstr ""
msgstr "Minuti dal primo ingresso (-1 al primo ingresso)"
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
msgid "Error: Product not found!"
msgstr ""
msgstr "Errore: Prodotto non trovato!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
msgid "Error: Variation not found!"
msgstr ""
msgstr "Errore: Variation non trovata!"
#: pretix/static/pretixcontrol/js/ui/editor.js
msgid "Check-in QR"
@@ -606,16 +600,12 @@ msgid "Group of objects"
msgstr "Gruppo di oggetti"
#: pretix/static/pretixcontrol/js/ui/editor.js
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "Oggetto testo"
msgstr "Oggetto testo (deprecato)"
#: pretix/static/pretixcontrol/js/ui/editor.js
#, fuzzy
#| msgid "Text object"
msgid "Text box"
msgstr "Oggetto testo"
msgstr "Riquadro testo"
#: pretix/static/pretixcontrol/js/ui/editor.js
msgid "Barcode area"
@@ -662,25 +652,24 @@ msgid "Unknown error."
msgstr "Errore sconosciuto."
#: pretix/static/pretixcontrol/js/ui/main.js
#, fuzzy
#| msgid "Your color has great contrast and is very easy to read!"
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "Il colore scelto ha un ottimo contrasto ed è molto leggibile!"
msgstr "Il colore scelto ha un ottimo contrasto ed è molto leggibile."
#: pretix/static/pretixcontrol/js/ui/main.js
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
"Il colore scelto ha un buon contrasto e probabilmente è abbastanza leggibile!"
"Il colore scelto ha un buon contrasto ed è sufficiente per i requisiti di "
"accessibilità."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
"Il colore scelto ha un contrasto insufficiente rispetto al bianco. "
"L'accessibilità nel tuo sito sarà ridotta."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Search query"
@@ -700,11 +689,11 @@ msgstr "Solo i selezionati"
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Enter page number between 1 and %(max)s."
msgstr ""
msgstr "Inserisci il numero di pagina tra 1 e %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Invalid page number."
msgstr ""
msgstr "Numero di pagina invalido."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Use a different name internally"
@@ -723,10 +712,8 @@ msgid "Calculating default price…"
msgstr "Calcolando il prezzo di default…"
#: pretix/static/pretixcontrol/js/ui/plugins.js
#, fuzzy
#| msgid "Search results"
msgid "No results"
msgstr "Risultati ricerca"
msgstr "Nessun risultato"
#: pretix/static/pretixcontrol/js/ui/question.js
msgid "Others"
@@ -756,39 +743,33 @@ msgstr "Carrello scaduto"
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Your cart is about to expire."
msgstr ""
msgstr "La selezione nel tuo carrello sta per scadere."
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Gli elementi nel tuo carrello sono riservati per 1 minuto."
msgstr[0] "Gli elementi nel tuo carrello sono riservati per un minuto."
msgstr[1] "Gli elementi nel tuo carrello sono riservati per {num} minuti."
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "Cart expired"
msgid "Your cart has expired."
msgstr "Carrello scaduto"
msgstr "Il tuo carrello è scaduto."
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid ""
#| "The items in your cart are no longer reserved for you. You can still "
#| "complete your order as long as theyre available."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as they're available."
msgstr ""
"Gli articoli nel tuo carrello non sono più riservati per te. Puoi ancora "
"Gli articoli nel tuo carrello non sono più riservati a te. Puoi ancora "
"completare il tuo ordine finché sono disponibili."
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Do you want to renew the reservation period?"
msgstr ""
msgstr "Vuoi rinnovare la scadenza del tuo carrello?"
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Renew reservation"
msgstr ""
msgstr "Rinnova scadenza"
#: pretix/static/pretixpresale/js/ui/main.js
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -807,69 +788,64 @@ msgid "Your local time:"
msgstr "Ora locale:"
#: pretix/static/pretixpresale/js/walletdetection.js
#, fuzzy
#| msgid "Apple Pay"
msgid "Google Pay"
msgstr "Apple Pay"
msgstr "Google Pay"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Quantity"
msgstr ""
msgstr "Quantità"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Decrease quantity"
msgstr ""
msgstr "Diminuisci quantità"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
msgstr "Aumenta quantità"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Filter events by"
msgstr ""
msgstr "Filtra eventi per"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Filter"
msgstr ""
msgstr "Filtra"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Price"
msgstr ""
msgstr "Prezzo"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
msgstr "Prezzo originale: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
msgstr "Nuovo prezzo: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Select %s"
msgctxt "widget"
msgid "Select"
msgstr "Seleziona %s"
msgstr "Seleziona"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -926,7 +902,7 @@ msgstr "da %(currency)s %(price)s"
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
msgstr "Immagine di %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -967,27 +943,21 @@ msgstr "Disponibile solo con voucher"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget"
msgid "Not yet available"
msgstr "attualmente disponibile: %s"
msgstr "Ancora non disponibile"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Not available anymore"
msgstr ""
msgstr "Non più disponibile"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget"
msgid "Currently not available"
msgstr "attualmente disponibile: %s"
msgstr "Attualmente non disponibile"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1095,18 +1065,17 @@ msgstr "Chiudi"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget"
msgid "Close checkout"
msgstr "Ricarica checkout"
msgstr "Finisci checkout"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr ""
"Non puoi annullare questa operazione. Per favore, aspetta che il caricamento "
"finisca."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1116,21 +1085,15 @@ msgstr "Continua"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Select variant %s"
msgctxt "widget"
msgid "Show variants"
msgstr "Seleziona variante %s"
msgstr "Mostra varianti"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Select variant %s"
msgctxt "widget"
msgid "Hide variants"
msgstr "Seleziona variante %s"
msgstr "Nascondi varianti"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1236,37 +1199,37 @@ msgstr "Do"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Monday"
msgstr ""
msgstr "Lunedì"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Tuesday"
msgstr ""
msgstr "Martedì"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Wednesday"
msgstr ""
msgstr "Mercoledì"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Thursday"
msgstr ""
msgstr "Giovedì"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Friday"
msgstr ""
msgstr "Venerdì"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Saturday"
msgstr ""
msgstr "Sabato"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Sunday"
msgstr ""
msgstr "Domenica"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
+19 -2
View File
@@ -23,7 +23,7 @@ import json
import logging
import urllib.parse
from collections import OrderedDict
from datetime import timedelta
from datetime import datetime, timedelta
from decimal import Decimal
from django import forms
@@ -645,7 +645,7 @@ class PaypalMethod(BasePaymentProvider):
def _execute_payment(self, request: HttpRequest, payment: OrderPayment):
payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('payment is already confirmed; possible return-view/webhook race-condition')
# payment is already confirmed; possible return-view/webhook race-condition
return
try:
@@ -677,6 +677,8 @@ class PaypalMethod(BasePaymentProvider):
raise PaymentException(_('We had trouble communicating with PayPal'))
else:
pp_captured_order = response.result
payment.info = json.dumps(pp_captured_order.dict())
payment.save()
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id)
@@ -830,6 +832,7 @@ class PaypalMethod(BasePaymentProvider):
payment.info = json.dumps(pp_captured_order.dict())
payment.save(update_fields=['info'])
payment.confirm()
self.log_payment_duration(payment)
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
# Payment has not any captures yet - so it's probably in created status
@@ -839,6 +842,20 @@ class PaypalMethod(BasePaymentProvider):
if 'payment_paypal_oid' in request.session:
del request.session['payment_paypal_oid']
@staticmethod
def log_payment_duration(payment: OrderPayment):
try:
capture = payment.info_data["purchase_units"][0]["payments"]["captures"][0]
create_time: str | None = capture["create_time"]
update_time: str | None = capture["update_time"]
except (KeyError, IndexError, TypeError):
create_time = None
update_time = None
if create_time is not None and update_time is not None:
duration = datetime.fromisoformat(update_time) - datetime.fromisoformat(create_time)
logger.info('{}: {} - paypal payment processing time'.format(str(payment.global_id), str(duration)))
def payment_pending_render(self, request, payment) -> str:
retry = True
try:
+7 -5
View File
@@ -357,14 +357,13 @@ def webhook(request, *args, **kwargs):
if 'resource_type' not in event_json:
return HttpResponse("Invalid body, no resource_type given", status=400)
if event_json['resource_type'] not in ["checkout-order", "refund", "capture"]:
return HttpResponse("Not interested in this resource type", status=200)
# Retrieve the Charge ID of the refunded payment
if event_json['resource_type'] == 'refund':
if event_json['resource_type'] == 'checkout-order':
payloadid = event_json['resource']['id']
elif event_json['resource_type'] == 'refund' or event_json['resource_type'] == 'capture':
payloadid = get_link(event_json['resource']['links'], 'up')['href'].split('/')[-1]
else:
payloadid = event_json['resource']['id']
return HttpResponse("Not interested in this resource type", status=200)
refs = [payloadid]
if event_json['resource'].get('supplementary_data', {}).get('related_ids', {}).get('order_id'):
@@ -424,6 +423,8 @@ def webhook(request, *args, **kwargs):
**event_json,
'_order_state': sale.dict(),
})
payment.info = json.dumps(sale.dict())
payment.save()
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'):
if event_json['resource_type'] == 'refund':
@@ -489,6 +490,7 @@ def webhook(request, *args, **kwargs):
payment.info = json.dumps(sale.dict())
payment.save(update_fields=['info'])
payment.confirm()
prov.log_payment_duration(payment)
except Quota.QuotaExceededException:
pass
elif sale['status'] == 'APPROVED':
+4
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from django.core.exceptions import ValidationError
from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
@@ -118,6 +119,7 @@ class RuleViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Rule.objects.filter(event=self.request.event)
@transaction.atomic()
def perform_create(self, serializer):
super().perform_create(serializer)
serializer.instance.log_action(
@@ -128,6 +130,7 @@ class RuleViewSet(viewsets.ModelViewSet):
)
@transaction.atomic()
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.log_action(
@@ -137,6 +140,7 @@ class RuleViewSet(viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.plugins.sendmail.rule.deleted',
@@ -19,19 +19,9 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import hashlib
from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile, File
from django.core.files.storage import default_storage
from django.core.management.base import BaseCommand
from django_scopes import scopes_disabled
from pretix.base.settings import GlobalSettingsObject
from pretix.presale.views.widget import (
generate_widget_js, version_max, version_min,
)
from pretix.presale.views.widget import regenerate_all_widget_js
class Command(BaseCommand):
@@ -41,26 +31,5 @@ class Command(BaseCommand):
parser.add_argument('--organizer', action='store', type=str)
parser.add_argument('--event', action='store', type=str)
@scopes_disabled()
def handle(self, *args, **options):
gs = GlobalSettingsObject()
for lc, ll in settings.LANGUAGES:
for version in range(version_min, version_max + 1):
data = generate_widget_js(version, lc).encode()
checksum = hashlib.sha1(data).hexdigest()
settings_file_key = 'widget_file_v{}_{}'.format(version, lc)
settings_checksum_key = 'widget_checksum_v{}_{}'.format(version, lc)
fname = gs.settings.get(settings_file_key)
if not fname or gs.settings.get(settings_checksum_key, '') != checksum:
newname = default_storage.save(
'pub/widget/widget.v{}.{}.{}.js'.format(version, lc, checksum),
ContentFile(data)
)
gs.settings.set(settings_file_key, 'file://' + newname)
gs.settings.set(settings_checksum_key, checksum)
cache.delete('widget_js_data_v{}_{}'.format(version, lc))
if fname:
if isinstance(fname, File):
default_storage.delete(fname.name)
else:
default_storage.delete(fname)
regenerate_all_widget_js()
+1 -1
View File
@@ -147,7 +147,7 @@ def get_font_stylesheet(font_name, organizer: Organizer = None, event: Event = N
stylesheet = []
font = get_fonts(event)[font_name]
for sty, formats in font.items():
if sty == 'sample':
if sty in ['sample', 'pdf_only']:
continue
stylesheet.append('@font-face { ')
stylesheet.append('font-family: "{}";'.format(font_name))
+57 -36
View File
@@ -229,6 +229,60 @@ def generate_widget_js(version, lang, use_vite=False):
return f"/* v{version} */\n" + code
def get_widget_js(version, lang, use_vite, force_regenerate=False):
variant = 'vite' if use_vite else 'legacy'
cache_prefix = 'widget_js_data_v{}_{}_{}'.format(version, lang, variant)
settings_key = 'widget_file_v{}_{}_{}'.format(version, lang, variant)
checksum_key = 'widget_checksum_v{}_{}_{}'.format(version, lang, variant)
gs = GlobalSettingsObject()
if not force_regenerate:
cached_js = cache.get(cache_prefix)
if cached_js and not settings.DEBUG:
return cached_js
fname = gs.settings.get(settings_key)
if fname and not settings.DEBUG:
if isinstance(fname, File):
fname = fname.name
try:
data = default_storage.open(fname).read()
cache.set(cache_prefix, data, 3600 * 4)
return data
except:
logger.exception('Failed to open widget.js')
else:
fname = gs.settings.get(settings_key)
data = generate_widget_js(version, lang, use_vite=use_vite).encode()
checksum = hashlib.sha1(data).hexdigest()
should_save = (
not fname
or gs.settings.get(checksum_key, '') != checksum
)
if should_save and not settings.DEBUG:
newname = default_storage.save(
'widget/widget.{}.{}.{}.{}.js'.format(version, lang, variant, checksum),
ContentFile(data)
)
gs.settings.set(settings_key, 'file://' + newname)
gs.settings.set(checksum_key, checksum)
cache.set(cache_prefix, data, 3600 * 4)
if fname:
if isinstance(fname, File):
default_storage.delete(fname.name)
else:
default_storage.delete(fname)
return data
def regenerate_all_widget_js():
for lc, ll in settings.LANGUAGES:
for version in range(version_min, version_max + 1):
for use_vite in [True, False]:
get_widget_js(version, lc, use_vite, force_regenerate=True)
@gzip_page
@condition(etag_func=widget_js_etag)
def widget_js(request, version, lang, **kwargs):
@@ -239,43 +293,9 @@ def widget_js(request, version, lang, **kwargs):
version = version_min
use_vite = _use_vite(request)
variant = 'vite' if use_vite else 'legacy'
cache_prefix = 'widget_js_data_v{}_{}_{}'.format(version, lang, variant)
data = get_widget_js(version, lang, use_vite)
cached_js = cache.get(cache_prefix)
if cached_js and not settings.DEBUG:
resp = HttpResponse(cached_js, content_type='text/javascript')
resp['Access-Control-Allow-Origin'] = '*'
return resp
settings_key = 'widget_file_v{}_{}_{}'.format(version, lang, variant)
checksum_key = 'widget_checksum_v{}_{}_{}'.format(version, lang, variant)
gs = GlobalSettingsObject()
fname = gs.settings.get(settings_key)
resp = None
if fname and not settings.DEBUG:
if isinstance(fname, File):
fname = fname.name
try:
data = default_storage.open(fname).read()
resp = HttpResponse(data, content_type='text/javascript')
cache.set(cache_prefix, data, 3600 * 4)
except:
logger.exception('Failed to open widget.js')
if not resp:
data = generate_widget_js(version, lang, use_vite=use_vite).encode()
checksum = hashlib.sha1(data).hexdigest()
if not settings.DEBUG:
newname = default_storage.save(
'widget/widget.{}.{}.{}.{}.js'.format(version, lang, variant, checksum),
ContentFile(data)
)
gs.settings.set(settings_key, 'file://' + newname)
gs.settings.set(checksum_key, checksum)
cache.set(cache_prefix, data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp = HttpResponse(data, content_type='text/javascript')
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -795,6 +815,7 @@ class WidgetAPIProductList(EventListMixin, View):
'target_url': eventreverse_absolute(request.event, 'presale:event.index'),
'subevent': self.subevent.pk if self.subevent else None,
'currency': request.event.currency,
'currency_places': settings.CURRENCY_PLACES.get(request.event.currency, 2),
'display_net_prices': request.event.settings.display_net_prices,
'use_native_spinners': request.event.settings.widget_use_native_spinners,
'show_variations_expanded': request.event.settings.show_variations_expanded,
@@ -0,0 +1,42 @@
$(document).on('pretix:bind-forms', () => {
document.querySelectorAll('.reldatetime, .reldate').forEach(container => {
const groups = container.querySelectorAll('.radio')
groups.forEach(group => {
const referenceSelect = group.querySelector('select[data-relative-choice]')
const beforeAfterSelect = group.querySelector('select[data-relation-choice]')
if (!referenceSelect || !beforeAfterSelect) return
const beforeOption = beforeAfterSelect.querySelector('option[value="before"]')
const afterOption = beforeAfterSelect.querySelector('option[value="after"]')
const updateBeforeOption = () => {
let supportsBefore = referenceSelect.selectedOptions[0].hasAttribute('data-supports-before')
if (beforeOption) {
beforeOption.disabled = !beforeOption.disabled && !supportsBefore
}
let supportsAfter = referenceSelect.selectedOptions[0].hasAttribute('data-supports-after')
if (afterOption) {
afterOption.disabled = !afterOption.disabled && !supportsAfter
}
let dirty = false
if (beforeOption.disabled && beforeAfterSelect.value === 'before') {
beforeAfterSelect.value = 'after'
dirty = true
}
if (afterOption.disabled && beforeAfterSelect.value === 'after') {
beforeAfterSelect.value = 'before'
dirty = true
}
if (dirty) {
beforeAfterSelect.dispatchEvent(new Event('change', {bubbles: true}))
}
}
referenceSelect.addEventListener('change', updateBeforeOption)
updateBeforeOption()
})
})
})
@@ -380,16 +380,16 @@ Vue.component('pricebox', {
},
display_price: function () {
if (this.$root.display_net_prices) {
return floatformat(parseFloat(this.price.net), 2);
return floatformat(this.price.net, this.$root.currency_places);
} else {
return floatformat(parseFloat(this.price.gross), 2);
return floatformat(this.price.gross, this.$root.currency_places);
}
},
display_price_nonlocalized: function () {
if (this.$root.display_net_prices) {
return parseFloat(this.price.net).toFixed(2);
return parseFloat(this.price.net).toFixed(this.$root.currency_places);
} else {
return parseFloat(this.price.gross).toFixed(2);
return parseFloat(this.price.gross).toFixed(this.$root.currency_places);
}
},
suggested_price_nonlocalized: function () {
@@ -398,9 +398,9 @@ Vue.component('pricebox', {
price = this.price;
}
if (this.$root.display_net_prices) {
return parseFloat(price.net).toFixed(2);
return parseFloat(price.net).toFixed(this.$root.currency_places);
} else {
return parseFloat(price.gross).toFixed(2);
return parseFloat(price.gross).toFixed(this.$root.currency_places);
}
},
original_price_aria_label: function () {
@@ -410,7 +410,7 @@ Vue.component('pricebox', {
return django.interpolate(strings.new_price, [this.stripHTML(this.priceline)]);
},
original_line: function () {
return '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + "</span> " + floatformat(parseFloat(this.original_price), 2);
return '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + "</span> " + floatformat(this.original_price, this.$root.currency_places);
},
priceline: function () {
if (this.price.gross === "0.00") {
@@ -645,19 +645,19 @@ Vue.component('item', {
if (this.item.free_price) {
return django.interpolate(strings.price_from, {
'currency': this.$root.currency,
'price': floatformat(this.item.min_price, 2)
'price': floatformat(this.item.min_price, this.$root.currency_places)
}, true).replace(this.$root.currency, '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + '</span>');
} else if (this.item.min_price !== this.item.max_price) {
return '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + "</span> "
+ floatformat(this.item.min_price, 2) + " "
+ floatformat(this.item.max_price, 2);
return '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + "</span> "
+ floatformat(this.item.min_price, this.$root.currency_places) + " "
+ floatformat(this.item.max_price, this.$root.currency_places);
} else if (this.item.min_price === "0.00" && this.item.max_price === "0.00") {
if (this.item.mandatory_priced_addons) {
return "\xA0"; // nbsp, because an empty string would cause the HTML element to collapse
}
return strings.free;
} else {
return '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + "</span> " + floatformat(this.item.min_price, 2);
return '<span class="pretix-widget-pricebox-currency">' + this.$root.currency + "</span> " + floatformat(this.item.min_price, this.$root.currency_places);
}
},
variationsToggleLabel: function () {
@@ -1949,6 +1949,7 @@ var shared_root_methods = {
root.location = data.location;
root.categories = data.items_by_category;
root.currency = data.currency;
root.currency_places = data.currency_places;
root.display_net_prices = data.display_net_prices;
root.voucher_explanation_text = data.voucher_explanation_text;
root.error = data.error;
@@ -1983,6 +1984,7 @@ var shared_root_methods = {
}, function (error) {
root.categories = [];
root.currency = '';
root.currency_places = 2;
if (error.status === 429) {
root.error = strings['loading_error_429'];
root.connection_error = true;
@@ -2339,6 +2341,7 @@ var create_widget = function (element, html_id=null) {
is_button: false,
categories: null,
currency: null,
currency_places: 2,
name: null,
date_range: null,
location: null,
@@ -22,6 +22,7 @@ export interface ProductListResponse {
location?: string
items_by_category?: Category[]
currency?: string
currency_places?: number
display_net_prices?: boolean
voucher_explanation_text?: string
error?: string
@@ -72,7 +72,7 @@ const pricerange = computed(() => {
STRINGS.price_from,
{
currency: store.currency,
price: floatformat(props.item.min_price || '0', 2),
price: floatformat(props.item.min_price || '0', store.currency_places),
},
true
).replace(
@@ -80,14 +80,14 @@ const pricerange = computed(() => {
`<span class="pretix-widget-pricebox-currency">${store.currency}</span>`
)
} else if (props.item.min_price !== props.item.max_price) {
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', 2)} ${floatformat(props.item.max_price || '0', 2)}`
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', store.currency_places)} ${floatformat(props.item.max_price || '0', store.currency_places)}`
} else if (props.item.min_price === '0.00' && props.item.max_price === '0.00') {
if (props.item.mandatory_priced_addons) {
return '\xA0' // nbsp, because an empty string would cause the HTML element to collapse
}
return STRINGS.free
} else {
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', 2)}`
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', store.currency_places)}`
}
})
@@ -25,9 +25,9 @@ const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.itemId
const displayPrice = computed(() => {
if (store.displayNetPrices) {
return floatformat(parseFloat(props.price.net), 2)
return floatformat(props.price.net, store.currency_places)
}
return floatformat(parseFloat(props.price.gross), 2)
return floatformat(props.price.gross, store.currency_places)
})
const displayPriceNonlocalized = computed(() => {
@@ -40,15 +40,15 @@ const displayPriceNonlocalized = computed(() => {
const suggestedPriceNonlocalized = computed(() => {
const price = props.suggestedPrice ?? props.price
if (store.displayNetPrices) {
return parseFloat(price.net).toFixed(2)
return parseFloat(price.net).toFixed(store.currency_places)
}
return parseFloat(price.gross).toFixed(2)
return parseFloat(price.gross).toFixed(store.currency_places)
})
// TODO BAD
const originalLine = computed(() => {
if (!props.originalPrice) return ''
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(parseFloat(props.originalPrice), 2)}`
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.originalPrice, store.currency_places)}`
})
// TODO BAD
@@ -71,6 +71,7 @@ export function createWidgetStore (config: {
frontpageText: null as string | null,
categories: [] as Category[],
currency: '',
currency_places: 2,
displayNetPrices: false,
voucherExplanationText: null as string | null,
displayAddToCart: false,
@@ -299,6 +300,7 @@ export function createWidgetStore (config: {
this.location = data.location ?? null
this.categories = data.items_by_category ?? []
this.currency = data.currency ?? ''
this.currency_places = data.currency_places ?? 2
this.displayNetPrices = data.display_net_prices ?? false
this.voucherExplanationText = data.voucher_explanation_text ?? null
this.error = data.error ?? null
@@ -338,6 +340,7 @@ export function createWidgetStore (config: {
} catch (e) {
this.categories = []
this.currency = ''
this.currency_places = 2
if (e instanceof ApiError && e.status === 429) {
this.error = STRINGS.loading_error_429
} else {
+25
View File
@@ -0,0 +1,25 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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/>.
#
def readonly_db(execute, sql, params, many, context):
if not sql.lower().startswith("select"):
raise Exception(f"Should not write anything to the database, but detected query: {sql}")
return execute(sql, params, many, context)
+40
View File
@@ -25,6 +25,7 @@ from unittest import mock
import pytest
from django.core.files.base import ContentFile
from django.db import connection
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
@@ -36,6 +37,7 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.base.models import (
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
)
from pretix.testutils.db import readonly_db
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
@@ -1739,3 +1741,41 @@ def test_exchange_create_gift_card(token_client, organizer, clist, event, order,
with scopes_disabled():
rm = ReusableMedium.objects.get(identifier="0412345")
assert rm.linked_giftcard.currency == "EUR"
@pytest.mark.django_db
def test_simulate(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
with connection.execute_wrapper(readonly_db):
resp = _redeem(token_client, organizer, clist, p.secret, {"simulate": True})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with scopes_disabled():
assert not p.checkins.exists()
@pytest.mark.django_db
def test_simulate_no_exchange(token_client, organizer, clist, event, order, item):
organizer.settings.reusable_media_type_nfc_uid = True
item.media_type = "nfc_uid"
item.media_policy = Item.MEDIA_POLICY_NEW
item.save()
with scopes_disabled():
rm = ReusableMedium.objects.create(
type="nfc_uid",
identifier="12345678",
organizer=organizer,
)
with connection.execute_wrapper(readonly_db):
resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", {
"source_type": "barcode",
"exchange_medium_type": "nfc_uid",
"exchange_medium_identifier": "12345678",
"simulate": True,
})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'error'
with scopes_disabled():
assert not rm.linked_orderpositions.exists()
+26
View File
@@ -20,13 +20,16 @@
# <https://www.gnu.org/licenses/>.
#
import base64
from datetime import datetime, timezone
import pytest
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from django_scopes import scopes_disabled
from freezegun import freeze_time
from pretix.base.models import Device
from pretix.base.models.devices import DeviceLastSeen
@pytest.fixture
@@ -386,3 +389,26 @@ def test_device_info_key_sets(device_client, device: Device):
base64.b64decode(ks['diversification_key']),
padding.PKCS1v15()
)
@pytest.mark.django_db
def test_update_last_seen(device_client, device: Device):
assert not DeviceLastSeen.objects.exists()
with freeze_time("2020-01-10T14:30:00+00:00"):
resp = device_client.get('/api/v1/device/info')
assert resp.status_code == 200
assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, tzinfo=timezone.utc)
with freeze_time("2020-01-10T14:30:05+00:00"):
resp = device_client.get('/api/v1/device/info')
assert resp.status_code == 200
# No update, interal too short
device.last_seen.refresh_from_db()
assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, tzinfo=timezone.utc)
with freeze_time("2020-01-10T14:30:30+00:00"):
resp = device_client.get('/api/v1/device/info')
assert resp.status_code == 200
device.last_seen.refresh_from_db()
assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, 30, tzinfo=timezone.utc)
+14 -14
View File
@@ -473,7 +473,7 @@ class QuotaTestCase(BaseQuotaTestCase):
@classscope(attr='o')
def test_waitinglist_auto_disable(self):
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=True)
RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=True)
)
self.quota.items.add(self.item1)
self.quota.size = 1
@@ -484,7 +484,7 @@ class QuotaTestCase(BaseQuotaTestCase):
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False)
RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=False)
)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
@@ -1192,7 +1192,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
self.order.status = Order.STATUS_EXPIRED
@@ -1347,7 +1347,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('last_order_modification_date', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.can_modify_answers
self.op2.subevent = se3
@@ -1363,7 +1363,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.event.date_from = datetime.datetime(2017, 5, 3, 12, 0, 0, tzinfo=datetime.timezone.utc)
self.event.save()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.payment_term_last == datetime.datetime(2017, 5, 1, 23, 59, 59, tzinfo=datetime.timezone.utc)
@@ -1379,7 +1379,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.payment_term_last > now()
self.op2.subevent = se3
@@ -1395,7 +1395,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.event.date_from = datetime.datetime(2017, 5, 3, 12, 0, 0, tzinfo=datetime.timezone.utc)
self.event.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.ticket_download_date == datetime.datetime(2017, 5, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
@@ -1411,7 +1411,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.ticket_download_date > now()
self.op2.subevent = se3
@@ -1582,13 +1582,13 @@ class OrderTestCase(BaseQuotaTestCase):
assert self.order.user_cancel_deadline is None
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline > now()
assert self.order.user_cancel_allowed
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline < now()
@@ -1607,7 +1607,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.save()
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline < now()
@@ -2018,13 +2018,13 @@ class OrderTestCase(BaseQuotaTestCase):
assert self.order.user_change_deadline is None
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline > now()
assert self.order.user_change_allowed
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline < now()
@@ -2046,7 +2046,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.save()
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline < now()
+2 -2
View File
@@ -204,7 +204,7 @@ def test_expiry_last_relative(event):
event.date_from = now() + timedelta(days=5)
event.save()
event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
@@ -245,7 +245,7 @@ def test_expiry_last_relative_subevents(event):
)
event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
order = _create_order(event, email='dummy@example.org', positions=[cp1, cp2],
now_dt=today,
+6 -6
View File
@@ -133,7 +133,7 @@ def test_availability_date_relative(event):
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
utc = datetime.timezone.utc
@@ -150,7 +150,7 @@ def test_availability_start_relative(event):
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', RelativeDateWrapper(
RelativeDate(days=2, time=datetime.time(12, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=datetime.time(12, 0), base_date_name='event__date_from', minutes=None)
))
utc = datetime.timezone.utc
@@ -193,12 +193,12 @@ def test_availability_date_cart_relative_subevents(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=3, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=3, time=None, base_date_name='event__date_from', minutes=None)
))
assert prov._is_available_by_time(cart_id="123")
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=4, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=4, time=None, base_date_name='event__date_from', minutes=None)
))
assert not prov._is_available_by_time(cart_id="123")
@@ -233,11 +233,11 @@ def test_availability_date_order_relative_subevents(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=3, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=3, time=None, base_date_name='event__date_from', minutes=None)
))
assert prov._is_available_by_time(order=order)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=4, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=4, time=None, base_date_name='event__date_from', minutes=None)
))
assert not prov._is_available_by_time(order=order)
+94 -36
View File
@@ -19,13 +19,13 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import datetime, time
from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
import pytest
from django_scopes import scope
from pretix.base.models import Event, Organizer
from pretix.base.models import Event, Order, Organizer
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
TOKYO = ZoneInfo('Asia/Tokyo')
@@ -40,7 +40,6 @@ def event():
date_from=datetime(2017, 12, 27, 5, 0, 0, tzinfo=TOKYO),
presale_start=datetime(2017, 12, 1, 5, 0, 0, tzinfo=TOKYO),
plugins='pretix.plugins.banktransfer'
)
event.settings.timezone = "Asia/Tokyo"
return event
@@ -56,57 +55,57 @@ def test_absolute_date(event):
@pytest.mark.django_db
def test_relative_date_without_time(event):
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None))
assert rdw.datetime(event).astimezone(TOKYO) == datetime(2017, 12, 26, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None, is_after=True))
assert rdw.to_string() == 'RELDATE/1/-/event__date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None, is_after=True))
assert rdw.datetime(event).astimezone(TOKYO) == datetime(2017, 12, 28, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/date_from/after'
assert rdw.to_string() == 'RELDATE/1/-/event__date_from/after'
@pytest.mark.django_db
def test_relative_date_other_base_point(event):
with scope(organizer=event.organizer):
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_start', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_start', minutes=None))
assert rdw.datetime(event) == datetime(2017, 11, 30, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/presale_start/'
assert rdw.to_string() == 'RELDATE/1/-/event__presale_start/'
# presale_end is unset, defaults to date_from
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_end', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_end', minutes=None))
assert rdw.datetime(event) == datetime(2017, 12, 26, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/presale_end/'
assert rdw.to_string() == 'RELDATE/1/-/event__presale_end/'
# subevent base
se = event.subevents.create(name="SE1", date_from=datetime(2017, 11, 27, 5, 0, 0, tzinfo=TOKYO))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 26, 5, 0, 0, tzinfo=TOKYO)
# presale_start is unset on subevent, default to event
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_start', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_start', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 30, 5, 0, 0, tzinfo=TOKYO)
# presale_end is unset on all, default to date_from of subevent
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_end', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_end', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 26, 5, 0, 0, tzinfo=TOKYO)
@pytest.mark.django_db
def test_relative_date_in_minutes(event):
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60))
assert rdw.to_string() == 'RELDATE/minutes/60/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=60))
assert rdw.to_string() == 'RELDATE/minutes/60/event__date_from/'
assert rdw.datetime(event) == datetime(2017, 12, 27, 4, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60, is_after=True))
assert rdw.to_string() == 'RELDATE/minutes/60/date_from/after'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=60, is_after=True))
assert rdw.to_string() == 'RELDATE/minutes/60/event__date_from/after'
assert rdw.datetime(event) == datetime(2017, 12, 27, 6, 0, 0, tzinfo=TOKYO)
@pytest.mark.django_db
def test_relative_date_with_time(event):
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/08:05:13/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/08:05:13/event__date_from/'
assert rdw.datetime(event) == datetime(2017, 12, 26, 8, 5, 13, tzinfo=TOKYO)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='date_from', minutes=None, is_after=True))
assert rdw.to_string() == 'RELDATE/1/08:05:13/date_from/after'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='event__date_from', minutes=None, is_after=True))
assert rdw.to_string() == 'RELDATE/1/08:05:13/event__date_from/after'
assert rdw.datetime(event) == datetime(2017, 12, 28, 8, 5, 13, tzinfo=TOKYO)
@@ -115,35 +114,94 @@ def test_relative_date_with_time_around_dst(event):
event.settings.timezone = "Europe/Berlin"
event.date_from = datetime(2020, 3, 29, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 3, 28, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 3, 29, 2, 30, 0, tzinfo=BERLIN)
event.date_from = datetime(2020, 10, 25, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 10, 24, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 10, 25, 2, 30, 0, tzinfo=BERLIN)
def test_unserialize():
def test_unserialize_backwards_compatibility():
d = datetime(2017, 12, 25, 10, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper.from_string(d.isoformat())
assert rdw.data == d
# keeping the test for the old from_string_format to ensure that we don't break anything
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_from/')
assert rdw.data == RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None)
# keeping the test for the old from_string_format to ensure that we don't break anything
rdw = RelativeDateWrapper.from_string('RELDATE/1/18:05:13/date_from/')
assert rdw.data == RelativeDate(days=1, time=time(18, 5, 13), base_date_name='date_from', minutes=None)
# keeping the test for the old from_string_format to ensure that we don't break anything
rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
def test_backwards_compatibility():
# the data model of RelativeDate had to be extended to support other models as relation target
# previously only:
# - date_from
# - date_to
# - date_admission
# - presale_start
# - presale_end
# where valid values for the fourth slot (base_date_names) of the serialized form.
# the relationship in this case always pointed at event
# so any preexisting base_date_names without __ should continue to work and upgrade to event__{old_base_date_name}
d = datetime(2017, 12, 25, 10, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper.from_string(d.isoformat())
assert rdw.data == d
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_from/')
assert rdw.data == RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None)
assert rdw.to_string() == 'RELDATE/1/-/event__date_from/'
rdw = RelativeDateWrapper.from_string('RELDATE/1/18:05:13/date_from/')
assert rdw.data == RelativeDate(days=1, time=time(18, 5, 13), base_date_name='date_from', minutes=None)
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_to/')
assert rdw.to_string() == 'RELDATE/1/-/event__date_to/'
rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_admission/')
assert rdw.to_string() == 'RELDATE/1/-/event__date_admission/'
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/presale_start/')
assert rdw.to_string() == 'RELDATE/1/-/event__presale_start/'
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/presale_end/')
assert rdw.to_string() == 'RELDATE/1/-/event__presale_end/'
# new order base_date_names should not work without __
with pytest.raises(TypeError):
RelativeDateWrapper.from_string('RELDATE/1/-/datetime/')
with pytest.raises(TypeError):
RelativeDateWrapper.from_string('RELDATE/1/-/expires/')
@pytest.mark.django_db
def test_relative_to_order(event):
with scope(organizer=event.organizer):
order_moment = datetime(2020, 3, 29, 18, 0, 0, tzinfo=TOKYO)
order = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=order_moment,
expires=order_moment + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='order__datetime', minutes=None, is_after=True))
assert rdw.datetime(order).astimezone(TOKYO) == datetime(2020, 3, 30, 18, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/order__datetime/after'
+1 -1
View File
@@ -235,7 +235,7 @@ class WaitingListTestCase(TestCase):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.set('waiting_list_auto', True)
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False)
RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=False)
)
self.event.save()
with scope(organizer=self.o):
+2 -2
View File
@@ -481,7 +481,7 @@ class EventsTest(SoupTest):
'payment_term_last_0': 'absolute',
'payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'),
'payment_term_last_2': '0',
'payment_term_last_3': 'date_from',
'payment_term_last_3': 'event__date_from',
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
@@ -496,7 +496,7 @@ class EventsTest(SoupTest):
'payment_term_last_0': 'relative',
'payment_term_last_1': '',
'payment_term_last_2': '10',
'payment_term_last_3': 'date_from',
'payment_term_last_3': 'event__date_from',
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
+28 -28
View File
@@ -230,19 +230,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -328,19 +328,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -403,19 +403,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -479,19 +479,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '02:30:00',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -567,19 +567,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -639,19 +639,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -711,19 +711,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
+25
View File
@@ -224,3 +224,28 @@ def test_one_view(logged_in_client, url, expected, event, item, item_category, o
)
response = logged_in_client.get(url)
assert response.status_code == expected
# Do not reintroduce any CSP nonces into control responses, as discussed in PR #6387
if response['Content-Type'] != 'application/json':
assert 'script-src' in response['Content-Security-Policy']
assert 'nonce-' not in response['Content-Security-Policy']
@pytest.mark.parametrize('url', [
'/control/login',
'/',
'/{orga}/{event}/',
])
@pytest.mark.django_db
def test_csp_header_unauthenticated(client, url, event):
# Do not reintroduce any CSP nonces into most presale responses, as discussed in PR #6387
with scope(organizer=event.organizer):
url = url.format(
event=event.slug, orga=event.organizer.slug,
)
event.live = True
event.save()
response = client.get(url)
assert response.status_code == 200
assert 'script-src' in response['Content-Security-Policy']
assert 'nonce-' not in response['Content-Security-Policy']
+4 -2
View File
@@ -22,6 +22,7 @@
import json
from datetime import timedelta
from decimal import Decimal
from unittest.mock import MagicMock
import pytest
from django.utils.timezone import now
@@ -409,7 +410,8 @@ def test_webhook_mark_paid(env, client, monkeypatch):
order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
pp_order = Result(get_test_order())
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
mock_orders_get_request = MagicMock(return_value=pp_order)
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
with scopes_disabled():
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
@@ -497,7 +499,7 @@ def test_webhook_mark_paid(env, client, monkeypatch):
"resource_version": "2.0"
}
), content_type='application_json')
mock_orders_get_request.assert_called_once_with('806440346Y391300T')
order.refresh_from_db()
assert order.status == Order.STATUS_PAID
+61
View File
@@ -697,6 +697,42 @@ class CartTest(CartTestMixin, TestCase):
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, 23)
def test_free_price_rounding(self):
self.ticket.free_price = True
self.ticket.save()
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '40.1234',
}, follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
cr1 = CartPosition.objects.get()
assert cr1.listed_price == Decimal('23.00')
assert cr1.custom_price_input == Decimal('40.12')
assert cr1.price == Decimal('40.12')
def test_free_price_rounding_jpy(self):
self.event.currency = "JPY"
self.event.save()
self.ticket.free_price = True
self.ticket.save()
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '40.1234',
}, follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
cr1 = CartPosition.objects.get()
assert cr1.listed_price == Decimal('23.00')
assert cr1.custom_price_input == Decimal('40.00')
assert cr1.price == Decimal('40.00')
def test_variation_inactive(self):
self.shirt_red.active = False
self.shirt_red.save()
@@ -3060,6 +3096,31 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp1.addons.count() == 3
assert all(a.price == Decimal('12.00') for a in cp1.addons.all())
@classscope(attr='orga')
def test_free_price_rounding(self):
self.event.settings.locales = ['de']
self.event.settings.locale = 'de'
self.event.currency = "JPY"
self.event.save()
self.workshop1.free_price = True
self.workshop1.save()
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() - timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), {
'cp_{}_item_{}'.format(cp1.pk, self.workshop1.pk): '1',
'cp_{}_item_{}_price'.format(cp1.pk, self.workshop1.pk): '99,99',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
assert cp1.addons.count() == 1
assert cp1.addons.first().item == self.workshop1
assert cp1.addons.first().price == Decimal('100')
@classscope(attr='orga')
def test_change_number(self):
cp1 = CartPosition.objects.create(
+1 -1
View File
@@ -1132,7 +1132,7 @@ class WaitingListTest(EventTestMixin, SoupTest):
def test_auto_disable(self):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='date_from', minutes=None, is_after=False)
RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='event__date_from', minutes=None, is_after=False)
)
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
+1 -1
View File
@@ -1527,7 +1527,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self.order.save()
self._subevent_setup()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
response = self.client.get(
+1 -1
View File
@@ -1076,7 +1076,7 @@ class OrdersTest(BaseOrdersTest):
self.event.date_from = now() + datetime.timedelta(days=3)
self.event.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(RelativeDate(
base_date_name='date_from', days=2, time=None, minutes=None
base_date_name='event__date_from', days=2, time=None, minutes=None
)))
response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
+4
View File
@@ -173,6 +173,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -379,6 +380,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -439,6 +441,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -524,6 +527,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>',
"show_variations_expanded": False,
"display_net_prices": False,