Compare commits

...
36 Commits
Author SHA1 Message Date
Raphael Michel 2b40c9f44c Bump django-oauth-toolkit to 2.2.* 2022-12-17 20:08:12 +01:00
Raphael Michel ad1dab3b7f Bank transfer: Fix refund export when plugin is disabled 2022-12-13 18:40:26 +01:00
Raphael Michel 930abe0cc5 Fix crash in gift card view (PRETIXEU-493) 2022-12-13 18:32:22 +01:00
Raphael Michel ba2cc56c82 Radio collapse elements: Deal with Firefox keeping form state on reload 2022-12-13 10:54:18 +01:00
Raphael Michel cb1f63bf80 Fix regression in address validation for resellers 2022-12-12 17:21:55 +01:00
aab7042cda PPv2: Simulate cart_payments in XHR-calls; only look at multi_use-payments for remaining value calculation (#2970)
Co-authored-by: Raphael Michel <michel@rami.io>
2022-12-12 15:35:16 +01:00
Raphael MichelandGitHub 495a21c683 GitHub actions: Ignore flake8 no longer supporting Python 3.7 (#2971) 2022-12-12 15:29:47 +01:00
Martin Gross 86b5ba6937 PPv2: Actually log dict-representation on value mismatch 2022-12-12 12:44:07 +01:00
Raphael MichelandGitHub 3d9679a144 Allow variations to override item meta data (#2965) 2022-12-12 12:06:09 +01:00
Raphael Michel 5f899ed5c5 Bump chardet to 5.1.* 2022-12-12 12:03:27 +01:00
Raphael Michel 47dabc1fe7 Bump pytest-xdist to 3.1.* 2022-12-12 10:53:32 +01:00
Raphael MichelandRaphael Michel 2d7c4a3d42 Translations: Add Croatian 2022-12-12 10:53:09 +01:00
Raphael MichelandRaphael Michel 51ef98f736 Translations: Add Croatian 2022-12-12 10:53:09 +01:00
Mie FrydensbjergandRaphael Michel 2d7d2b1a90 Translations: Update Danish
Currently translated at 71.1% (143 of 201 strings)

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

powered by weblate
2022-12-12 10:53:09 +01:00
Mie FrydensbjergandRaphael Michel cede7ba3aa Translations: Update Danish
Currently translated at 35.6% (1755 of 4919 strings)

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

powered by weblate
2022-12-12 10:53:09 +01:00
Raphael Michel 4fd8726b05 Bump flake8 to 6.0.*, pycodestyle to 2.10.* and pyflakes to 3.0.* 2022-12-12 10:53:01 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b344ce90ba Bump vue and vue-template-compiler in /src/pretix/static/npm_dir (#2940)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 09:25:36 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
69dc7f56e5 Bump @babel/core from 7.19.6 to 7.20.5 in /src/pretix/static/npm_dir (#2941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 09:25:23 +01:00
Raphael Michel 247a61489f XLSX generation: Remove invalid unicode characters 2022-12-09 17:41:33 +01:00
Raphael Michel 979d23e997 Invoice renderer: Unify HTML cleaning and clean intro and additional
text
2022-12-09 17:30:26 +01:00
Raphael Michel 28e529995d Add missing license headers 2022-12-09 13:24:17 +01:00
Raphael Michel a982cbf6b6 Name field: Improve compatibility with old formats 2022-12-09 10:42:26 +01:00
Raphael Michel f1c2ae5b6b Revert "Bump pycodestyle to 2.10.*"
This reverts commit dfe3454915.
2022-12-08 14:17:22 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5b27ac66f9 Bump decode-uri-component from 0.2.0 to 0.2.2 in /src/pretix/static/npm_dir (#2952)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 13:50:47 +01:00
Raphael Michel c71ac2141f Bump drf_ujson2 to 1.7.* 2022-12-08 13:50:20 +01:00
Raphael Michel e59498d65d Bump pytest-rerunfailures to 10.* 2022-12-08 13:50:20 +01:00
Raphael Michel dfe3454915 Bump pycodestyle to 2.10.* 2022-12-08 13:50:20 +01:00
Raphael MichelandGitHub b64c5735a8 Make str.format_map with untrusted input safer (#2931) 2022-12-08 13:49:07 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11eecd739d Bump @rollup/plugin-babel from 6.0.2 to 6.0.3 in /src/pretix/static/npm_dir (#2942)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 13:47:34 +01:00
Raphael Michel 07a6d4898a Fix missing Discount.is_available_by_time method 2022-12-08 10:53:00 +01:00
Raphael Michel a759e23504 Docs: Add internal_name to digital content api 2022-12-08 10:48:13 +01:00
Richard SchreiberandGitHub 3eaf05502a Checkout: copy answers from previous item instead of first (#Z23112272) 2022-12-07 09:24:24 +01:00
04df1c2032 Introduce country-specific address validation (#2945)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2022-12-05 12:42:46 +01:00
Raphael MichelandGitHub 6a8df75a9f Fix regression in handling gift card payments (#2936) 2022-12-05 11:32:27 +01:00
Richard SchreiberandGitHub 547cfdffd6 PDF editor: Reduce precision size of empty page (Z#23112472) (#2935) 2022-12-01 13:19:21 +01:00
Raphael Michel f72a0b4c09 Bump version to 4.16.0.dev0 2022-11-30 09:53:57 +01:00
69 changed files with 28349 additions and 518 deletions
+2 -2
View File
@@ -23,10 +23,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
+4 -4
View File
@@ -21,10 +21,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -47,10 +47,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
+6 -6
View File
@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -42,10 +42,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -63,10 +63,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mariadb-client
- name: Install Python dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
working-directory: ./src
- name: Run checks
run: python manage.py check
+16 -5
View File
@@ -43,8 +43,13 @@ available_until datetime The last date t
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
meta_data object Values set for event-specific meta data parameters.
===================================== ========================== =======================================================
.. versionchanged:: 4.16
The ``meta_data`` attribute has been added.
Endpoints
---------
@@ -94,6 +99,7 @@ Endpoints
"default_price": "223.00",
"price": 223.0,
"original_price": null,
"meta_data": {}
},
{
"id": 3,
@@ -108,7 +114,8 @@ Endpoints
"description": {},
"position": 1,
"default_price": null,
"price": 15.0
"price": 15.0,
"meta_data": {}
}
]
}
@@ -161,7 +168,8 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
"position": 0,
"meta_data": {}
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -198,7 +206,8 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
"position": 0,
"meta_data": {}
}
**Example response**:
@@ -225,7 +234,8 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
"position": 0,
"meta_data": {}
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a variation for
@@ -283,7 +293,8 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
"position": 1,
"meta_data": {}
}
:param organizer: The ``slug`` field of the organizer to modify
+15
View File
@@ -123,6 +123,7 @@ variations list of objects A list with one
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
├ meta_data object Values set for event-specific meta data parameters.
└ position integer An integer, used for sorting
addons list of objects Definition of add-ons that can be chosen for this item.
Only writable during creation,
@@ -155,6 +156,10 @@ meta_data object Values set for
The attributes ``require_membership_hidden`` attribute has been added.
.. versionchanged:: 4.16
The ``variations[x].meta_data`` attribute has been added.
Notes
-----
@@ -247,6 +252,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 0
},
{
@@ -262,6 +268,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 1
}
],
@@ -361,6 +368,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"meta_data": {},
"position": 0
},
{
@@ -376,6 +384,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 1
}
],
@@ -455,6 +464,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 0
},
{
@@ -470,6 +480,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 1
}
],
@@ -538,6 +549,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 0
},
{
@@ -553,6 +565,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 1
}
],
@@ -652,6 +665,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 0
},
{
@@ -667,6 +681,7 @@ Endpoints
"available_until": null,
"hide_without_voucher": false,
"description": null,
"meta_data": {},
"position": 1
}
],
+1
View File
@@ -91,6 +91,7 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal content ID
title multi-lingual string The content title (required)
internal_name string An optional name that is only used in the backend
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
url string The location of the digital content
file file A downloadable file. Either ``url`` or ``file`` must be ``null``.
+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__ = "4.15.0"
__version__ = "4.16.0.dev0"
@@ -0,0 +1,77 @@
# Generated by Django 3.2.16 on 2022-12-17 18:47
import uuid
import django.db.models.deletion
import oauth2_provider.generators
import oauth2_provider.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0226_itemvariationmetavalue'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('pretixapi', '0008_webhookcallretry'),
]
run_before = [
('oauth2_provider', '0002_auto_20190406_1805'),
]
operations = [
migrations.AddField(
model_name='oauthapplication',
name='algorithm',
field=models.CharField(default='', max_length=5),
),
migrations.AddField(
model_name='oauthgrant',
name='claims',
field=models.TextField(default=''),
preserve_default=False,
),
migrations.AddField(
model_name='oauthgrant',
name='code_challenge',
field=models.CharField(default='', max_length=128),
),
migrations.AddField(
model_name='oauthgrant',
name='code_challenge_method',
field=models.CharField(default='', max_length=10),
),
migrations.AddField(
model_name='oauthgrant',
name='nonce',
field=models.CharField(default='', max_length=255),
),
migrations.AlterField(
model_name='oauthapplication',
name='client_secret',
field=oauth2_provider.models.ClientSecretField(db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255),
),
migrations.CreateModel(
name='OAuthIDToken',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('jti', models.UUIDField(default=uuid.uuid4, unique=True)),
('expires', models.DateTimeField()),
('scope', models.TextField()),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('organizers', models.ManyToManyField(to='pretixbase.Organizer')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pretixapi_oauthidtoken', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='oauthaccesstoken',
name='id_token',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to='pretixapi.oauthidtoken'),
),
]
+17 -3
View File
@@ -29,8 +29,8 @@ from oauth2_provider.generators import (
generate_client_id, generate_client_secret,
)
from oauth2_provider.models import (
AbstractAccessToken, AbstractApplication, AbstractGrant,
AbstractRefreshToken,
AbstractAccessToken, AbstractApplication, AbstractGrant, AbstractIDToken,
AbstractRefreshToken, ClientSecretField,
)
from oauth2_provider.validators import URIValidator
@@ -46,7 +46,7 @@ class OAuthApplication(AbstractApplication):
verbose_name=_("Client ID"),
max_length=100, unique=True, default=generate_client_id, db_index=True
)
client_secret = models.CharField(
client_secret = ClientSecretField(
verbose_name=_("Client secret"),
max_length=255, blank=False, default=generate_client_secret, db_index=True
)
@@ -67,12 +67,26 @@ class OAuthGrant(AbstractGrant):
redirect_uri = models.CharField(max_length=2500) # Only 255 in AbstractGrant, which caused problems
class OAuthIDToken(AbstractIDToken):
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE,
)
organizers = models.ManyToManyField('pretixbase.Organizer')
class OAuthAccessToken(AbstractAccessToken):
source_refresh_token = models.OneToOneField(
# unique=True implied by the OneToOneField
'OAuthRefreshToken', on_delete=models.SET_NULL, blank=True, null=True,
related_name="refreshed_access_token"
)
id_token = models.OneToOneField(
OAuthIDToken,
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="access_token",
)
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE, blank=True, null=True,
)
+72 -3
View File
@@ -47,13 +47,14 @@ from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
Question, QuestionOption, Quota,
ItemVariationMetaValue, Question, QuestionOption, Quota,
)
class InlineItemVariationSerializer(I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
coerce_to_string=True)
meta_data = MetaDataField(required=False, source='*')
class Meta:
model = ItemVariation
@@ -61,16 +62,23 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
'position', 'default_price', 'price', 'original_price', 'require_approval',
'require_membership', 'require_membership_types',
'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
if key not in self.parent.parent.item_meta_properties:
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
return value
class ItemVariationSerializer(I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
coerce_to_string=True)
meta_data = MetaDataField(required=False, source='*')
class Meta:
model = ItemVariation
@@ -78,12 +86,63 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
'position', 'default_price', 'price', 'original_price', 'require_approval',
'require_membership', 'require_membership_types',
'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
variation = ItemVariation.objects.create(**validated_data)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
ItemVariationMetaValue.objects.create(
property=self.item_meta_properties.get(key),
value=value,
variation=variation
)
return variation
@cached_property
def item_meta_properties(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
if key not in self.item_meta_properties:
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
return value
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
variation = super().update(instance, validated_data)
# Meta data
if meta_data is not None:
current = {mv.property: mv for mv in variation.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.item_meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
variation.meta_values.create(
property=self.item_meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
return variation
class InlineItemBundleSerializer(serializers.ModelSerializer):
class Meta:
@@ -263,9 +322,19 @@ class ItemSerializer(I18nAwareModelSerializer):
for variation_data in variations_data:
require_membership_types = variation_data.pop('require_membership_types', [])
var_meta_data = variation_data.pop('meta_data', {})
v = ItemVariation.objects.create(item=item, **variation_data)
if require_membership_types:
v.require_membership_types.add(*require_membership_types)
if var_meta_data is not None:
for key, value in var_meta_data.items():
ItemVariationMetaValue.objects.create(
property=self.item_meta_properties.get(key),
value=value,
variation=v
)
for addon_data in addons_data:
ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data:
+13 -4
View File
@@ -371,10 +371,19 @@ class PdfDataSerializer(serializers.Field):
for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v
if not hasattr(instance.item, '_cached_meta_data'):
instance.item._cached_meta_data = instance.item.meta_data
for k, v in instance.item._cached_meta_data.items():
res['itemmeta:' + k] = v
if instance.variation_id:
print(instance, instance.variation, instance.variation_id, instance.item)
if not hasattr(instance.variation, '_cached_meta_data'):
instance.variation.item = instance.item # saves some database lookups
instance.variation._cached_meta_data = instance.variation.meta_data
print(instance.variation._cached_meta_data.items())
for k, v in instance.variation._cached_meta_data.items():
res['itemmeta:' + k] = v
else:
if not hasattr(instance.item, '_cached_meta_data'):
instance.item._cached_meta_data = instance.item.meta_data
for k, v in instance.item._cached_meta_data.items():
res['itemmeta:' + k] = v
res['images'] = {}
+8 -2
View File
@@ -84,7 +84,9 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.items.select_related('tax_rule').prefetch_related(
'variations', 'addons', 'bundles', 'meta_values'
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
'variations__meta_values', 'variations__meta_values__property',
'require_membership_types', 'variations__require_membership_types',
).all()
def perform_create(self, serializer):
@@ -147,7 +149,11 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
return self.item.variations.all()
return self.item.variations.all().prefetch_related(
'meta_values',
'meta_values__property',
'require_membership_types'
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
+12 -5
View File
@@ -65,9 +65,10 @@ from pretix.api.views import RichOrderingFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
Invoice, InvoiceAddress, ItemMetaValue, Order, OrderFee, OrderPayment,
OrderPosition, OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule,
TeamAPIToken, generate_secret,
Invoice, InvoiceAddress, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Order, OrderFee, OrderPayment, OrderPosition,
OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, TeamAPIToken,
generate_secret,
)
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
from pretix.base.payment import PaymentException
@@ -232,7 +233,9 @@ class OrderViewSet(viewsets.ModelViewSet):
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)),
'variation',
Prefetch('variation', queryset=ItemVariation.objects.prefetch_related(
Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)),
'answers', 'answers__options', 'answers__question',
'item__category',
'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question',
@@ -999,7 +1002,11 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
'variation', 'answers', 'answers__options', 'answers__question',
Prefetch('variation', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
'answers', 'answers__options', 'answers__question',
'item__category',
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached',
+227
View File
@@ -0,0 +1,227 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from localflavor.ar.forms import ARPostalCodeField
from localflavor.at.forms import ATZipCodeField
from localflavor.au.forms import AUPostCodeField
from localflavor.be.forms import BEPostalCodeField
from localflavor.br.forms import BRZipCodeField
from localflavor.ca.forms import CAPostalCodeField
from localflavor.ch.forms import CHZipCodeField
from localflavor.cn.forms import CNPostCodeField
from localflavor.cu.forms import CUPostalCodeField
from localflavor.cz.forms import CZPostalCodeField
from localflavor.de.forms import DEZipCodeField
from localflavor.dk.forms import DKPostalCodeField
from localflavor.ee.forms import EEZipCodeField
from localflavor.es.forms import ESPostalCodeField
from localflavor.fi.forms import FIZipCodeField
from localflavor.fr.forms import FRZipCodeField
from localflavor.gb.forms import GBPostcodeField
from localflavor.gr.forms import GRPostalCodeField
from localflavor.hr.forms import HRPostalCodeField
from localflavor.id_.forms import IDPostCodeField
from localflavor.ie.forms import EircodeField
from localflavor.il.forms import ILPostalCodeField
from localflavor.in_.forms import INZipCodeField
from localflavor.ir.forms import IRPostalCodeField
from localflavor.is_.is_postalcodes import IS_POSTALCODES
from localflavor.it.forms import ITZipCodeField
from localflavor.jp.forms import JPPostalCodeField
from localflavor.lt.forms import LTPostalCodeField
from localflavor.lv.forms import LVPostalCodeField
from localflavor.ma.forms import MAPostalCodeField
from localflavor.mt.forms import MTPostalCodeField
from localflavor.mx.forms import MXZipCodeField
from localflavor.nl.forms import NLZipCodeField
from localflavor.no.forms import NOZipCodeField
from localflavor.nz.forms import NZPostCodeField
from localflavor.pk.forms import PKPostCodeField
from localflavor.pl.forms import PLPostalCodeField
from localflavor.pt.forms import PTZipCodeField
from localflavor.ro.forms import ROPostalCodeField
from localflavor.ru.forms import RUPostalCodeField
from localflavor.se.forms import SEPostalCodeField
from localflavor.sg.forms import SGPostCodeField
from localflavor.si.si_postalcodes import SI_POSTALCODES
from localflavor.sk.forms import SKPostalCodeField
from localflavor.tr.forms import TRPostalCodeField
from localflavor.ua.forms import UAPostalCodeField
from localflavor.us.forms import USZipCodeField
from localflavor.za.forms import ZAPostCodeField
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
_validator_classes = defaultdict(list)
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED = {
# We don't presume this for countries we don't have knowledge about, there are countries in the
# world e.g. without zipcodes
'AR', 'AT', 'AU', 'BE', 'BR', 'CA', 'CH', 'CN', 'CU', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR',
'GB', 'GR', 'HR', 'ID', 'IE', 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'LT', 'LV', 'MA', 'MT', 'MX',
'NL', 'NO', 'NZ', 'PK', 'PL', 'PT', 'RO', 'RU', 'SE', 'SG', 'SI', 'SK', 'TR', 'UA', 'US', 'ZA',
}
def validate_address(address: dict, all_optional=False):
"""
:param address: A dictionary with at least the entries ``street``, ``zipcode``, ``city``, ``country``,
``state``
:return: The dictionary, possibly with changes
"""
if not address.get('street') and not address.get('zipcode') and not address.get('city'):
# Consider the actual address part to be empty, no further validation necessary, if the
# address should be required, it's the callers job to validate that at least one of these
# fields is filled
return address
if not address.get('country'):
raise ValidationError({'country': [_('This field is required.')]})
if str(address['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS and not address.get('state') and not all_optional:
raise ValidationError({'state': [_('This field is required.')]})
if str(address['country']) in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED and not all_optional:
for f in ('street', 'zipcode', 'city'):
if not address.get(f):
raise ValidationError({f: [_('This field is required.')]})
for klass in _validator_classes[str(address['country'])]:
validator = klass()
try:
if address.get('zipcode'):
address['zipcode'] = validator.validate_zipcode(address['zipcode'])
except ValidationError as e:
raise ValidationError({'zipcode': list(e)})
return address
def register_validator_for(country):
def inner(klass):
_validator_classes[country].append(klass)
return klass
return inner
class BaseValidator:
required_fields = []
def validate_zipcode(self, value):
return value
"""
Currently, mostly have validators that are auto-generated from django-localflavor
but custom ones can be added like this:
@register_validator_for('DE')
class DEValidator(BaseValidator):
def validate_zipcode(value):
return value
In the future, we can also add additional methods to validate that e.g. a city
is plausible for a given zip code.
"""
_zip_code_fields = {
'AR': ARPostalCodeField,
'AT': ATZipCodeField,
'AU': AUPostCodeField,
'BE': BEPostalCodeField,
'BR': BRZipCodeField,
'CA': CAPostalCodeField,
'CH': CHZipCodeField,
'CN': CNPostCodeField,
'CU': CUPostalCodeField,
'CZ': CZPostalCodeField,
'DE': DEZipCodeField,
'DK': DKPostalCodeField,
'EE': EEZipCodeField,
'ES': ESPostalCodeField,
'FI': FIZipCodeField,
'FR': FRZipCodeField,
'GB': GBPostcodeField,
'GR': GRPostalCodeField,
'HR': HRPostalCodeField,
'ID': IDPostCodeField,
'IE': EircodeField,
'IL': ILPostalCodeField,
'IN': INZipCodeField,
'IR': IRPostalCodeField,
'IT': ITZipCodeField,
'JP': JPPostalCodeField,
'LT': LTPostalCodeField,
'LV': LVPostalCodeField,
'MA': MAPostalCodeField,
'MT': MTPostalCodeField,
'MX': MXZipCodeField,
'NL': NLZipCodeField,
'NO': NOZipCodeField,
'NZ': NZPostCodeField,
'PK': PKPostCodeField,
'PL': PLPostalCodeField,
'PT': PTZipCodeField,
'RO': ROPostalCodeField,
'RU': RUPostalCodeField,
'SE': SEPostalCodeField,
'SG': SGPostCodeField,
'SK': SKPostalCodeField,
'TR': TRPostalCodeField,
'UA': UAPostalCodeField,
'US': USZipCodeField,
'ZA': ZAPostCodeField,
}
def _generate_class_from_zipcode_field(field_class):
class _GeneratedValidator(BaseValidator):
def validate_zipcode(self, value):
return field_class().clean(value)
return _GeneratedValidator
for cc, field_class in _zip_code_fields.items():
register_validator_for(cc)(_generate_class_from_zipcode_field(field_class))
@register_validator_for('IS')
class ISValidator(BaseValidator):
def validate_zipcode(self, value):
if value not in [entry[0] for entry in IS_POSTALCODES]:
raise ValidationError(_('Enter a postal code in the format XXX.'), code='invalid')
return value
@register_validator_for('SI')
class SIValidator(BaseValidator):
def validate_zipcode(self, value):
try:
if int(value) not in [entry[0] for entry in SI_POSTALCODES]:
raise ValidationError(_('Enter a postal code in the format XXXX.'), code='invalid')
except ValueError:
raise ValidationError(_('Enter a postal code in the format XXXX.'), code='invalid')
return value
+14 -4
View File
@@ -29,7 +29,7 @@ from openpyxl.utils import get_column_letter
from ...helpers.safe_openpyxl import SafeCell
from ..channels import get_all_sales_channels
from ..exporter import ListExporter
from ..models import ItemMetaValue
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
from ..signals import register_data_exporters
@@ -106,18 +106,27 @@ class ItemDataExporter(ListExporter):
yield row
for i in self.event.items.prefetch_related(
'variations',
Prefetch(
'meta_values',
ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
)
),
Prefetch(
'variations',
queryset=ItemVariation.objects.prefetch_related(
Prefetch(
'meta_values',
ItemVariationMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
),
),
),
).select_related('category', 'tax_rule'):
m = i.meta_data
vars = list(i.variations.all())
if vars:
for v in vars:
m = v.meta_data
row = [
i.pk,
v.pk,
@@ -160,6 +169,7 @@ class ItemDataExporter(ListExporter):
yield row
else:
m = i.meta_data
row = [
i.pk,
"",
+20 -1
View File
@@ -36,9 +36,11 @@ import json
from decimal import Decimal
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Prefetch
from django.dispatch import receiver
from ..exporter import BaseExporter
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
from ..signals import register_data_exporters
@@ -106,9 +108,26 @@ class JSONExporter(BaseExporter):
'available_from': variation.available_from,
'available_until': variation.available_until,
'hide_without_voucher': variation.hide_without_voucher,
'meta_data': variation.meta_data,
} for variation in item.variations.all()
]
} for item in self.event.items.select_related('tax_rule').prefetch_related('variations')
} for item in self.event.items.select_related('tax_rule').prefetch_related(
Prefetch(
'meta_values',
ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
),
Prefetch(
'variations',
queryset=ItemVariation.objects.prefetch_related(
Prefetch(
'meta_values',
ItemVariationMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
),
),
),
)
],
'questions': [
{
+10 -3
View File
@@ -135,6 +135,10 @@ class NamePartsWidget(forms.MultiWidget):
data.append(value.get(fname, ""))
if '_legacy' in value and not data[-1]:
data[-1] = value.get('_legacy', '')
elif not any(d for d in data) and '_scheme' in value:
scheme = PERSON_NAME_SCHEMES[value['_scheme']]
data[-1] = scheme['concatenation'](value).strip()
return data
def render(self, name: str, value, attrs=None, renderer=None) -> str:
@@ -915,6 +919,7 @@ class BaseQuestionsForm(forms.Form):
class BaseInvoiceAddressForm(forms.ModelForm):
vat_warning = False
address_validation = False
class Meta:
model = InvoiceAddress
@@ -1050,6 +1055,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
def clean(self):
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
@@ -1065,9 +1073,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
if data.get('city') and data.get('country') and str(data['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not data.get('state'):
self.add_error('state', _('This field is required.'))
if self.address_validation:
self.cleaned_data = data = validate_address(data, self.all_optional)
self.instance.name_parts = data.get('name_parts')
+18 -12
View File
@@ -242,6 +242,12 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
buffer.seek(0)
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return bleach.clean(
text,
tags=tags or []
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
@@ -266,7 +272,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
p = Paragraph(self._clean_text(self.invoice.address_invoice_to),
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
@@ -279,7 +285,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_invoice_from(self, canvas):
p = Paragraph(
bleach.clean(self.invoice.full_invoice_from, tags=[]).strip().replace('\n', '<br />\n'),
self._clean_text(self.invoice.full_invoice_from),
style=self.stylesheet['InvoiceFrom']
)
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -474,8 +480,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.custom_field:
story.append(Paragraph(
'{}: {}'.format(
bleach.clean(str(self.invoice.event.settings.invoice_address_custom_field), tags=[]).strip().replace('\n', '<br />\n'),
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
self._clean_text(self.invoice.custom_field),
),
self.stylesheet['Normal']
))
@@ -483,7 +489,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=bleach.clean(self.invoice.internal_reference, tags=[]).strip().replace('\n', '<br />\n'),
reference=self._clean_text(self.invoice.internal_reference),
),
self.stylesheet['Normal']
))
@@ -491,20 +497,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ': ' +
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' +
bleach.clean(self.invoice.invoice_to_beneficiary, tags=[]).replace("\n", "<br />\n"),
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal']
))
if self.invoice.introductory_text:
story.append(Paragraph(
self.invoice.introductory_text,
self._clean_text(self.invoice.introductory_text, tags=['br']),
self.stylesheet['Normal']
))
story.append(Spacer(1, 10 * mm))
@@ -571,7 +577,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description = description + "\n" + single_price_line
tdata.append((
Paragraph(
bleach.clean(description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
str(len(lines)),
@@ -587,7 +593,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description = description + "\n" + single_price_line
tdata.append((
Paragraph(
bleach.clean(description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
str(len(lines)),
@@ -657,7 +663,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.additional_text:
story.append(Paragraph(
self.invoice.additional_text,
self._clean_text(self.invoice.additional_text, tags=['br']),
self.stylesheet['Normal']
))
story.append(Spacer(1, 5 * mm))
@@ -794,7 +800,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
if not self.invoice.invoice_from:
return
c = [
bleach.clean(l, tags=[]).strip().replace('\n', '<br />\n')
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
@@ -0,0 +1,29 @@
# Generated by Django 3.2.16 on 2022-12-09 10:06
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0225_orderpayment_process_initiated'),
]
operations = [
migrations.CreateModel(
name='ItemVariationMetaValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('value', models.TextField()),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variation_values', to='pretixbase.itemmetaproperty')),
('variation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.itemvariation')),
],
options={
'unique_together': {('variation', 'property')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]
+2 -2
View File
@@ -34,8 +34,8 @@ from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
ItemVariation, Question, QuestionOption, Quota, SubEventItem,
SubEventItemVariation, itempicture_upload_to,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SubEventItem, SubEventItemVariation, itempicture_upload_to,
)
from .log import LogEntry
from .memberships import Membership, MembershipType
+9
View File
@@ -28,6 +28,7 @@ from typing import Dict, Optional, Tuple
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
@@ -198,6 +199,14 @@ class Discount(LoggedModel):
'subevent_mode': self.subevent_mode,
})
def is_available_by_time(self, now_dt=None) -> bool:
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
return False
return True
def _apply_min_value(self, positions, idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in idx_group) < self.condition_min_value:
return
+9 -3
View File
@@ -728,7 +728,7 @@ class Event(EventMixin, LoggedModel):
from ..signals import event_copy_data
from . import (
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
Question, Quota,
ItemVariationMetaValue, Question, Quota,
)
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
@@ -804,12 +804,18 @@ class Event(EventMixin, LoggedModel):
v.item = i
v.save(force_insert=True)
for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'):
for imv in ItemMetaValue.objects.filter(item__event=other):
imv.pk = None
imv.property = item_meta_properties_map[imv.property.pk]
imv.property = item_meta_properties_map[imv.property_id]
imv.item = item_map[imv.item.pk]
imv.save(force_insert=True)
for imv in ItemVariationMetaValue.objects.filter(variation__item__event=other):
imv.pk = None
imv.property = item_meta_properties_map[imv.property_id]
imv.variation = variation_map[imv.variation_id]
imv.save(force_insert=True)
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
ia.pk = None
ia.base_item = item_map[ia.base_item.pk]
+27 -4
View File
@@ -1008,6 +1008,16 @@ class ItemVariation(models.Model):
return False
return True
@property
def meta_data(self):
data = self.item.meta_data
if hasattr(self, 'meta_values_cached'):
data.update({v.property.name: v.value for v in self.meta_values_cached})
else:
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))
class ItemAddOn(models.Model):
"""
@@ -1787,8 +1797,21 @@ class ItemMetaValue(LoggedModel):
class Meta:
unique_together = ('item', 'property')
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
class ItemVariationMetaValue(LoggedModel):
"""
A meta-data value assigned to an item variation, overriding the value on the item.
:param variation: The variation this metadata is valid for
:type variation: ItemVariation
:param property: The property this value belongs to
:type property: ItemMetaProperty
:param value: The actual value
:type value: str
"""
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE, related_name='meta_values')
property = models.ForeignKey('ItemMetaProperty', on_delete=models.CASCADE, related_name='variation_values')
value = models.TextField()
class Meta:
unique_together = ('variation', 'property')
+10 -8
View File
@@ -80,6 +80,7 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
@@ -996,7 +997,7 @@ class Order(LockModel, LoggedModel):
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import (
SendMailException, TolerantDict, mail, render_mail,
SendMailException, mail, render_mail,
)
if not self.email and not (position and position.attendee_email):
@@ -1012,7 +1013,7 @@ class Order(LockModel, LoggedModel):
try:
email_content = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
@@ -1651,7 +1652,7 @@ class OrderPayment(models.Model):
}, user=user, auth=auth)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_date=None):
ignore_date=False, lock=True, payment_date=None, generate_invoice=True):
"""
Marks the payment as complete. If possible, this also marks the order as paid if no further
payment is required
@@ -1714,10 +1715,11 @@ class OrderPayment(models.Model):
))
return
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum)
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
generate_invoice)
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_refund_sum=0):
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
)
@@ -1734,7 +1736,7 @@ class OrderPayment(models.Model):
ignore_date=ignore_date)
invoice = None
if invoice_qualified(self.order):
if invoice_qualified(self.order) and allow_generate_invoice:
invoices = self.order.invoices.filter(is_cancellation=False).count()
cancellations = self.order.invoices.filter(is_cancellation=True).count()
gen_invoice = (
@@ -2413,7 +2415,7 @@ class OrderPosition(AbstractPosition):
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import (
SendMailException, TolerantDict, mail, render_mail,
SendMailException, mail, render_mail,
)
if not self.attendee_email:
@@ -2426,7 +2428,7 @@ class OrderPosition(AbstractPosition):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
+8 -5
View File
@@ -68,6 +68,7 @@ from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map
from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.views import get_cart, get_cart_total
@@ -175,7 +176,9 @@ class BasePaymentProvider:
"""
Set this to ``True`` if your ``execute_payment`` function needs to be triggered by a user request, i.e. either
needs the ``request`` object or might require a browser redirect. If this is ``False``, you will not receive
a ``request`` and may not redirect since execute_payment might be called server-side.
a ``request`` and may not redirect since execute_payment might be called server-side. You should ensure that
your ``execute_payment`` method has a limited execution time (i.e. by using ``timeout`` for all external calls)
and handles all error cases appropriately.
"""
return True
@@ -1120,12 +1123,12 @@ class ManualPayment(BasePaymentProvider):
}
def order_pending_mail_render(self, order, payment) -> str:
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order, payment))
msg = format_map(self.settings.get('email_instructions', as_type=LazyI18nString), self.format_map(order, payment))
return msg
def payment_pending_render(self, request, payment) -> str:
return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order, payment))
format_map(self.settings.get('pending_description', as_type=LazyI18nString), self.format_map(payment.order, payment))
)
@@ -1385,7 +1388,7 @@ class GiftCardPayment(BasePaymentProvider):
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
def execute_payment(self, request: HttpRequest, payment: OrderPayment, is_early_special_case=False) -> str:
for p in payment.order.positions.all():
if p.item.issue_giftcard:
raise PaymentException(_("You cannot pay with gift cards when buying a gift card."))
@@ -1421,7 +1424,7 @@ class GiftCardPayment(BasePaymentProvider):
'gift_card': gc.pk,
'transaction_id': trans.pk,
}
payment.confirm()
payment.confirm(send_mail=not is_early_special_case, generate_invoice=not is_early_special_case)
except PaymentException as e:
payment.fail(info={'error': str(e)})
raise e
+4
View File
@@ -746,6 +746,8 @@ class Renderer:
def replace(x):
if x.group(1).startswith('itemmeta:'):
if op.variation_id:
return op.variation.meta_data.get(x.group(1)[9:]) or ''
return op.item.meta_data.get(x.group(1)[9:]) or ''
elif x.group(1).startswith('meta:'):
return ev.meta_data.get(x.group(1)[5:]) or ''
@@ -766,6 +768,8 @@ class Renderer:
return re.sub(r'\{([a-zA-Z0-9:_]+)\}', replace, text)
elif o['content'].startswith('itemmeta:'):
if op.variation_id:
return op.variation.meta_data.get(o['content'][9:]) or ''
return op.item.meta_data.get(o['content'][9:]) or ''
elif o['content'].startswith('meta:'):
+5 -4
View File
@@ -35,12 +35,13 @@ from pretix.base.models import (
SubEvent, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, TolerantDict, mail
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
)
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -51,7 +52,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
try:
mail(
wle.email,
str(subject).format_map(TolerantDict(email_context)),
format_map(subject, email_context),
message,
email_context,
wle.event,
@@ -71,7 +72,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event)
real_subject = str(subject).format_map(TolerantDict(email_context))
real_subject = format_map(subject, email_context)
try:
order.send_mail(
real_subject, message, email_context,
@@ -86,7 +87,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = str(subject).format_map(TolerantDict(email_context))
real_subject = format_map(subject, email_context)
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event,
refund_amount=refund_amount,
+5 -3
View File
@@ -76,6 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.helpers.format import format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
@@ -85,6 +86,7 @@ INVALID_ADDRESS = 'invalid-pretix-mail-address'
class TolerantDict(dict):
# kept for backwards compatibility with plugins
def __missing__(self, key):
return key
@@ -109,7 +111,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
:param template: The filename of a template to be used. It will be rendered with the locale given in the locale
argument and the context given in the next argument. Alternatively, you can pass a LazyI18nString and
``context`` will be used as the argument to a Python ``.format_map()`` call on the template.
``context`` will be used as the argument to a ``pretix.helpers.format.format_map(template, context)`` call on the template.
:param context: The context for rendering the template (see ``template`` parameter)
@@ -177,7 +179,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
})
renderer = ClassicMailRenderer(None, organizer)
content_plain = body_plain = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(str(subject), context)
sender = (
sender or
(event.settings.get('mail_from') if event else None) or
@@ -608,7 +610,7 @@ def render_mail(template, context):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = body.format_map(TolerantDict(context))
body = format_map(body, context)
else:
tpl = get_template(template)
body = tpl.render(context)
+60 -31
View File
@@ -74,7 +74,7 @@ from pretix.base.models.orders import (
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.payment import PaymentException
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets
@@ -1029,6 +1029,9 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
locked = True
lockfn = event.lock
warnings = []
any_payment_failed = False
with lockfn() as now_dt:
positions = list(
positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons')
@@ -1042,25 +1045,52 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer)
try:
for p in payment_objs:
if p.provider == 'free':
p.confirm(send_mail=False, lock=not locked, generate_invoice=False)
except Quota.QuotaExceededException:
pass
free_order_flow = (
payment_objs and
any(p['provider'] == 'free' for p in payment_requests) and
order.pending_sum == Decimal('0.00') and
not order.require_approval
)
if free_order_flow:
# We give special treatment to GiftCardPayment here because our invoice renderer expects gift cards to already be
# processed, and because we historically treat gift card orders like free orders with regards to email texts.
# It would be great to give external gift card plugins the same special treatment, but it feels to risky for now, as
# (a) there would be no email at all if the plugin fails in a weird way and (b) we'd be able to run into
# contradictions when a plugin set both execute_payment_needs_user=False as well as requires_invoice_immediately=True
for p in payment_objs:
if isinstance(p.payment_provider, GiftCardPayment):
try:
for p in payment_objs:
if p.provider == 'free':
p.confirm(send_mail=False, lock=not locked)
except Quota.QuotaExceededException:
pass
p.process_initiated = True
p.save(update_fields=['process_initiated'])
p.payment_provider.execute_payment(None, p, is_early_special_case=True)
except PaymentException as e:
warnings.append(str(e))
any_payment_failed = True
except Exception:
logger.exception('Error during payment attempt')
pending_sum = order.pending_sum
free_order_flow = (
payment_objs and
(
any(p['provider'] == 'free' for p in payment_requests) or
all(p['provider'] == 'giftcard' for p in payment_requests)
) and
pending_sum == Decimal('0.00') and
not order.require_approval
)
invoice = order.invoices.last() # Might be generated by plugin already
if not invoice and invoice_qualified(order):
if event.settings.get('invoice_generate') == 'True' or (
event.settings.get('invoice_generate') == 'paid' and any(p['pprov'].requires_invoice_immediately for p in payment_requests)):
invoice_required = (
event.settings.get('invoice_generate') == 'True' or (
event.settings.get('invoice_generate') == 'paid' and (
any(p['pprov'].requires_invoice_immediately for p in payment_requests) or
pending_sum <= Decimal('0.00')
)
)
)
if invoice_required:
invoice = generate_invoice(
order,
trigger_pdf=not event.settings.invoice_email_attachment or not order.email
@@ -1100,23 +1130,22 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
_order_placed_email_attendee(event, order, p, email_attendees_template, subject_attendees_template, log_entry,
is_free=free_order_flow)
warnings = []
any_failed = False
for p in payment_objs:
if not p.payment_provider.execute_payment_needs_user:
try:
p.process_initiated = True
p.save(update_fields=['process_initiated'])
resp = p.payment_provider.execute_payment(None, p)
if isinstance(resp, str):
logger.warning('Payment provider returned URL from execute_payment even though execute_payment_needs_user is not set')
except PaymentException as e:
warnings.append(str(e))
any_failed = True
except Exception:
logger.exception('Error during payment attempt')
if not any_payment_failed:
for p in payment_objs:
if not p.payment_provider.execute_payment_needs_user and not p.process_initiated:
try:
p.process_initiated = True
p.save(update_fields=['process_initiated'])
resp = p.payment_provider.execute_payment(None, p)
if isinstance(resp, str):
logger.warning('Payment provider returned URL from execute_payment even though execute_payment_needs_user is not set')
except PaymentException as e:
warnings.append(str(e))
any_payment_failed = True
except Exception:
logger.exception('Error during payment attempt')
if any_failed:
if any_payment_failed:
# Cancel all other payments because their amount might be wrong now.
for p in payment_objs:
if p.state == OrderPayment.PAYMENT_STATE_CREATED:
+63 -1
View File
@@ -44,6 +44,7 @@ from django.core.files.uploadedfile import UploadedFile
from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import (
@@ -436,12 +437,21 @@ class ItemCreateForm(I18nModelForm):
v.pk = None
v.item = instance
v.save()
for mv in variation.meta_values.all():
mv.pk = None
mv.variation = v
mv.save(force_insert=True)
else:
ItemVariation.objects.create(
item=instance, value=__('Standard')
)
if self.cleaned_data.get('copy_from'):
for mv in self.cleaned_data['copy_from'].meta_values.all():
mv.pk = None
mv.item = instance
mv.save(force_insert=True)
for question in self.cleaned_data['copy_from'].questions.all():
question.items.add(instance)
question.log_action('pretix.event.question.changed', user=self.user, data={
@@ -727,6 +737,31 @@ class ItemVariationForm(I18nModelForm):
del self.fields['require_membership']
del self.fields['require_membership_types']
self.meta_fields = []
meta_defaults = {}
if self.instance.pk:
for mv in self.instance.meta_values.all():
meta_defaults[mv.property_id] = mv.value
for p in self.meta_properties:
self.initial[f'meta_{p.name}'] = meta_defaults.get(p.pk)
self.fields[f'meta_{p.name}'] = forms.CharField(
label=p.name,
widget=forms.TextInput(
attrs={
'placeholder': _('Use value from product'),
'data-typeahead-url': reverse('control:event.items.meta.typeahead', kwargs={
'organizer': self.event.organizer.slug,
'event': self.event.slug
}) + '?' + urlencode({
'property': p.name,
}),
},
),
required=False,
)
self.meta_fields.append(f'meta_{p.name}')
class Meta:
model = ItemVariation
localized_fields = '__all__'
@@ -757,6 +792,26 @@ class ItemVariationForm(I18nModelForm):
}),
}
def save(self, commit=True):
instance = super().save(commit)
self.meta_fields = []
current_values = {v.property_id: v for v in instance.meta_values.all()}
for p in self.meta_properties:
if self.cleaned_data[f'meta_{p.name}']:
if p.pk in current_values:
current_values[p.pk].value = self.cleaned_data[f'meta_{p.name}']
current_values[p.pk].save()
else:
instance.meta_values.create(property=p, value=self.cleaned_data[f'meta_{p.name}'])
elif p.pk in current_values:
current_values[p.pk].delete()
@property
def meta_properties(self):
if not hasattr(self.event, '_cached_item_meta_properties'):
self.event._cached_item_meta_properties = self.event.item_meta_properties.all()
return self.event._cached_item_meta_properties
class ItemAddOnsFormSet(I18nFormSet):
title = _('Add-ons')
@@ -845,6 +900,7 @@ class ItemBundleFormSet(I18nFormSet):
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
kwargs['item'] = self.item
kwargs['item_qs'] = self.item_qs
return super()._construct_form(i, **kwargs)
@property
@@ -856,12 +912,17 @@ class ItemBundleFormSet(I18nFormSet):
empty_permitted=True,
use_required_attribute=False,
locales=self.locales,
item_qs=self.item_qs,
item=self.item,
event=self.event
)
self.add_fields(form, None)
return form
@cached_property
def item_qs(self):
return self.event.items.prefetch_related('variations').all()
def clean(self):
super().clean()
ivs = set()
@@ -889,6 +950,7 @@ class ItemBundleForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item')
self.item_qs = kwargs.pop('item_qs')
super().__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
initial = kwargs.get('initial', {})
@@ -906,7 +968,7 @@ class ItemBundleForm(I18nModelForm):
super().__init__(*args, **kwargs)
choices = []
for i in self.event.items.prefetch_related('variations').all():
for i in self.item_qs:
pname = str(i)
if not i.is_available():
pname += ' ({})'.format(_('inactive'))
@@ -1,6 +1,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% load getitem %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" id="item_variations">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
@@ -29,16 +30,20 @@
{% endif %}
</div>
<div class="col-md-2 col-xs-6">
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
</div>
<div class="col-md-2 col-xs-6">
{% for k, c in sales_channels.items %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% endfor %}
</div>
<div class="col-md-1 col-xs-6 text-right flip variation-price">
@@ -69,6 +74,27 @@
{% bootstrap_field form.default_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.description layout="control" %}
{% if form.meta_fields %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for fname in form.meta_fields %}
{% with form|getitem:fname as field %}
<div class="row">
<div class="col-md-4">
<label for="{{ field.id_for_label }}">
{{ field.label }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_field field layout="inline" %}
</div>
</div>
{% endwith %}
{% endfor %}
</div>
</div>
{% endif %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.sales_channels layout="control" %}
@@ -110,16 +136,20 @@
{% endif %}
</div>
<div class="col-md-2 col-xs-6">
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
</div>
<div class="col-md-2 col-xs-6">
{% for k, c in sales_channels.items %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% endfor %}
</div>
<div class="col-md-1 col-xs-6 text-right flip variation-price">
@@ -141,6 +171,27 @@
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field formset.empty_form.original_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field formset.empty_form.description layout="control" %}
{% if formset.empty_form.meta_fields %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for fname in formset.empty_form.meta_fields %}
{% with formset.empty_form|getitem:fname as field %}
<div class="row">
<div class="col-md-4">
<label for="{{ field.id_for_label }}">
{{ field.label }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_field field layout="inline" %}
</div>
</div>
{% endwith %}
{% endfor %}
</div>
</div>
{% endif %}
{% bootstrap_field formset.empty_form.available_from layout="control" %}
{% bootstrap_field formset.empty_form.available_until layout="control" %}
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
+4 -4
View File
@@ -93,8 +93,8 @@ from ...base.i18n import language
from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota,
)
from ...base.services.mail import TolerantDict
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
from ...helpers.format import format_map
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -734,10 +734,10 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.event.settings.region):
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = bleach.clean(v).format_map(self.placeholders(preview_item))
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
format_map(v, self.placeholders(preview_item))
)
return JsonResponse({
@@ -761,7 +761,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
def get(self, request, *args, **kwargs):
v = str(request.event.settings.mail_text_order_placed)
v = v.format_map(TolerantDict(self.placeholders('mail_text_order_placed')))
v = format_map(v, self.placeholders('mail_text_order_placed'))
renderers = request.event.get_html_mail_renderers()
if request.GET.get('renderer') in renderers:
with rolledback_transaction():
+1 -1
View File
@@ -1426,7 +1426,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
can_order=True, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
queryset=ItemVariation.objects.filter(item=self.get_object()),
queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related('meta_values', 'require_membership_types'),
event=self.request.event, prefix="variations"
)),
('addons', inlineformset_factory(
+9 -7
View File
@@ -65,6 +65,10 @@ class OAuthApplicationRegistrationView(ApplicationRegistration):
def form_valid(self, form):
form.instance.client_type = 'confidential'
form.instance.authorization_grant_type = 'authorization-code'
secret = generate_client_secret()
messages.success(self.request, _('Your application has been created and an application secret has been generated. '
'Please copy and save it right now as it will not be shown again: {secret}').format(secret=secret))
form.instance.client_secret = secret
oauth_application_registered.send(
sender=self.request, user=self.request.user, application=form.instance
)
@@ -74,18 +78,14 @@ class OAuthApplicationRegistrationView(ApplicationRegistration):
class ApplicationUpdateForm(forms.ModelForm):
class Meta:
model = OAuthApplication
fields = ("name", "client_id", "client_secret", "redirect_uris")
fields = ("name", "client_id", "redirect_uris")
def clean_client_id(self):
return self.instance.client_id
def clean_client_secret(self):
return self.instance.client_secret
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['client_id'].widget.attrs['readonly'] = True
self.fields['client_secret'].widget.attrs['readonly'] = True
class OAuthApplicationUpdateView(ApplicationUpdate):
@@ -103,8 +103,10 @@ class OAuthApplicationRollView(ApplicationDetail):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
messages.success(request, _('A new client secret has been generated and is now effective.'))
self.object.client_secret = generate_client_secret()
secret = generate_client_secret()
messages.success(request, _('A new client secret has been generated. Please copy and save it right now as '
'it will not be shown again: {secret}').format(secret=secret))
self.object.client_secret = secret
self.object.save()
return HttpResponseRedirect(self.object.get_absolute_url())
+4 -5
View File
@@ -93,9 +93,7 @@ from pretix.base.services.invoices import (
invoice_qualified, regenerate_invoice,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import (
SendMailException, TolerantDict, render_mail,
)
from pretix.base.services.mail import SendMailException, render_mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
@@ -127,6 +125,7 @@ from pretix.control.forms.orders import (
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin
from pretix.helpers.format import format_map
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -2032,7 +2031,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
self.preview_output = {
@@ -2097,7 +2096,7 @@ class OrderPositionSendMail(OrderSendMail):
with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
self.preview_output = {
+4 -3
View File
@@ -109,6 +109,7 @@ from pretix.control.views import PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import GroupConcat
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
@@ -335,10 +336,10 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.organizer.settings.region):
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = bleach.clean(v).format_map(self.placeholders(preview_item))
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
format_map(v, self.placeholders(preview_item))
)
return JsonResponse({
@@ -1403,7 +1404,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
messages.error(request, _('The transaction could not be reversed.'))
else:
messages.success(request, _('The transaction has been reversed.'))
elif 'value' in request.POST:
elif request.POST.get('value'):
try:
value = DecimalField(localize=True).to_python(request.POST.get('value'))
except ValidationError:
+2 -2
View File
@@ -156,8 +156,8 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
p = PdfWriter()
try:
p.add_blank_page(
width=float(request.POST.get('width')) * mm,
height=float(request.POST.get('height')) * mm,
width=Decimal('%.5f' % (float(request.POST.get('width')) * mm)),
height=Decimal('%.5f' % (float(request.POST.get('height')) * mm)),
)
except ValueError:
return JsonResponse({
+16 -2
View File
@@ -48,7 +48,7 @@ from django.utils.translation import gettext as _, pgettext
from pretix.base.models import (
EventMetaProperty, EventMetaValue, ItemMetaProperty, ItemMetaValue,
ItemVariation, Order, Organizer, User, Voucher,
ItemVariation, ItemVariationMetaValue, Order, Organizer, User, Voucher,
)
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import (
@@ -747,6 +747,10 @@ def item_meta_values(request, organizer, event):
value__icontains=q,
property__name=propname
)
var_matches = ItemVariationMetaValue.objects.filter(
value__icontains=q,
property__name=propname
)
defaults = ItemMetaProperty.objects.filter(
name=propname,
default__icontains=q
@@ -758,6 +762,7 @@ def item_meta_values(request, organizer, event):
defaults = defaults.filter(event__organizer_id=organizer.pk)
matches = matches.filter(item__event__organizer_id=organizer.pk)
var_matches = var_matches.filter(variation__item__event__organizer_id=organizer.pk)
all_access = (
request.user.has_active_staff_session(request.session.session_key)
or request.user.teams.filter(all_events=True, organizer=organizer, can_change_items=True).exists()
@@ -773,10 +778,19 @@ def item_meta_values(request, organizer, event):
'limit_events__id', flat=True
)
)
var_matches = matches.filter(
variation__item__event__id__in=request.user.teams.filter(can_change_items=True).values_list(
'limit_events__id', flat=True
)
)
return JsonResponse({
'results': [
{'name': v, 'id': v}
for v in sorted(set(defaults.values_list('default', flat=True)[:10]) | set(matches.values_list('value', flat=True)[:10]))
for v in sorted(
set(defaults.values_list('default', flat=True)[:10]) |
set(matches.values_list('value', flat=True)[:10]) |
set(var_matches.values_list('value', flat=True)[:10])
)
]
})
+55
View File
@@ -0,0 +1,55 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from string import Formatter
logger = logging.getLogger(__name__)
class SafeFormatter(Formatter):
"""
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
"""
def __init__(self, context):
self.context = context
def get_field(self, field_name, args, kwargs):
if '.' in field_name or '[' in field_name:
logger.warning(f'Ignored invalid field name "{field_name}"')
return ('{' + str(field_name) + '}', field_name)
return super().get_field(field_name, args, kwargs)
def get_value(self, key, args, kwargs):
if key not in self.context:
return '{' + str(key) + '}'
return self.context[key]
def format_field(self, value, format_spec):
# Ignore format _spec
return super().format_field(value, '')
def format_map(template, context):
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context).format(template)
+7 -2
View File
@@ -24,8 +24,7 @@ from inspect import isgenerator
from openpyxl import Workbook
from openpyxl.cell.cell import (
ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, TIME_TYPES, TYPE_FORMULA, TYPE_STRING,
Cell,
KNOWN_TYPES, TIME_TYPES, TYPE_FORMULA, TYPE_STRING, Cell,
)
from openpyxl.compat import NUMERIC_TYPES
from openpyxl.utils import column_index_from_string
@@ -49,6 +48,12 @@ There are mainly two problems this solves:
- It removes characters considered invalid by Excel to avoid exporter crashes.
"""
ILLEGAL_CHARACTERS_RE = re.compile(
# From the XML specification
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
r'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]'
)
def remove_invalid_excel_chars(val):
if isinstance(val, Cell):
+57 -86
View File
@@ -4,8 +4,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-29 20:30+0000\n"
"PO-Revision-Date: 2022-09-23 22:00+0000\n"
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
">\n"
"Language: da\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.14.1\n"
"X-Generator: Weblate 4.14.2\n"
#: htmlcov/pretix_control_views_dashboards_py.html:963
#: pretix/control/templates/pretixcontrol/events/index.html:140
@@ -66,9 +66,8 @@ msgid "pretixSCAN (online only, no order sync)"
msgstr ""
#: pretix/api/auth/devicesecurity.py:146
#, fuzzy
msgid "pretixPOS"
msgstr "Standard"
msgstr "pretixPOS"
#: pretix/api/models.py:39 pretix/base/models/customers.py:388
msgid "Application name"
@@ -290,7 +289,7 @@ msgstr "Ny bestilling afgivet"
#: pretix/api/webhooks.py:209 pretix/base/notifications.py:239
msgid "New order requires approval"
msgstr "Ny ordre kræver godkendelse"
msgstr "Ny bestilling kræver godkendelse"
#: pretix/api/webhooks.py:213 pretix/base/notifications.py:245
msgid "Order marked as paid"
@@ -304,23 +303,20 @@ msgid "Order canceled"
msgstr "Bestilling annulleret"
#: pretix/api/webhooks.py:221 pretix/base/notifications.py:257
#, fuzzy
msgid "Order reactivated"
msgstr "Bestiiling udløbet"
msgstr "Bestilling genaktiveret"
#: pretix/api/webhooks.py:225 pretix/base/notifications.py:263
msgid "Order expired"
msgstr "Bestiiling udløbet"
msgstr "Bestilling udløbet"
#: pretix/api/webhooks.py:229
#, fuzzy
#| msgid "The order's expiry date has been changed."
msgid "Order expiry date changed"
msgstr "Bestillingens udløbsdato er blevet ændret."
msgstr "Bestillingens udløbsdato ændret"
#: pretix/api/webhooks.py:233 pretix/base/notifications.py:269
msgid "Order information changed"
msgstr ""
msgstr "Bestillingsoplysninger ændret"
#: pretix/api/webhooks.py:237 pretix/base/notifications.py:275
msgid "Order contact address changed"
@@ -332,46 +328,36 @@ msgid "Order changed"
msgstr "Bestilling ændret"
#: pretix/api/webhooks.py:245
#, fuzzy
msgid "Refund of payment created"
msgstr "Aktiver betalingsmetode"
msgstr "Tilbagebetaling oprettet"
#: pretix/api/webhooks.py:249 pretix/base/notifications.py:293
#, fuzzy
msgid "External refund of payment"
msgstr "Udvid betalingsfrist"
msgstr "Ekstern tilbagebetaling"
#: pretix/api/webhooks.py:253
#, fuzzy
#| msgid "Text (requested by user)"
msgid "Refund of payment requested by customer"
msgstr "Tekst (forespurgt af bruger)"
msgstr "Tilbagebetaling anmodet af kunden"
#: pretix/api/webhooks.py:257
#, fuzzy
#| msgid "Payment completed."
msgid "Refund of payment completed"
msgstr "Betaling gennemført."
msgstr "Tilbagebetaling gennemført"
#: pretix/api/webhooks.py:261
#, fuzzy
msgid "Refund of payment canceled"
msgstr "Bestillingen er blevet annulleret."
msgstr "Tilbagebetaling annulleret"
#: pretix/api/webhooks.py:265
#, fuzzy
msgid "Refund of payment failed"
msgstr "Tilbagebetal bestilling"
msgstr "Tilbagebetaling mislykkedes"
#: pretix/api/webhooks.py:269
#, fuzzy
msgid "Order approved"
msgstr "Bestiiling udløbet"
msgstr "Bestilling godkendt"
#: pretix/api/webhooks.py:273
#, fuzzy
msgid "Order denied"
msgstr "Bestiiling udløbet"
msgstr "Bestilling afvist"
#: pretix/api/webhooks.py:277
#, fuzzy
@@ -383,25 +369,21 @@ msgid "Ticket check-in reverted"
msgstr "Billet-check-in annulleret"
#: pretix/api/webhooks.py:285
#, fuzzy
msgid "Event created"
msgstr "Arrangementsdato"
msgstr "Arrangement oprettet"
#: pretix/api/webhooks.py:289
#, fuzzy
msgid "Event details changed"
msgstr "Arrangementsdatoer"
msgstr "Arrangementsoplysninger ændret"
#: pretix/api/webhooks.py:293
#, fuzzy
msgid "Event deleted"
msgstr "Arrangementsdato"
msgstr "Arrangement slettet"
#: pretix/api/webhooks.py:297
#, fuzzy
msgctxt "subevent"
msgid "Event series date added"
msgstr "Arrangements startdato og -tidspunkt"
msgstr "Dato for arrangementsserien tilføjet"
#: pretix/api/webhooks.py:301
#, fuzzy
@@ -416,10 +398,8 @@ msgid "Event series date deleted"
msgstr "Arrangements startdato og -tidspunkt"
#: pretix/api/webhooks.py:309
#, fuzzy
#| msgid "Shop not live"
msgid "Shop taken live"
msgstr "Butik ikke i luften"
msgstr "Butik gået i luften"
#: pretix/api/webhooks.py:313
#, fuzzy
@@ -428,19 +408,17 @@ msgid "Shop taken offline"
msgstr "Butik ikke i luften"
#: pretix/api/webhooks.py:317
#, fuzzy
msgid "Test-Mode of shop has been activated"
msgstr "Bestilling oprettet."
msgstr "Butikkens testtilstand er blevet aktiveret"
#: pretix/api/webhooks.py:321
#, fuzzy
msgid "Test-Mode of shop has been deactivated"
msgstr "Bestilling oprettet."
msgstr "Butikkens testtilstand er blevet deaktiveret"
#: pretix/base/auth.py:143
#, python-brace-format
msgid "{system} User"
msgstr ""
msgstr "{system} Bruger"
#: pretix/base/auth.py:152 pretix/base/exporters/customers.py:65
#: pretix/base/forms/auth.py:246 pretix/base/models/auth.py:246
@@ -457,7 +435,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html:306
#: pretix/presale/templates/pretixpresale/organizers/customer_profile.html:26
msgid "E-mail"
msgstr "E-mailadresse"
msgstr "E-mail"
#: pretix/base/auth.py:154 pretix/base/forms/auth.py:153
#: pretix/base/forms/auth.py:207 pretix/base/models/customers.py:95
@@ -468,7 +446,7 @@ msgstr "Adgangskode"
#: pretix/base/channels.py:119
msgid "Online shop"
msgstr ""
msgstr "Netbutik"
#: pretix/base/context.py:45
#, python-brace-format
@@ -550,7 +528,7 @@ msgstr "Standard"
#: pretix/base/email.py:207
msgid "Simple with logo"
msgstr ""
msgstr "Enkelt med logo"
#: pretix/base/email.py:536 pretix/base/email.py:618 pretix/base/email.py:634
#: pretix/base/email.py:639 pretix/base/pdf.py:164 pretix/base/pdf.py:322
@@ -729,9 +707,8 @@ msgstr "E-mailadresse"
#: pretix/base/exporters/customers.py:77 pretix/base/models/customers.py:100
#: pretix/control/templates/pretixcontrol/organizers/customer.html:67
#, fuzzy
msgid "Last login"
msgstr "Login"
msgstr "Seneste login"
#: pretix/base/exporters/customers.py:78 pretix/base/models/customers.py:101
#: pretix/control/templates/pretixcontrol/organizers/customer.html:65
@@ -1720,9 +1697,8 @@ msgstr "Fakturanumre"
#: pretix/base/exporters/orderlist.py:629 pretix/base/orderimport.py:651
#: pretix/control/forms/filter.py:538
#: pretix/control/templates/pretixcontrol/order/index.html:181
#, fuzzy
msgid "Sales channel"
msgstr "Gem ændringer"
msgstr "Salgskanal"
#: pretix/base/exporters/orderlist.py:295
#: pretix/base/exporters/orderlist.py:821 pretix/base/models/orders.py:219
@@ -2005,7 +1981,7 @@ msgstr "Tjek-ind-status"
#: pretix/plugins/checkinlists/exporters.py:332
#: pretix/presale/templates/pretixpresale/event/order.html:66
msgid "Order"
msgstr "Rækkefølge"
msgstr "Bestilling"
#: pretix/base/exporters/orderlist.py:820
#: pretix/base/exporters/orderlist.py:997
@@ -2936,13 +2912,12 @@ msgid ""
msgstr ""
#: pretix/base/models/checkin.py:255
#, fuzzy
msgid "Entry"
msgstr "Land"
msgstr "Indgang"
#: pretix/base/models/checkin.py:256
msgid "Exit"
msgstr ""
msgstr "Udgang"
#: pretix/base/models/checkin.py:271
msgid "Unknown ticket"
@@ -2974,7 +2949,7 @@ msgstr "Standardtidszone"
#: pretix/base/models/checkin.py:277
msgid "Ticket type not allowed here"
msgstr "Billettype ikke tilladt her"
msgstr "Billettype er ikke tilladt her"
#: pretix/base/models/checkin.py:278
msgid "Ticket code is ambiguous on list"
@@ -3816,7 +3791,7 @@ msgstr ""
#: pretix/base/models/items.py:863 pretix/base/models/vouchers.py:262
#: pretix/base/models/waitinglist.py:96 pretix/base/orderimport.py:284
msgid "Product variation"
msgstr "Produktvariant"
msgstr "Produktvariation"
#: pretix/base/models/items.py:864
#: pretix/control/templates/pretixcontrol/item/create.html:64
@@ -5524,26 +5499,26 @@ msgstr "Gratis"
#: pretix/base/payment.py:993
msgid "Box office"
msgstr ""
msgstr "Box office"
#: pretix/base/payment.py:1041 pretix/base/payment.py:1060
#: pretix/plugins/manualpayment/apps.py:30
#: pretix/plugins/manualpayment/apps.py:33
#, fuzzy
msgid "Manual payment"
msgstr "Ugyldige betalinger"
msgstr "Manuel betaling"
#: pretix/base/payment.py:1046 pretix/plugins/banktransfer/payment.py:188
msgid ""
"In test mode, you can just manually mark this order as paid in the backend "
"after it has been created."
msgstr ""
"I testtilstand kan du blot manuelt markere denne ordre som betalt i backend, "
"efter den er oprettet."
#: pretix/base/payment.py:1067 pretix/base/payment.py:1200
#: pretix/plugins/banktransfer/payment.py:147
#, fuzzy
msgid "Payment method name"
msgstr "Betalingsmetode"
msgstr "Navn på betalingsmetode"
#: pretix/base/payment.py:1071
msgid "Payment process description during checkout"
@@ -5581,14 +5556,12 @@ msgid ""
msgstr ""
#: pretix/base/payment.py:1135
#, fuzzy
msgid "Offsetting"
msgstr "Indstillinger"
msgstr "Forskydning"
#: pretix/base/payment.py:1149 pretix/control/views/orders.py:967
#, fuzzy
msgid "You entered an order that could not be found."
msgstr "Dine ændringer kunne ikke gemmes."
msgstr "Du har indtastet en ordre, der ikke kunne findes."
#: pretix/base/payment.py:1178
#, python-format
@@ -12576,7 +12549,7 @@ msgstr "Den valgte dato er blevet slettet."
#: pretix/control/logdisplay.py:355
msgid "The order has been created."
msgstr "Bestilling oprettet."
msgstr "Bestillingen er oprettet"
#: pretix/control/logdisplay.py:356
msgid "The order requires approval before it can continue to be processed."
@@ -13730,6 +13703,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_change.html:81
#: pretix/presale/templates/pretixpresale/event/order_pay.html:27
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html:60
#, fuzzy
msgid "Continue"
msgstr "Fortsæt"
@@ -22683,22 +22657,21 @@ msgstr ""
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base_organizer.html:4
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base_organizer.html:7
msgid "Import bank data"
msgstr "Importer bankdata"
msgstr "Importér bankdata"
#: pretix/plugins/banktransfer/signals.py:61
#: pretix/plugins/banktransfer/signals.py:97
#, fuzzy
msgid "Export refunds"
msgstr "Udfør ændringer"
msgstr "Eksportér refunderinger"
#: pretix/plugins/banktransfer/tasks.py:69
#, fuzzy, python-format
#, python-format
msgid "Your order received an incomplete payment: %(code)s"
msgstr "Din bestilling afventer betaling: %(code)s"
msgstr "Din ordre modtog en ufuldstændig betaling: %(code)s"
#: pretix/plugins/banktransfer/tasks.py:170
msgid "The order has already been canceled."
msgstr "Denne bestilling er allerede blevet annulleret."
msgstr "Ordren er allerede blevet annulleret."
#: pretix/plugins/banktransfer/tasks.py:180
msgid "Automatic split to multiple orders not possible."
@@ -22722,14 +22695,12 @@ msgid ""
msgstr ""
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:8
#, fuzzy
msgid "Account"
msgstr "Beløb"
msgstr "Konto"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:22
#, fuzzy
msgid "Transfer amount"
msgstr "Navn"
msgstr "Overførselsbeløb"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:32
msgid "Reference code"
@@ -23256,7 +23227,7 @@ msgstr ""
#: pretix/plugins/paypal2/payment.py:134 pretix/plugins/paypal2/payment.py:984
#: pretix/plugins/paypal2/payment.py:985
msgid "PayPal"
msgstr ""
msgstr "PayPal"
#: pretix/plugins/paypal/apps.py:53
msgid ""
@@ -24556,7 +24527,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py:243 pretix/plugins/stripe/payment.py:990
msgid "giropay"
msgstr ""
msgstr "giropay"
#: pretix/plugins/stripe/payment.py:245 pretix/plugins/stripe/payment.py:252
#: pretix/plugins/stripe/payment.py:259 pretix/plugins/stripe/payment.py:266
@@ -24568,7 +24539,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py:250 pretix/plugins/stripe/payment.py:1062
msgid "iDEAL"
msgstr ""
msgstr "iDEAL"
#: pretix/plugins/stripe/payment.py:257 pretix/plugins/stripe/payment.py:1114
msgid "Alipay"
@@ -24576,11 +24547,11 @@ msgstr ""
#: pretix/plugins/stripe/payment.py:264 pretix/plugins/stripe/payment.py:1157
msgid "Bancontact"
msgstr ""
msgstr "Bancontact"
#: pretix/plugins/stripe/payment.py:271 pretix/plugins/stripe/payment.py:1229
msgid "SOFORT"
msgstr ""
msgstr "SOFORT"
#: pretix/plugins/stripe/payment.py:276
msgid ""
@@ -24600,7 +24571,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py:299 pretix/plugins/stripe/payment.py:1428
msgid "Przelewy24"
msgstr ""
msgstr "Przelewy24"
#: pretix/plugins/stripe/payment.py:306 pretix/plugins/stripe/payment.py:1484
msgid "WeChat Pay"
+47 -57
View File
@@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-29 20:31+0000\n"
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
"da/>\n"
"Language: 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 4.11.2\n"
"X-Generator: Weblate 4.14.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -30,51 +30,51 @@ msgstr "Kommentar:"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
msgid "PayPal"
msgstr ""
msgstr "PayPal"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
msgid "Venmo"
msgstr ""
msgstr "Venmo"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
msgid "Apple Pay"
msgstr ""
msgstr "Apple Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
msgid "Itaú"
msgstr ""
msgstr "Itaú"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
msgid "PayPal Credit"
msgstr ""
msgstr "PayPal Credit"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
msgid "Credit Card"
msgstr ""
msgstr "Kreditkort"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
msgid "PayPal Pay Later"
msgstr ""
msgstr "PayPal Pay Later"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL"
msgstr ""
msgstr "iDEAL"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"
msgstr ""
msgstr "SEPA Direct Debit"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
msgid "Bancontact"
msgstr ""
msgstr "Bancontact"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
msgid "giropay"
msgstr ""
msgstr "giropay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
msgid "SOFORT"
msgstr ""
msgstr "SOFORT"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
#, fuzzy
@@ -84,11 +84,11 @@ msgstr "Ja"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
msgid "MyBank"
msgstr ""
msgstr "MyBank"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
msgid "Przelewy24"
msgstr ""
msgstr "Przelewy24"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
msgid "Verkkopankki"
@@ -146,7 +146,7 @@ msgstr "Bekræfter din betaling …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:246
msgid "Payment method unavailable"
msgstr ""
msgstr "Betalingsmetode er ikke tilgængelig"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
@@ -176,67 +176,64 @@ msgstr "Kontakter din bank …"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
msgid "Select a check-in list"
msgstr ""
msgstr "Vælg en check-in liste"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found."
msgstr ""
msgstr "Der blev ikke fundet nogen aktive check-in lister."
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list"
msgstr ""
msgstr "Skift check-in liste"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results"
msgstr ""
msgstr "Søgeresultater"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
#, fuzzy
#| msgctxt "widget"
#| msgid "Close ticket shop"
msgid "No tickets found"
msgstr "Luk billetbutik"
msgstr "Ingen billetter fundet"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
msgid "Result"
msgstr ""
msgstr "Resultat"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
msgid "This ticket requires special attention"
msgstr ""
msgstr "Denne billet kræver særlig opmærksomhed"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction"
msgstr ""
msgstr "Skift retning"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
msgid "Entry"
msgstr ""
msgstr "Indgang"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
msgid "Exit"
msgstr ""
msgstr "Udgang"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
msgid "Scan a ticket or search and press return…"
msgstr ""
msgstr "Scan en billet eller søg og tryk på retur…"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
msgid "Load more"
msgstr ""
msgstr "Hent flere"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
msgid "Valid"
msgstr ""
msgstr "Gyldig"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
msgid "Unpaid"
msgstr ""
msgstr "Ubetalt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
msgid "Canceled"
msgstr ""
msgstr "Annulleret"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
#, fuzzy
@@ -264,7 +261,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"
msgstr ""
msgstr "Gyldig billet"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded"
@@ -279,18 +276,16 @@ msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
#, fuzzy
#| msgid "Unknown error."
msgid "Unknown ticket"
msgstr "Ukendt fejl."
msgstr "Ukendt billet"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here"
msgstr ""
msgstr "Billettype er ikke tilladt her"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed"
msgstr ""
msgstr "Adgang ikke tilladt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
@@ -298,7 +293,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Order canceled"
msgstr ""
msgstr "Bestilling annulleret"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code is ambiguous on list"
@@ -312,11 +307,11 @@ msgstr "Check-in QR"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Valid Tickets"
msgstr ""
msgstr "Gyldige billetter"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Currently inside"
msgstr ""
msgstr "Inde i øjeblikket"
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
@@ -376,17 +371,15 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:145
#: pretix/static/pretixcontrol/js/ui/mail.js:21
#, fuzzy
#| msgid "The request took to long. Please try again."
msgid "The request took too long. Please try again."
msgstr "Forespørgselen tog for lang tid. Prøv venligst igen."
msgstr "Forespørgslen tog for lang tid. Prøv igen."
#: pretix/static/pretixbase/js/asynctask.js:186
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Vi kan ikke komme i kontakt med serveren. Prøv venligst igen. Fejlkode: "
"Vi kan i øjeblikket ikke komme i kontakt med serveren. Prøv igen. Fejlkode: "
"{code}"
#: pretix/static/pretixbase/js/asynctask.js:208
@@ -433,23 +426,20 @@ msgid "Product"
msgstr "Produkt"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgid "Product variation"
msgstr "Vis varianter"
msgstr "Produktvariation"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
msgid "Current date and time"
msgstr ""
msgstr "Aktuel dato og klokkeslæt"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
msgstr "Aktuel ugedag (1 = mandag, 7 = søndag)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
msgid "Number of previous entries"
msgstr ""
msgstr "Antal tidligere poster"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
msgid "Number of previous entries since midnight"
@@ -901,7 +891,7 @@ msgstr "Åbn sædevalg"
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Load more"
msgstr ""
msgstr "Hent flere"
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,910 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-29 20:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr ""
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
msgid "PayPal"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
msgid "Venmo"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
msgid "Apple Pay"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
msgid "Itaú"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
msgid "PayPal Credit"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
msgid "Credit Card"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
msgid "PayPal Pay Later"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
msgid "Bancontact"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
msgid "giropay"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
msgid "SOFORT"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
msgid "eps"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
msgid "MyBank"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
msgid "Przelewy24"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
msgid "Verkkopankki"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:50
msgid "PayU"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51
msgid "BLIK"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52
msgid "Trustly"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:53
msgid "Zimpler"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:54
msgid "Maxima"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:55
msgid "OXXO"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:56
msgid "Boleto"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57
msgid "WeChat Pay"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:163
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:221
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:246
msgid "Payment method unavailable"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Placed orders"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Paid orders"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
msgid "Total revenue"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
msgid "Contacting Stripe …"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
msgid "Total"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
msgid "Contacting your bank …"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
msgid "Select a check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found."
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
msgid "No tickets found"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
msgid "Result"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
msgid "This ticket requires special attention"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
msgid "Entry"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
msgid "Exit"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
msgid "Scan a ticket or search and press return…"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
msgid "Load more"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
msgid "Valid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
msgid "Unpaid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
msgid "Canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Redeemed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Ticket already used"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Order canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code is ambiguous on list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Checked-in Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:120
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:125
msgid "Your request has been queued on the server and will soon be processed."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:131
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:178
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:93
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:145
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:186
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:208
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:216
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:276
#: pretix/static/pretixcontrol/js/ui/main.js:71
msgid "Close message"
msgstr ""
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr ""
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
msgid "is one of"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
msgid "is before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
msgid "is after"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
msgid "Product"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
msgid "Product variation"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
msgid "Current date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
msgid "Number of previous entries"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
msgid "Number of previous entries since midnight"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:83
msgid "Number of days with a previous entry"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:87
msgid "Minutes since last entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:91
msgid "Minutes since first entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:112
msgid "All of the conditions below (AND)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:113
msgid "At least one of the conditions below (OR)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:114
msgid "Event start"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:115
msgid "Event end"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:116
msgid "Event admission"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:117
msgid "custom date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
msgid "custom time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
msgid "minutes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:71
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:384
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:653
msgid "Group of objects"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:659
msgid "Text object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:661
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:663
msgid "Image area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:665
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:667
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:671
msgid "Ticket design"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:961
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1011
#: pretix/static/pretixcontrol/js/ui/editor.js:1050
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1035
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:19
msgid "An error has occurred."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:54
msgid "Generating messages …"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:107
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:454
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:455
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:456
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:459
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:858
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:894
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:966
msgid "You have unsaved changes!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
msgid "(one more date)"
msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:50
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] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Register"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Open ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid ""
"We could not create your cart, since there are currently too many users in "
"this ticket shop. Please click \"Continue\" to retry in a new tab."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgctxt "widget"
msgid "Next week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Previous week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
msgid "Open seat selection"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Load more"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:77
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "December"
msgstr ""
+1 -1
View File
@@ -797,7 +797,7 @@ class SepaXMLExportForm(forms.Form):
bic = BICFormField(label="BIC")
def set_initial_from_event(self, event: Event):
banktransfer = event.get_payment_providers(cached=True)[BankTransfer.identifier]
banktransfer = BankTransfer(event)
self.initial["account_holder"] = banktransfer.settings.get("bank_details_sepa_name")
self.initial["iban"] = banktransfer.settings.get("bank_details_sepa_iban")
self.initial["bic"] = banktransfer.settings.get("bank_details_sepa_bic")
+1 -1
View File
@@ -611,7 +611,7 @@ class PaypalMethod(BasePaymentProvider):
pass
if str(pp_captured_order.purchase_units[0].amount.value) != str(payment.amount) or \
pp_captured_order.purchase_units[0].amount.currency_code != self.event.currency:
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_captured_order)))
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_captured_order.dict())))
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
'proceed.'))
+17 -9
View File
@@ -149,19 +149,27 @@ class XHRView(View):
else:
cart_total = get_cart_total(request)
cart_payments = cart_session(request).get('payments', [])
for fee in get_fees(request.event, request, cart_total, None, cart_payments, get_cart(request)):
multi_use_cart_payments = [p for p in cart_payments if p.get('multi_use_supported')]
simulated_payments = multi_use_cart_payments + [{
'provider': 'paypal',
'multi_use_supported': False,
'min_value': None,
'max_value': None,
'info_data': {},
}]
for fee in get_fees(request.event, request, cart_total, None, simulated_payments, get_cart(request)):
cart_total += fee.value
total_remaining = cart_total
for p in cart_session(request).get('payments', []):
if p['provider'] != 'paypal':
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
continue
for p in multi_use_cart_payments:
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
continue
to_pay = total_remaining
if p.get('max_value') and to_pay > Decimal(p['max_value']):
to_pay = min(to_pay, Decimal(p['max_value']))
total_remaining -= to_pay
to_pay = total_remaining
if p.get('max_value') and to_pay > Decimal(p['max_value']):
to_pay = min(to_pay, Decimal(p['max_value']))
total_remaining -= to_pay
cart_total = total_remaining
+5 -4
View File
@@ -40,6 +40,7 @@ from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.format import format_map
@app.task(base=ProfiledEventTask, acks_late=True)
@@ -116,8 +117,8 @@ def send_mails(event: Event, user: int, subject: dict, message: dict, orders: li
user=user,
data={
'position': p.positionid,
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': p.attendee_email
}
)
@@ -143,8 +144,8 @@ def send_mails(event: Event, user: int, subject: dict, message: dict, orders: li
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': o.email
}
)
+10 -10
View File
@@ -52,12 +52,12 @@ from pretix.base.email import get_available_placeholders
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.services.mail import TolerantDict
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import CreateView, PaginationMixin, UpdateView
from pretix.plugins.sendmail.tasks import send_mails
from ...helpers.format import format_map
from . import forms
from .models import Rule, ScheduledMail
@@ -203,7 +203,7 @@ class SenderView(EventPermissionRequiredMixin, FormView):
if self.request.POST.get("action") != "send":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = TolerantDict()
context_dict = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
@@ -212,9 +212,9 @@ class SenderView(EventPermissionRequiredMixin, FormView):
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
preview_subject = subject.format_map(context_dict)
preview_subject = format_map(subject, context_dict)
message = form.cleaned_data['message'].localize(l)
preview_text = markdown_compile_email(message.format_map(context_dict))
preview_text = markdown_compile_email(format_map(message, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
@@ -350,7 +350,7 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
if self.request.POST.get("action") == "preview":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = TolerantDict()
context_dict = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
@@ -359,9 +359,9 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
preview_subject = subject.format_map(context_dict)
preview_subject = format_map(subject, context_dict)
template = form.cleaned_data['template'].localize(l)
preview_text = markdown_compile_email(template.format_map(context_dict))
preview_text = markdown_compile_email(format_map(template, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
@@ -427,7 +427,7 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
for lang in self.request.event.settings.locales:
with language(lang, self.request.event.settings.region):
placeholders = TolerantDict()
placeholders = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order', 'position_or_address']).items():
placeholders[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
@@ -435,9 +435,9 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
)
subject = bleach.clean(self.object.subject.localize(lang), tags=[])
preview_subject = subject.format_map(placeholders)
preview_subject = format_map(subject, placeholders)
template = self.object.template.localize(lang)
preview_text = markdown_compile_email(template.format_map(placeholders))
preview_text = markdown_compile_email(format_map(template, placeholders))
o[lang] = {
'subject': _('Subject: {subject}'.format(subject=preview_subject)),
+1
View File
@@ -113,6 +113,7 @@ class ContactForm(forms.Form):
class InvoiceAddressForm(BaseInvoiceAddressForm):
required_css_class = 'required'
vat_warning = True
address_validation = True
def __init__(self, *args, **kwargs):
allow_save = kwargs.pop('allow_save', False)
+2 -4
View File
@@ -30,6 +30,7 @@ from django.utils.translation import gettext as _
from pretix.base.email import get_email_context
from pretix.base.models import Event
from pretix.helpers.format import format_map
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -112,9 +113,6 @@ def get_private_icals(event, positions):
- It would be pretty hard to implement it in a way that doesn't require us to use distinct
settings fields for emails to customers and to attendees, which feels like an overcomplication.
"""
from pretix.base.services.mail import TolerantDict
tz = pytz.timezone(event.settings.timezone)
creation_time = datetime.datetime.now(pytz.utc)
@@ -131,7 +129,7 @@ def get_private_icals(event, positions):
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = str(event.settings.mail_attach_ical_description).format_map(TolerantDict(ctx))
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
+4 -1
View File
@@ -377,6 +377,7 @@ INSTALLED_APPS = [
'django_countries',
'hijack',
'oauth2_provider',
'localflavor',
'phonenumber_field'
]
@@ -860,6 +861,7 @@ AUTH_PASSWORD_VALIDATORS = [
OAUTH2_PROVIDER_APPLICATION_MODEL = 'pretixapi.OAuthApplication'
OAUTH2_PROVIDER_GRANT_MODEL = 'pretixapi.OAuthGrant'
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'pretixapi.OAuthAccessToken'
OAUTH2_PROVIDER_ID_TOKEN_MODEL = 'pretixapi.OAuthIDToken'
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'pretixapi.OAuthRefreshToken'
OAUTH2_PROVIDER = {
'SCOPES': {
@@ -871,7 +873,8 @@ OAUTH2_PROVIDER = {
'ALLOWED_REDIRECT_URI_SCHEMES': ['https'] if not DEBUG else ['http', 'https'],
'ACCESS_TOKEN_EXPIRE_SECONDS': 3600 * 24,
'ROTATE_REFRESH_TOKEN': False,
'PKCE_REQUIRED': False,
'OIDC_RESPONSE_TYPES_SUPPORTED': ["code"], # We don't support proper OIDC for now
}
COUNTRIES_OVERRIDE = {
+124 -124
View File
@@ -8,14 +8,14 @@
"name": "pretix",
"version": "0.0.0",
"dependencies": {
"@babel/core": "^7.19.6",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@rollup/plugin-babel": "^6.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "^2.79.1",
"rollup-plugin-vue": "^5.0.1",
"vue": "^2.7.13",
"vue-template-compiler": "^2.7.13"
"vue": "^2.7.14",
"vue-template-compiler": "^2.7.14"
}
},
"node_modules/@ampproject/remapping": {
@@ -50,20 +50,20 @@
}
},
"node_modules/@babel/core": {
"version": "7.19.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz",
"integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
"integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.19.6",
"@babel/helper-compilation-targets": "^7.19.3",
"@babel/helper-module-transforms": "^7.19.6",
"@babel/helpers": "^7.19.4",
"@babel/parser": "^7.19.6",
"@babel/generator": "^7.20.5",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-module-transforms": "^7.20.2",
"@babel/helpers": "^7.20.5",
"@babel/parser": "^7.20.5",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.19.6",
"@babel/types": "^7.19.4",
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -98,11 +98,11 @@
}
},
"node_modules/@babel/generator": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
"integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
"integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
"dependencies": {
"@babel/types": "^7.20.0",
"@babel/types": "^7.20.5",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -295,18 +295,18 @@
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.19.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz",
"integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==",
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz",
"integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==",
"dependencies": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
"@babel/helper-simple-access": "^7.19.4",
"@babel/helper-simple-access": "^7.20.2",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.19.6",
"@babel/types": "^7.19.4"
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2"
},
"engines": {
"node": ">=6.9.0"
@@ -364,11 +364,11 @@
}
},
"node_modules/@babel/helper-simple-access": {
"version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz",
"integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==",
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
"integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
"dependencies": {
"@babel/types": "^7.19.4"
"@babel/types": "^7.20.2"
},
"engines": {
"node": ">=6.9.0"
@@ -435,13 +435,13 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
"integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
"integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
"dependencies": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.0"
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5"
},
"engines": {
"node": ">=6.9.0"
@@ -461,9 +461,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
"integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1527,18 +1527,18 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
"integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
"integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.1",
"@babel/generator": "^7.20.5",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.1",
"@babel/types": "^7.20.0",
"@babel/parser": "^7.20.5",
"@babel/types": "^7.20.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -1547,9 +1547,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz",
"integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"dependencies": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
@@ -1610,9 +1610,9 @@
}
},
"node_modules/@rollup/plugin-babel": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.2.tgz",
"integrity": "sha512-Vnt8XIWYwCf3MD7qhBWYlP9pjSZvcE++nlPXhQYw6YNehl5742AzFbrV6h4BHb20VAOVUlIksVLymQCTwVCGDg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.3.tgz",
"integrity": "sha512-fKImZKppa1A/gX73eg4JGo+8kQr/q1HBQaCGKECZ0v4YBBv3lFqi14+7xyApECzvkLTHCifx+7ntcrvtBIRcpg==",
"dependencies": {
"@babel/helper-module-imports": "^7.18.6",
"@rollup/pluginutils": "^5.0.1"
@@ -1710,9 +1710,9 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
},
"node_modules/@vue/compiler-sfc": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.13.tgz",
"integrity": "sha512-zzu2rLRZlgIU+OT3Atbr7Y6PG+LW4wVQpPfNRrGDH3dM9PsrcVfa+1pKb8bW467bGM3aDOvAnsYLWVpYIv3GRg==",
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
"integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
"dependencies": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
@@ -1720,9 +1720,9 @@
}
},
"node_modules/@vue/compiler-sfc/node_modules/postcss": {
"version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
"version": "8.4.19",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
"integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
"funding": [
{
"type": "opencollective",
@@ -2358,9 +2358,9 @@
}
},
"node_modules/decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"optional": true,
"engines": {
"node": ">=0.10"
@@ -3934,11 +3934,11 @@
}
},
"node_modules/vue": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.13.tgz",
"integrity": "sha512-QnM6ULTNnPmn71eUO+4hdjfBIA3H0GLsBnchnI/kS678tjI45GOUZhXd0oP/gX9isikXz1PAzSnkPspp9EUNfQ==",
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
"integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
"dependencies": {
"@vue/compiler-sfc": "2.7.13",
"@vue/compiler-sfc": "2.7.14",
"csstype": "^3.1.0"
}
},
@@ -3948,9 +3948,9 @@
"integrity": "sha512-pZfGp+PW/IXEOyETE09xQHR1CKkR9HfHZdnMD/FVLUNI+HxYTa82evx5WrF6Kz4s82qtqHvMZ8MZpbk2zT2E1Q=="
},
"node_modules/vue-template-compiler": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz",
"integrity": "sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog==",
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
@@ -4049,20 +4049,20 @@
"integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ=="
},
"@babel/core": {
"version": "7.19.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz",
"integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
"integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.19.6",
"@babel/helper-compilation-targets": "^7.19.3",
"@babel/helper-module-transforms": "^7.19.6",
"@babel/helpers": "^7.19.4",
"@babel/parser": "^7.19.6",
"@babel/generator": "^7.20.5",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-module-transforms": "^7.20.2",
"@babel/helpers": "^7.20.5",
"@babel/parser": "^7.20.5",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.19.6",
"@babel/types": "^7.19.4",
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -4083,11 +4083,11 @@
}
},
"@babel/generator": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
"integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
"integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
"requires": {
"@babel/types": "^7.20.0",
"@babel/types": "^7.20.5",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -4229,18 +4229,18 @@
}
},
"@babel/helper-module-transforms": {
"version": "7.19.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz",
"integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==",
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz",
"integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==",
"requires": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
"@babel/helper-simple-access": "^7.19.4",
"@babel/helper-simple-access": "^7.20.2",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.19.6",
"@babel/types": "^7.19.4"
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2"
}
},
"@babel/helper-optimise-call-expression": {
@@ -4280,11 +4280,11 @@
}
},
"@babel/helper-simple-access": {
"version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz",
"integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==",
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
"integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
"requires": {
"@babel/types": "^7.19.4"
"@babel/types": "^7.20.2"
}
},
"@babel/helper-skip-transparent-expression-wrappers": {
@@ -4330,13 +4330,13 @@
}
},
"@babel/helpers": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
"integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
"integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
"requires": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.0"
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5"
}
},
"@babel/highlight": {
@@ -4350,9 +4350,9 @@
}
},
"@babel/parser": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
"integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw=="
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA=="
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.18.6",
@@ -5045,26 +5045,26 @@
}
},
"@babel/traverse": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
"integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
"integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.1",
"@babel/generator": "^7.20.5",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.1",
"@babel/types": "^7.20.0",
"@babel/parser": "^7.20.5",
"@babel/types": "^7.20.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz",
"integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
@@ -5112,9 +5112,9 @@
}
},
"@rollup/plugin-babel": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.2.tgz",
"integrity": "sha512-Vnt8XIWYwCf3MD7qhBWYlP9pjSZvcE++nlPXhQYw6YNehl5742AzFbrV6h4BHb20VAOVUlIksVLymQCTwVCGDg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.3.tgz",
"integrity": "sha512-fKImZKppa1A/gX73eg4JGo+8kQr/q1HBQaCGKECZ0v4YBBv3lFqi14+7xyApECzvkLTHCifx+7ntcrvtBIRcpg==",
"requires": {
"@babel/helper-module-imports": "^7.18.6",
"@rollup/pluginutils": "^5.0.1"
@@ -5176,9 +5176,9 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
},
"@vue/compiler-sfc": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.13.tgz",
"integrity": "sha512-zzu2rLRZlgIU+OT3Atbr7Y6PG+LW4wVQpPfNRrGDH3dM9PsrcVfa+1pKb8bW467bGM3aDOvAnsYLWVpYIv3GRg==",
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
"integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
"requires": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
@@ -5186,9 +5186,9 @@
},
"dependencies": {
"postcss": {
"version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
"version": "8.4.19",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
"integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
@@ -5677,9 +5677,9 @@
"optional": true
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"optional": true
},
"deepmerge": {
@@ -6909,11 +6909,11 @@
"optional": true
},
"vue": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.13.tgz",
"integrity": "sha512-QnM6ULTNnPmn71eUO+4hdjfBIA3H0GLsBnchnI/kS678tjI45GOUZhXd0oP/gX9isikXz1PAzSnkPspp9EUNfQ==",
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
"integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
"requires": {
"@vue/compiler-sfc": "2.7.13",
"@vue/compiler-sfc": "2.7.14",
"csstype": "^3.1.0"
}
},
@@ -6923,9 +6923,9 @@
"integrity": "sha512-pZfGp+PW/IXEOyETE09xQHR1CKkR9HfHZdnMD/FVLUNI+HxYTa82evx5WrF6Kz4s82qtqHvMZ8MZpbk2zT2E1Q=="
},
"vue-template-compiler": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz",
"integrity": "sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog==",
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
"requires": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
+4 -4
View File
@@ -4,13 +4,13 @@
"private": true,
"scripts": {},
"dependencies": {
"@babel/core": "^7.19.6",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@rollup/plugin-babel": "^6.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"vue": "^2.7.13",
"vue": "^2.7.14",
"rollup": "^2.79.1",
"rollup-plugin-vue": "^5.0.1",
"vue-template-compiler": "^2.7.13"
"vue-template-compiler": "^2.7.14"
}
}
@@ -499,6 +499,10 @@ var form_handlers = function (el) {
el.find('input[data-typeahead-url]').each(function () {
var $inp = $(this);
if ($inp.data("ttTypeahead") || $inp.hasClass("tt-hint")) {
// Already initialized on this element
return;
}
$inp.typeahead(null, {
minLength: 1,
highlight: true,
+18 -11
View File
@@ -187,6 +187,11 @@ function setup_basics(el) {
$($(this).attr("data-parent")).find(".collapse.in").collapse('hide');
$($(this).attr("data-target")).collapse('show');
});
el.find("input[data-toggle=radiocollapse]:checked").each(function () {
if (!$($(this).attr("data-parent")).find(".collapse.in").length) {
$($(this).attr("data-target")).collapse('show');
}
});
el.find(".js-only").removeClass("js-only");
el.find(".js-hidden").hide();
@@ -287,14 +292,14 @@ $(function () {
$(".js-copy-answers").click(function (e) {
e.preventDefault();
e.stopPropagation();
let idx = $(this).data('id');
const addonDivs = $('div[data-idx="' + idx +'"]')
var idx = $(this).data('id');
var addonDivs = $('div[data-idx="' + idx +'"]');
addonDivs.each(function (index) {
const elements = $(this).find('input, select, textarea');
var elements = $(this).find('input, select, textarea');
const addonIdx = $(this).attr("data-addonidx");
const answersDiv = $('div[data-idx="0"][data-addonidx="' + addonIdx + '"]');
const answers = answersDiv.find('input, select, textarea');
var addonIdx = $(this).attr("data-addonidx");
var answersDiv = $('div[data-idx="' + (idx - 1) + '"][data-addonidx="' + addonIdx + '"]');
var answers = answersDiv.find('input, select, textarea');
copy_answers(elements, answers);
})
@@ -303,11 +308,11 @@ $(function () {
$(".js-copy-answers-addon").click(function (e) {
e.preventDefault();
e.stopPropagation();
const id = $(this).data('id');
const addonId = $(this).data('addonid');
const addonDiv = $('div[data-idx="' + id +'"][data-addonidx="' + addonId + '"]');
const elements = addonDiv.find('input, select, textarea');
const answers = $('*[data-idx="' + id + '"] input, *[data-idx="' + id + '"] select, *[data-idx="' + id + '"] textarea');
var id = $(this).data('id');
var addonId = $(this).data('addonid');
var addonDiv = $('div[data-idx="' + id +'"][data-addonidx="' + addonId + '"]');
var elements = addonDiv.find('input, select, textarea');
var answers = $('[data-idx="' + id + '"][data-addonidx="' + (addonId - 1) + '"] input, [data-idx="' + id + '"][data-addonidx="' + (addonId - 1) + '"] select, [data-idx="' + id + '"][data-addonidx="' + (addonId - 1) + '"] textarea').reverse();
copy_answers(elements, answers);
return false;
});
@@ -711,6 +716,8 @@ function copy_answers(elements, answers) {
input.prop("checked", answers.filter("[name$=" + suffix + "]").prop("checked"));
}
break;
case "file":
break
default:
input.val(answers.filter("[name$=" + suffix + "]").val());
}
+8 -8
View File
@@ -163,7 +163,7 @@ setup(
'BeautifulSoup4==4.11.*',
'bleach==5.0.*',
'celery==4.4.*',
'chardet==5.0.*',
'chardet==5.1.*',
'cryptography>=3.4.2',
'css-inline==0.8.*',
'defusedcsv>=1.1.0',
@@ -182,7 +182,7 @@ setup(
'django-localflavor==3.1',
'django-markup',
'django-mysql',
'django-oauth-toolkit==1.2.*',
'django-oauth-toolkit==2.2.*',
'django-otp==1.1.*',
'django-phonenumber-field==7.0.*',
'django-redis==5.2.*',
@@ -190,7 +190,7 @@ setup(
'django-statici18n==2.3.*',
'djangorestframework==3.14.*',
'dnspython<2.0', # do not upgrade, causes issues with eventlet / gunicorn 19 and we cannot upgrade gunicorn right now
'drf_ujson2==1.6.*',
'drf_ujson2==1.7.*',
'isoweek',
'jsonschema',
'kombu==4.6.*',
@@ -239,20 +239,20 @@ setup(
'coverage',
'coveralls',
'django-debug-toolbar==3.7.*',
'flake8==5.0.*',
'flake8==6.0.*',
'freezegun',
'isort==5.10.*',
'pep8-naming==0.12.*',
'potypo',
'pycodestyle==2.9.*',
'pyflakes==2.5.*',
'pycodestyle==2.10.*',
'pyflakes==3.0.*',
'pytest-cache',
'pytest-cov',
'pytest-django==4.*',
'pytest-mock==3.10.*',
'pytest-rerunfailures==10.2',
'pytest-rerunfailures==10.*',
'pytest-sugar',
'pytest-xdist==3.0.*',
'pytest-xdist==3.1.*',
'pytest==7.2.*',
'responses',
],
+21 -6
View File
@@ -384,7 +384,8 @@ def test_item_detail_variations(token_client, organizer, event, team, item):
"available_from": None,
"available_until": None,
"hide_without_voucher": False,
"original_price": None
"original_price": None,
"meta_data": {}
}]
res["has_variations"] = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
@@ -551,7 +552,10 @@ def test_item_create_with_variation(token_client, organizer, event, item, catego
"description": None,
"position": 0,
"default_price": None,
"price": "23.00"
"price": "23.00",
"meta_data": {
"day": "Wednesday",
},
}
]
},
@@ -564,6 +568,7 @@ def test_item_create_with_variation(token_client, organizer, event, item, catego
assert new_item.variations.first().value.localize('en') == "Comment"
assert new_item.variations.first().require_approval is True
assert set(new_item.variations.first().sales_channels) == set(get_all_sales_channels().keys())
assert new_item.variations.first().meta_data == {"day": "Wednesday"}
@pytest.mark.django_db
@@ -1258,7 +1263,8 @@ TEST_VARIATIONS_RES = {
"available_from": None,
"available_until": None,
"hide_without_voucher": False,
"original_price": None
"original_price": None,
"meta_data": {}
}
TEST_VARIATIONS_UPDATE = {
@@ -1277,7 +1283,8 @@ TEST_VARIATIONS_UPDATE = {
"available_from": None,
"available_until": None,
"hide_without_voucher": False,
"original_price": None
"original_price": None,
"meta_data": {}
}
@@ -1314,7 +1321,10 @@ def test_variations_create(token_client, organizer, event, item, variation):
"position": 1,
"default_price": None,
"original_price": "23.42",
"price": 23.0
"price": 23.0,
"meta_data": {
"day": "Wednesday",
},
},
format='json'
)
@@ -1324,6 +1334,7 @@ def test_variations_create(token_client, organizer, event, item, variation):
assert var.position == 1
assert var.price == 23.0
assert set(var.sales_channels) == set(get_all_sales_channels().keys())
assert var.meta_data == {"day": "Wednesday"}
@pytest.mark.django_db
@@ -1355,6 +1366,7 @@ def test_variations_update(token_client, organizer, event, item, item3, variatio
res["price"] = "20.00"
res["default_price"] = "20.00"
res["original_price"] = "50.00"
res["meta_data"] = {"day": "Thursday"}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/variations/{}/'.format(organizer.slug, event.slug, item.pk, variation.pk),
{
@@ -1364,7 +1376,10 @@ def test_variations_update(token_client, organizer, event, item, item3, variatio
"position": 1,
"sales_channels": ["web"],
"default_price": "20.00",
"original_price": "50.00"
"original_price": "50.00",
"meta_data": {
"day": "Thursday",
},
},
format='json'
)
+38 -17
View File
@@ -37,6 +37,7 @@ import json
from urllib.parse import quote
import pytest
from django.utils.crypto import get_random_string
from pretix.api.models import (
OAuthAccessToken, OAuthApplication, OAuthGrant, OAuthRefreshToken,
@@ -64,12 +65,17 @@ def admin_user(admin_team):
@pytest.fixture
def application():
return OAuthApplication.objects.create(
secret = get_random_string(32)
a = OAuthApplication.objects.create(
name="pretalx",
redirect_uris="https://pretalx.com",
client_type='confidential',
client_secret=secret,
authorization_grant_type='authorization-code'
)
a._cached_secret = secret
a.save()
return a
@pytest.mark.django_db
@@ -267,7 +273,7 @@ def test_token_from_code(client, admin_user, organizer, application: OAuthApplic
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
assert data['expires_in'] == 86400
@@ -306,7 +312,7 @@ def test_use_token_for_access_one_organizer(client, admin_user, organizer, appli
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
@@ -348,7 +354,7 @@ def test_use_token_for_access_two_organizers(client, admin_user, organizer, appl
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
@@ -389,7 +395,7 @@ def test_token_refresh(client, admin_user, organizer, application: OAuthApplicat
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
refresh_token = data['refresh_token']
@@ -398,7 +404,7 @@ def test_token_refresh(client, admin_user, organizer, application: OAuthApplicat
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
assert not OAuthAccessToken.objects.filter(token=access_token).exists() # old token revoked
data = json.loads(resp.content.decode())
@@ -431,7 +437,7 @@ def test_allow_write(client, admin_user, organizer, application: OAuthApplicatio
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
@@ -463,7 +469,7 @@ def test_allow_read_only(client, admin_user, organizer, application: OAuthApplic
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
@@ -495,7 +501,7 @@ def test_token_revoke_refresh_token(client, admin_user, organizer, application:
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
refresh_token = data['refresh_token']
@@ -503,7 +509,7 @@ def test_token_revoke_refresh_token(client, admin_user, organizer, application:
resp = client.post('/api/v1/oauth/revoke_token', data={
'token': refresh_token,
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
assert not OAuthAccessToken.objects.get(token=access_token).is_valid()
assert not OAuthRefreshToken.objects.filter(token=refresh_token, revoked__isnull=True).exists()
@@ -511,7 +517,7 @@ def test_token_revoke_refresh_token(client, admin_user, organizer, application:
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 400
@@ -539,7 +545,7 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
refresh_token = data['refresh_token']
@@ -547,7 +553,7 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O
resp = client.post('/api/v1/oauth/revoke_token', data={
'token': access_token,
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
assert not OAuthAccessToken.objects.get(token=access_token).is_valid() # old token revoked
@@ -555,7 +561,7 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
@@ -587,7 +593,7 @@ def test_user_revoke(client, admin_user, organizer, application: OAuthApplicatio
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
refresh_token = data['refresh_token']
@@ -606,7 +612,7 @@ def test_user_revoke(client, admin_user, organizer, application: OAuthApplicatio
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 400
@@ -634,7 +640,7 @@ def test_allow_profile_only(client, admin_user, organizer, application: OAuthApp
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
@@ -642,3 +648,18 @@ def test_allow_profile_only(client, admin_user, organizer, application: OAuthApp
assert resp.status_code == 403
resp = client.get('/api/v1/me', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 200
@pytest.mark.django_db
def test_reject_other_response_types(client, admin_user, organizer, application: OAuthApplication):
client.login(email='dummy@dummy.dummy', password='dummy')
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code+id_token' % (
application.client_id, quote(application.redirect_uris)
))
assert resp.status_code == 302
assert 'error=unauthorized_client' in resp['Location']
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=id_token' % (
application.client_id, quote(application.redirect_uris)
))
assert resp.status_code == 302
assert 'error=unsupported_response_type' in resp['Location']
+145
View File
@@ -0,0 +1,145 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import pytest
from django.core.exceptions import ValidationError
from pretix.base.addressvalidation import validate_address
@pytest.mark.parametrize(
"input,output,all_optional",
[
# No address is allowed
({"name": "Peter"}, {"name": "Peter"}, False),
# Country must be given if any part of the address is filled
({"street": "Main Street"}, {"country": ["This field is required."]}, False),
# Country without any semantic validation
(
{"street": "Main Street", "country": "CR"},
{"street": "Main Street", "country": "CR"},
False,
),
# Country that requires all fields except state to be filled
(
{"street": "Main Street", "country": "DE"},
{"zipcode": ["This field is required."]},
False,
),
(
{"street": "Main Street", "country": "DE", "zipcode": "12345"},
{"city": ["This field is required."]},
False,
),
(
{"city": "Heidelberg", "country": "DE", "zipcode": "12345"},
{"street": ["This field is required."]},
False,
),
# All-optional flag works
(
{"street": "Main Street", "country": "DE"},
{"street": "Main Street", "country": "DE"},
True,
),
(
{"street": "Main Street", "country": "DE", "zipcode": "12345"},
{"street": "Main Street", "country": "DE", "zipcode": "12345"},
True,
),
(
{"city": "Heidelberg", "country": "DE", "zipcode": "12345"},
{"city": "Heidelberg", "country": "DE", "zipcode": "12345"},
True,
),
(
{
"street": "Main Street",
"city": "Heidelberg",
"country": "DE",
"zipcode": "12345",
},
True,
False,
),
# Country that requires state to be filled
(
{
"street": "Main street",
"city": "Heidelberg",
"country": "US",
"zipcode": "12345",
},
{"state": ["This field is required."]},
False,
),
# Country with zip code validation inherited from django-localflavor
(
{
"street": "Main street",
"city": "Heidelberg",
"country": "DE",
"zipcode": "ABCDE",
},
{"zipcode": ["Enter a zip code in the format XXXXX."]},
False,
),
# Country with zip code validation implemented directly
(
{
"street": "Main street",
"city": "Heidelberg",
"country": "IS",
"zipcode": "ABCDE",
},
{"zipcode": ["Enter a postal code in the format XXX."]},
False,
),
# Country with zip code normalization inherited from django-localflavor
(
{
"street": "Main street",
"city": "London",
"country": "GB",
"zipcode": "se19de",
},
{
"street": "Main street",
"city": "London",
"country": "GB",
"zipcode": "SE1 9DE",
},
False,
),
],
)
def test_validate_address(input, output, all_optional):
try:
actual_output = validate_address(input, all_optional)
except ValidationError as e:
assert {
k: ["".join(s for s in e) for e in v] for k, v in e.error_dict.items()
} == output
else:
if output is True:
assert actual_output == input
else:
assert output == actual_output
+2
View File
@@ -73,6 +73,7 @@ def test_full_clone_same_organizer():
assert item1.meta_data
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15)
item2v = item2.variations.create(value="red", default_price=15)
item2v.meta_values.create(property=item_meta, value="Bar")
item2.require_membership_types.add(membership_type)
ItemAddOn.objects.create(base_item=item1, addon_category=category)
ItemBundle.objects.create(base_item=item1, bundled_item=item2, bundled_variation=item2v)
@@ -147,6 +148,7 @@ def test_full_clone_same_organizer():
assert copied_item1.tax_rule == copied_event.tax_rules.get()
assert copied_item1.category == copied_event.categories.get()
assert copied_item1.meta_data == item1.meta_data
assert copied_item2.variations.get().meta_data == item2v.meta_data
assert copied_item1.hidden_if_available == copied_q2
assert copied_item1.grant_membership_type == membership_type
assert copied_item2.variations.count() == 1
+26
View File
@@ -1962,6 +1962,32 @@ class ItemTest(TestCase):
assert not Item.objects.filter_available().exists()
assert Item.objects.filter_available(voucher=v).exists()
@classscope(attr='o')
def test_meta_data_inheritance(self):
prop = self.event.item_meta_properties.create(name="day", default="Monday")
i = Item.objects.create(
event=self.event, name="Ticket", default_price=23,
active=True, available_until=now() + timedelta(days=1),
)
v = i.variations.create(value="Day 1")
assert i.meta_data == {"day": "Monday"}
assert v.meta_data == {"day": "Monday"}
i.meta_values.create(property=prop, value="Tuesday")
i = Item.objects.get(pk=i.pk)
v = ItemVariation.objects.get(pk=v.pk)
assert i.meta_data == {"day": "Tuesday"}
assert v.meta_data == {"day": "Tuesday"}
v.meta_values.create(property=prop, value="Wednesday")
i = Item.objects.get(pk=i.pk)
v = ItemVariation.objects.get(pk=v.pk)
assert i.meta_data == {"day": "Tuesday"}
assert v.meta_data == {"day": "Wednesday"}
class EventTest(TestCase):
@classmethod
+3
View File
@@ -637,6 +637,8 @@ class ItemsTest(ItemFormTest):
q = Question.objects.create(event=self.event1, question="Size", type="N")
q.items.add(self.item2)
self.item2.sales_channels = ["web", "bar"]
prop = self.event1.item_meta_properties.create(name="Foo")
self.item2.meta_values.create(property=prop, value="Bar")
self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
'name_0': 'Intermediate',
@@ -657,6 +659,7 @@ class ItemsTest(ItemFormTest):
assert i_new.hide_without_voucher == i_old.hide_without_voucher
assert i_new.allow_cancel == i_old.allow_cancel
assert i_new.sales_channels == i_old.sales_channels
assert i_new.meta_data == i_old.meta_data == {"Foo": "Bar"}
assert set(i_new.questions.all()) == set(i_old.questions.all())
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])
+30
View File
@@ -0,0 +1,30 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from pretix.helpers.format import format_map
def test_format_map():
assert format_map("Foo {bar}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}"
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"
+3 -1
View File
@@ -24,11 +24,13 @@ from openpyxl.cell.cell import TYPE_STRING
from pretix.helpers.safe_openpyxl import SafeWorkbook
def test_nullbyte_removed():
def test_invalid_byte_removed():
wb = SafeWorkbook()
ws = wb.create_sheet()
ws.append(["foo\u0000bar"])
assert ws.cell(1, 1).value == "foobar"
ws.append(["foo\uffffbaz"])
assert ws.cell(2, 1).value == "foobaz"
def test_no_formulas():
+1 -1
View File
@@ -249,7 +249,7 @@ EXPECTED = [
],
[
{'amount': '5000.00', 'date': '2002-03-17',
'reference': '68790452\nVerwendungszweck 1\nVerwendungszweck 2\nEmpfänger\nBuchungstext\n12345\n1000\n4711'},
'reference': '68790452\nVerwendungszweck 1\nVerwendungszweck 2\nEmpf√§nger\nBuchungstext\n12345\n1000\n4711'},
{'amount': '20000.00', 'date': '2002-03-22', 'reference': ''},
{'amount': '20000.00', 'date': '2002-03-22', 'reference': ''},
{'amount': '20000.00', 'date': '2002-03-22', 'reference': ''},
+77 -17
View File
@@ -170,7 +170,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -193,7 +193,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'is_business': 'individual',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': '',
@@ -229,7 +229,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -259,7 +259,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '3000',
'city': 'Here',
'country': 'AU',
'state': 'QLD',
@@ -293,7 +293,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -324,7 +324,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'FR',
'vat_id': 'AT123456',
@@ -357,7 +357,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -391,7 +391,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -428,7 +428,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -492,7 +492,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -551,7 +551,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'is_business': 'individual',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost'
@@ -603,7 +603,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'is_business': 'individual',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost'
@@ -631,7 +631,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'is_business': 'individual',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '99501',
'city': 'Here',
'country': 'US',
'state': 'CA',
@@ -684,7 +684,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1345',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -748,7 +748,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '12345',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
@@ -1102,6 +1102,57 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
}
assert ia.name_cached == 'Mr John Kennedy'
def test_invoice_address_validated(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_address_not_asked_free = True
self.event.settings.set('name_scheme', 'title_given_middle_family')
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="city"]')), 1)
# Not all required fields filled out correctly, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '123456',
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '12345',
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
def test_invoice_address_hidden_for_free(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
@@ -1136,10 +1187,19 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="city"]')), 1)
# Not all required fields filled out, expect failure
# Partial address is not allowed
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'country': 'DE',
'city': 'Musterstadt',
'email': 'admin@localhost'
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
# No address works
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
+36 -1
View File
@@ -345,6 +345,35 @@ class OrdersTest(BaseOrdersTest):
with scopes_disabled():
assert self.ticket_pos.answers.get(question=self.question).answer == 'ABC'
def test_modify_invoice_address_validated(self):
self.event.settings.set('invoice_reissue_after_modify', True)
self.event.settings.set('invoice_address_asked', True)
with scopes_disabled():
generate_invoice(self.order)
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret),
target_status_code=200)
# Only questions changed
with scopes_disabled():
assert self.order.invoices.count() == 1
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
'zipcode': 'XXINVALIDXX',
'street': 'Main Street',
'city': 'Heidelberg',
'country': 'DE',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
def test_modify_invoice_regenerate(self):
self.event.settings.set('invoice_reissue_after_modify', True)
self.event.settings.set('invoice_address_asked', True)
@@ -366,7 +395,10 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
'zipcode': '1234',
'zipcode': '12345',
'street': 'Main Street',
'city': 'Heidelberg',
'country': 'DE',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -381,6 +413,9 @@ class OrdersTest(BaseOrdersTest):
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
'zipcode': '54321',
'street': 'Main Street',
'city': 'Heidelberg',
'country': 'DE',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,