mirror of
https://github.com/pretix/pretix.git
synced 2026-01-03 18:52:26 +00:00
Compare commits
16 Commits
pdf-bulk-v
...
quota-cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e54837c532 | ||
|
|
bc49f0f7f1 | ||
|
|
3e122e0270 | ||
|
|
e8ea6e0f5c | ||
|
|
e94e5be878 | ||
|
|
1073ea626e | ||
|
|
23ab8df443 | ||
|
|
d6caf01a38 | ||
|
|
1424ae78e9 | ||
|
|
827382edc3 | ||
|
|
85482bc939 | ||
|
|
42ce545f2f | ||
|
|
e49bc5d78d | ||
|
|
6e7a32ef2a | ||
|
|
37df7a6313 | ||
|
|
d5951415a4 |
@@ -96,6 +96,20 @@ http://localhost:8000/control/ for the admin view.
|
||||
port (for example because you develop on `pretixdroid`_), you can check
|
||||
`Django's documentation`_ for more options.
|
||||
|
||||
When running the local development webserver, ensure Celery is not configured
|
||||
in ``pretix.cfg``. i.e., you should remove anything such as::
|
||||
|
||||
[celery]
|
||||
backend=redis://redis:6379/2
|
||||
broker=redis://redis:6379/2
|
||||
|
||||
If you choose to use Celery for development, you must also start a Celery worker
|
||||
process::
|
||||
|
||||
celery -A pretix.celery_app worker -l info
|
||||
|
||||
However, beware that code changes will not auto-reload within Celery.
|
||||
|
||||
.. _`checksandtests`:
|
||||
|
||||
Code checks and unit tests
|
||||
|
||||
@@ -90,7 +90,7 @@ dependencies = [
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==7.4.*",
|
||||
"redis==4.5.*,>=4.5.4",
|
||||
"redis==4.6.*",
|
||||
"reportlab==4.0.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==1.15.*",
|
||||
@@ -112,6 +112,7 @@ memcached = ["pylibmc"]
|
||||
dev = [
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.18.*",
|
||||
"flake8==6.0.*",
|
||||
"freezegun",
|
||||
"isort==5.12.*",
|
||||
|
||||
@@ -27,6 +27,7 @@ from decimal import Decimal
|
||||
import pycountry
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.timezone import now
|
||||
@@ -372,11 +373,15 @@ class PdfDataSerializer(serializers.Field):
|
||||
self.context['vars_images'] = get_images(self.context['event'])
|
||||
|
||||
for k, f in self.context['vars'].items():
|
||||
try:
|
||||
res[k] = f['evaluate'](instance, instance.order, ev)
|
||||
except:
|
||||
logger.exception('Evaluating PDF variable failed')
|
||||
res[k] = '(error)'
|
||||
if 'evaluate_bulk' in f:
|
||||
# Will be evaluated later by our list serializers
|
||||
res[k] = (f['evaluate_bulk'], instance)
|
||||
else:
|
||||
try:
|
||||
res[k] = f['evaluate'](instance, instance.order, ev)
|
||||
except:
|
||||
logger.exception('Evaluating PDF variable failed')
|
||||
res[k] = '(error)'
|
||||
|
||||
if not hasattr(ev, '_cached_meta_data'):
|
||||
ev._cached_meta_data = ev.meta_data
|
||||
@@ -429,6 +434,38 @@ class PdfDataSerializer(serializers.Field):
|
||||
return res
|
||||
|
||||
|
||||
class OrderPositionListSerializer(serializers.ListSerializer):
|
||||
|
||||
def to_representation(self, data):
|
||||
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements unevaluated
|
||||
# with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to save on SQL queries.
|
||||
|
||||
if isinstance(self.parent, OrderSerializer) and isinstance(self.parent.parent, OrderListSerializer):
|
||||
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
|
||||
# full result set.
|
||||
return super().to_representation(data)
|
||||
|
||||
iterable = data.all() if isinstance(data, models.Manager) else data
|
||||
|
||||
data = []
|
||||
evaluate_queue = defaultdict(list)
|
||||
|
||||
for item in iterable:
|
||||
entry = self.child.to_representation(item)
|
||||
if "pdf_data" in entry:
|
||||
for k, v in entry["pdf_data"].items():
|
||||
if isinstance(v, tuple) and callable(v[0]):
|
||||
evaluate_queue[v[0]].append((v[1], entry, k))
|
||||
data.append(entry)
|
||||
|
||||
for func, entries in evaluate_queue.items():
|
||||
results = func([item for (item, entry, k) in entries])
|
||||
for (item, entry, k), result in zip(entries, results):
|
||||
entry["pdf_data"][k] = result
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = CheckinSerializer(many=True, read_only=True)
|
||||
answers = AnswerSerializer(many=True)
|
||||
@@ -440,6 +477,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = OrderPositionListSerializer
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||
@@ -468,6 +506,20 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
def validate(self, data):
|
||||
raise TypeError("this serializer is readonly")
|
||||
|
||||
def to_representation(self, data):
|
||||
if isinstance(self.parent, (OrderListSerializer, OrderPositionListSerializer)):
|
||||
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
|
||||
# full result set.
|
||||
return super().to_representation(data)
|
||||
|
||||
entry = super().to_representation(data)
|
||||
if "pdf_data" in entry:
|
||||
for k, v in entry["pdf_data"].items():
|
||||
if isinstance(v, tuple) and callable(v[0]):
|
||||
entry["pdf_data"][k] = v[0]([v[1]])[0]
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
class RequireAttentionField(serializers.Field):
|
||||
def to_representation(self, instance: OrderPosition):
|
||||
@@ -613,6 +665,34 @@ class OrderURLField(serializers.URLField):
|
||||
})
|
||||
|
||||
|
||||
class OrderListSerializer(serializers.ListSerializer):
|
||||
|
||||
def to_representation(self, data):
|
||||
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements
|
||||
# unevaluated with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to
|
||||
# save on SQL queries.
|
||||
iterable = data.all() if isinstance(data, models.Manager) else data
|
||||
|
||||
data = []
|
||||
evaluate_queue = defaultdict(list)
|
||||
|
||||
for item in iterable:
|
||||
entry = self.child.to_representation(item)
|
||||
for p in entry.get("positions", []):
|
||||
if "pdf_data" in p:
|
||||
for k, v in p["pdf_data"].items():
|
||||
if isinstance(v, tuple) and callable(v[0]):
|
||||
evaluate_queue[v[0]].append((v[1], p, k))
|
||||
data.append(entry)
|
||||
|
||||
for func, entries in evaluate_queue.items():
|
||||
results = func([item for (item, entry, k) in entries])
|
||||
for (item, entry, k), result in zip(entries, results):
|
||||
entry["pdf_data"][k] = result
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||
@@ -627,6 +707,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
list_serializer_class = OrderListSerializer
|
||||
fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
|
||||
@@ -43,6 +43,7 @@ from typing import Optional, Tuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import dateutil.parser
|
||||
import django_redis
|
||||
from dateutil.tz import datetime_exists
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -57,7 +58,6 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import is_naive, make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from django_redis import get_redis_connection
|
||||
from django_scopes import ScopedManager
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
@@ -1910,8 +1910,13 @@ class Quota(LoggedModel):
|
||||
|
||||
def rebuild_cache(self, now_dt=None):
|
||||
if settings.HAS_REDIS:
|
||||
rc = get_redis_connection("redis")
|
||||
rc.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
|
||||
rc = django_redis.get_redis_connection("redis")
|
||||
p = rc.pipeline()
|
||||
p.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
|
||||
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw', str(self.pk))
|
||||
p.hdel(f'quotas:{self.event_id}:availabilitycache:igcl', str(self.pk))
|
||||
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw:igcl', str(self.pk))
|
||||
p.execute()
|
||||
self.availability(now_dt=now_dt)
|
||||
|
||||
def availability(
|
||||
|
||||
@@ -108,7 +108,10 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
("positionid", {
|
||||
"label": _("Order position number"),
|
||||
"editor_sample": "1",
|
||||
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
|
||||
"evaluate": lambda orderposition, order, event: str(orderposition.positionid),
|
||||
# There is no performance gain in using evaluate_bulk here, but we want to make sure it is used somewhere
|
||||
# in core to make sure we notice if the implementation of the API breaks.
|
||||
"evaluate_bulk": lambda orderpositions: [str(p.positionid) for p in orderpositions],
|
||||
}),
|
||||
("order_positionid", {
|
||||
"label": _("Order code and position number"),
|
||||
|
||||
@@ -24,13 +24,13 @@ import time
|
||||
from collections import Counter, defaultdict
|
||||
from itertools import zip_longest
|
||||
|
||||
import django_redis
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import (
|
||||
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||
)
|
||||
from django.utils.timezone import now
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
from pretix.base.models import (
|
||||
CartPosition, Checkin, Order, OrderPosition, Quota, Voucher,
|
||||
@@ -102,6 +102,12 @@ class QuotaAvailability:
|
||||
self.count_waitinglist = defaultdict(int)
|
||||
self.count_cart = defaultdict(int)
|
||||
|
||||
self._cache_key_suffix = ""
|
||||
if not self._count_waitinglist:
|
||||
self._cache_key_suffix += ":nocw"
|
||||
if self._ignore_closed:
|
||||
self._cache_key_suffix += ":igcl"
|
||||
|
||||
self.sizes = {}
|
||||
|
||||
def queue(self, *quota):
|
||||
@@ -121,17 +127,14 @@ class QuotaAvailability:
|
||||
if self._full_results:
|
||||
raise ValueError("You cannot combine full_results and allow_cache.")
|
||||
|
||||
elif not self._count_waitinglist:
|
||||
raise ValueError("If you set allow_cache, you need to set count_waitinglist.")
|
||||
|
||||
elif settings.HAS_REDIS:
|
||||
rc = get_redis_connection("redis")
|
||||
rc = django_redis.get_redis_connection("redis")
|
||||
quotas_by_event = defaultdict(list)
|
||||
for q in [_q for _q in self._queue if _q.id in quota_ids_set]:
|
||||
quotas_by_event[q.event_id].append(q)
|
||||
|
||||
for eventid, evquotas in quotas_by_event.items():
|
||||
d = rc.hmget(f'quotas:{eventid}:availabilitycache', [str(q.pk) for q in evquotas])
|
||||
d = rc.hmget(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', [str(q.pk) for q in evquotas])
|
||||
for redisval, q in zip(d, evquotas):
|
||||
if redisval is not None:
|
||||
data = [rv for rv in redisval.decode().split(',')]
|
||||
@@ -164,12 +167,12 @@ class QuotaAvailability:
|
||||
if not settings.HAS_REDIS or not quotas:
|
||||
return
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
rc = django_redis.get_redis_connection("redis")
|
||||
# We write the computed availability to redis in a per-event hash as
|
||||
#
|
||||
# quota_id -> (availability_state, availability_number, timestamp).
|
||||
#
|
||||
# We store this in a hash instead of inidividual values to avoid making two many redis requests
|
||||
# We store this in a hash instead of individual values to avoid making too many redis requests
|
||||
# which would introduce latency.
|
||||
|
||||
# The individual entries in the hash are "valid" for 120 seconds. This means in a typical peak scenario with
|
||||
@@ -179,16 +182,16 @@ class QuotaAvailability:
|
||||
# these quotas. We choose 10 seconds since that should be well above the duration of a write.
|
||||
|
||||
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
|
||||
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}'):
|
||||
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
|
||||
return
|
||||
rc.setex(f'quotas:availabilitycachewrite:{lock_name}', '1', 10)
|
||||
rc.setex(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', 10)
|
||||
|
||||
update = defaultdict(list)
|
||||
for q in quotas:
|
||||
update[q.event_id].append(q)
|
||||
|
||||
for eventid, quotas in update.items():
|
||||
rc.hmset(f'quotas:{eventid}:availabilitycache', {
|
||||
rc.hmset(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', {
|
||||
str(q.id): ",".join(
|
||||
[str(i) for i in self.results[q]] +
|
||||
[str(int(time.time()))]
|
||||
@@ -197,7 +200,7 @@ class QuotaAvailability:
|
||||
# To make sure old events do not fill up our redis instance, we set an expiry on the cache. However, we set it
|
||||
# on 7 days even though we mostly ignore values older than 2 monites. The reasoning is that we have some places
|
||||
# where we set allow_cache_stale and use the old entries anyways to save on performance.
|
||||
rc.expire(f'quotas:{eventid}:availabilitycache', 3600 * 24 * 7)
|
||||
rc.expire(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', 3600 * 24 * 7)
|
||||
|
||||
# We used to also delete item_quota_cache:* from the event cache here, but as the cache
|
||||
# gets more complex, this does not seem worth it. The cache is only present for up to
|
||||
|
||||
@@ -683,12 +683,16 @@ dictionaries as values that contain keys like in the following example::
|
||||
"product": {
|
||||
"label": _("Product name"),
|
||||
"editor_sample": _("Sample product"),
|
||||
"evaluate": lambda orderposition, order, event: str(orderposition.item)
|
||||
"evaluate": lambda orderposition, order, event: str(orderposition.item),
|
||||
"evaluate_bulk": lambda orderpositions: [str(op.item) for op in orderpositions],
|
||||
}
|
||||
}
|
||||
|
||||
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
|
||||
also be a subevent, if applicable.
|
||||
|
||||
The ``evaluate_bulk`` member is optional but can significantly improve performance in some situations because you
|
||||
can perform database fetches in bulk instead of single queries for every position.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -461,11 +461,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
)
|
||||
|
||||
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={
|
||||
|
||||
@@ -96,7 +96,9 @@
|
||||
<tr>
|
||||
<th>
|
||||
{% if "can_change_vouchers" in request.eventpermset %}
|
||||
<input type="checkbox" data-toggle-table />
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label">
|
||||
<input type="checkbox" data-toggle-table />
|
||||
</label>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th>
|
||||
@@ -139,7 +141,9 @@
|
||||
<tr>
|
||||
<td>
|
||||
{% if "can_change_vouchers" in request.eventpermset %}
|
||||
<input type="checkbox" name="voucher" class="" value="{{ v.pk }}"/>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label">
|
||||
<input type="checkbox" name="voucher" class="batch-select-checkbox" value="{{ v.pk }}"/>
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -194,9 +198,12 @@
|
||||
</table>
|
||||
</div>
|
||||
{% if "can_change_vouchers" in request.eventpermset %}
|
||||
<button type="submit" class="btn btn-default btn-save" name="action" value="delete">
|
||||
{% trans "Delete selected" %}
|
||||
</button>
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-danger" name="action" value="delete">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
{% trans "Delete selected" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
|
||||
@@ -785,8 +785,8 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
||||
return ctx
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
v = str(request.event.settings.mail_text_order_payment_failed)
|
||||
v = format_map(v, self.placeholders('mail_text_order_payment_failed'))
|
||||
v = str(request.event.settings.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():
|
||||
|
||||
@@ -1188,30 +1188,46 @@ class MetaDataEditorMixin:
|
||||
|
||||
@cached_property
|
||||
def meta_forms(self):
|
||||
if hasattr(self, 'object') and self.object:
|
||||
if getattr(self, 'object', None):
|
||||
val_instances = {
|
||||
v.property_id: v for v in self.object.meta_values.all()
|
||||
}
|
||||
else:
|
||||
val_instances = {}
|
||||
|
||||
if getattr(self, 'copy_from', None):
|
||||
defaults = {
|
||||
v.property_id: v.value for v in self.copy_from.meta_values.all()
|
||||
}
|
||||
else:
|
||||
defaults = {}
|
||||
|
||||
formlist = []
|
||||
|
||||
for p in self.request.event.item_meta_properties.all():
|
||||
formlist.append(self._make_meta_form(p, val_instances))
|
||||
formlist.append(self._make_meta_form(p, val_instances, defaults))
|
||||
return formlist
|
||||
|
||||
def _make_meta_form(self, p, val_instances):
|
||||
def _make_meta_form(self, p, val_instances, defaults):
|
||||
return self.meta_form(
|
||||
prefix='prop-{}'.format(p.pk),
|
||||
property=p,
|
||||
instance=val_instances.get(p.pk, self.meta_model(property=p, item=self.object)),
|
||||
instance=val_instances.get(
|
||||
p.pk,
|
||||
self.meta_model(
|
||||
property=p,
|
||||
item=self.object if getattr(self, 'object', None) else None,
|
||||
value=defaults.get(p.pk, None)
|
||||
)
|
||||
),
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
)
|
||||
|
||||
def save_meta(self):
|
||||
for f in self.meta_forms:
|
||||
if f.cleaned_data.get('value'):
|
||||
if not f.instance.item_id:
|
||||
f.instance.item = self.object
|
||||
f.save()
|
||||
elif f.instance and f.instance.pk:
|
||||
f.instance.delete()
|
||||
@@ -1257,6 +1273,7 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
|
||||
ret = super().form_valid(form)
|
||||
self.save_meta()
|
||||
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
|
||||
k: (form.cleaned_data.get(k).name
|
||||
if isinstance(form.cleaned_data.get(k), File)
|
||||
@@ -1283,6 +1300,14 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = None
|
||||
form = self.get_form()
|
||||
if form.is_valid() and all([f.is_valid() for f in self.meta_forms]):
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
|
||||
form_class = ItemUpdateForm
|
||||
|
||||
28586
src/pretix/locale/cy/LC_MESSAGES/django.po
Normal file
28586
src/pretix/locale/cy/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
|
||||
"PO-Revision-Date: 2023-07-28 09:11+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2023-08-16 22:00+0000\n"
|
||||
"Last-Translator: Felix Hartnagel <felix@fhcom.de>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
"Language: de_Informal\n"
|
||||
@@ -31019,7 +31019,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:43
|
||||
msgid "Please note that we still await your payment to complete the process."
|
||||
msgstr ""
|
||||
"Bitte beachten Sie, dass wir noch deine Zahlung erwarten, um den Prozess "
|
||||
"Bitte beachte, dass wir noch deine Zahlung erwarten, um den Prozess "
|
||||
"abzuschließen."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:55
|
||||
|
||||
@@ -4,8 +4,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
|
||||
"PO-Revision-Date: 2023-08-09 03:00+0000\n"
|
||||
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
|
||||
"PO-Revision-Date: 2023-08-16 22:00+0000\n"
|
||||
"Last-Translator: Maurice Kaag <maurice@kaag.me>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language: fr\n"
|
||||
@@ -247,9 +247,7 @@ msgstr ""
|
||||
|
||||
#: pretix/api/serializers/item.py:185 pretix/control/forms/item.py:1084
|
||||
msgid "The bundled item must not have bundles on its own."
|
||||
msgstr ""
|
||||
"Un forfait ne doit pas contenir des produits, qui sont eux-mêmes des "
|
||||
"forfaits."
|
||||
msgstr "Un produit groupé ne doit pas contenir des produits groupés."
|
||||
|
||||
#: pretix/api/serializers/item.py:262
|
||||
msgid ""
|
||||
@@ -3097,7 +3095,7 @@ msgstr "Annulation"
|
||||
#: pretix/base/invoice.py:620 pretix/base/invoice.py:628
|
||||
msgctxt "invoice"
|
||||
msgid "Description"
|
||||
msgstr "Déscription"
|
||||
msgstr "Description"
|
||||
|
||||
#: pretix/base/invoice.py:621 pretix/base/invoice.py:629
|
||||
msgctxt "invoice"
|
||||
@@ -6805,16 +6803,12 @@ msgid "List of Add-Ons"
|
||||
msgstr "Liste des Addons"
|
||||
|
||||
#: pretix/base/pdf.py:364
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Add-on 1\n"
|
||||
#| "Add-on 2"
|
||||
msgid ""
|
||||
"Add-on 1\n"
|
||||
"2x Add-on 2"
|
||||
msgstr ""
|
||||
"Add-on 1\n"
|
||||
"Add-on 2"
|
||||
"2x Add-on 2"
|
||||
|
||||
#: pretix/base/pdf.py:370 pretix/control/forms/filter.py:1275
|
||||
#: pretix/control/forms/filter.py:1277
|
||||
@@ -11136,7 +11130,7 @@ msgstr "Degré (après le nom)"
|
||||
#: pretix/base/settings.py:3577
|
||||
msgctxt "person_name_sample"
|
||||
msgid "MA"
|
||||
msgstr ""
|
||||
msgstr "MA"
|
||||
|
||||
#: pretix/base/settings.py:3684 pretix/control/forms/event.py:217
|
||||
msgid ""
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
<span class="icon icon-upload"></span> {% trans "Continue" %}
|
||||
</button>
|
||||
<div class="flipped-scroll-wrapper clearfix">
|
||||
<table class="table table-condensed flipped-scroll-inner">
|
||||
<table class="table table-condensed table-th-sticky-horizontal flipped-scroll-inner">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th scope="row">{% trans "Date" %}</th>
|
||||
{% for col in rows.0 %}
|
||||
<th>
|
||||
<input type="radio" name="date" value="{{ forloop.counter0 }}"/>
|
||||
@@ -22,7 +22,7 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Amount" %}</th>
|
||||
<th scope="row">{% trans "Amount" %}</th>
|
||||
{% for col in rows.0 %}
|
||||
<th>
|
||||
<input type="radio" name="amount" value="{{ forloop.counter0 }}" required="required"/>
|
||||
@@ -30,7 +30,7 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Reference" %}</th>
|
||||
<th scope="row">{% trans "Reference" %}</th>
|
||||
{% for col in rows.0 %}
|
||||
<th>
|
||||
<input type="checkbox" name="reference" value="{{ forloop.counter0 }}"/>
|
||||
@@ -38,7 +38,7 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Payer" %}</th>
|
||||
<th scope="row">{% trans "Payer" %}</th>
|
||||
{% for col in rows.0 %}
|
||||
<th>
|
||||
<input type="checkbox" name="payer" value="{{ forloop.counter0 }}"/>
|
||||
@@ -46,7 +46,7 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<th scope="row">
|
||||
{% trans "IBAN" %}
|
||||
<label for="id_iban_clear">
|
||||
<span class="btn btn-default btn-sm fa fa-close"></span>
|
||||
@@ -62,7 +62,7 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<th scope="row">
|
||||
{% trans "BIC" %}
|
||||
<label for="id_bic_clear">
|
||||
<span class="btn btn-default btn-sm fa fa-close"></span>
|
||||
|
||||
@@ -813,7 +813,11 @@ tbody[data-dnd-url] {
|
||||
tbody th {
|
||||
background: $table-bg-hover;
|
||||
}
|
||||
|
||||
.table-th-sticky-horizontal th[scope=row] {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: linear-gradient(0deg, rgba(255,255,255,1) 0%, rgba(255,255,255,1) 96%, rgba(255,255,255,0) 100%);;
|
||||
}
|
||||
.large-link-group {
|
||||
a.list-group-item {
|
||||
&::before {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#
|
||||
from contextlib import contextmanager
|
||||
|
||||
import fakeredis
|
||||
from pytest_mock import MockFixture
|
||||
|
||||
|
||||
@@ -34,3 +35,7 @@ def mocker_context():
|
||||
result = MockFixture(FakePytestConfig())
|
||||
yield result
|
||||
result.stopall()
|
||||
|
||||
|
||||
def get_redis_connection(alias="default", write=True):
|
||||
return fakeredis.FakeStrictRedis(server=fakeredis.FakeServer.get_server("127.0.0.1:None:v(7, 0)", (7, 0)))
|
||||
|
||||
@@ -37,6 +37,7 @@ filterwarnings =
|
||||
ignore::ResourceWarning
|
||||
ignore:django.contrib.staticfiles.templatetags.static:DeprecationWarning
|
||||
ignore::DeprecationWarning:compressor
|
||||
ignore:.*FakeStrictRedis.hmset.*:DeprecationWarning:
|
||||
ignore:pkg_resources is deprecated as an API:
|
||||
ignore:.*pkg_resources.declare_namespace.*:
|
||||
|
||||
|
||||
@@ -1794,6 +1794,8 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['positions'][0].get('pdf_data')
|
||||
assert resp.data['positions'][0]['pdf_data']['positionid'] == '1'
|
||||
assert resp.data['positions'][0]['pdf_data']['order'] == order.code
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/'.format(
|
||||
organizer.slug, event.slug, order.code
|
||||
))
|
||||
@@ -1807,6 +1809,8 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['results'][0]['positions'][0].get('pdf_data')
|
||||
assert resp.data['results'][0]['positions'][0]['pdf_data']['positionid'] == '1'
|
||||
assert resp.data['results'][0]['positions'][0]['pdf_data']['order'] == order.code
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
@@ -1820,6 +1824,8 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['results'][0].get('pdf_data')
|
||||
assert resp.data['results'][0]['pdf_data']['positionid'] == '1'
|
||||
assert resp.data['results'][0]['pdf_data']['order'] == order.code
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
@@ -1834,6 +1840,8 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.get('pdf_data')
|
||||
assert resp.data['pdf_data']['positionid'] == '1'
|
||||
assert resp.data['pdf_data']['order'] == order.code
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
|
||||
organizer.slug, event.slug, posid
|
||||
))
|
||||
|
||||
@@ -98,6 +98,7 @@ class BaseQuotaTestCase(TestCase):
|
||||
self.var3 = ItemVariation.objects.create(item=self.item3, value='Fancy')
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("fakeredis_client")
|
||||
class QuotaTestCase(BaseQuotaTestCase):
|
||||
@classscope(attr='o')
|
||||
def test_available(self):
|
||||
@@ -434,6 +435,62 @@ class QuotaTestCase(BaseQuotaTestCase):
|
||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||
self.assertEqual(self.var1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_waitinglist_cache_separation(self):
|
||||
self.quota.items.add(self.item1)
|
||||
self.quota.size = 1
|
||||
self.quota.save()
|
||||
WaitingListEntry.objects.create(
|
||||
event=self.event, item=self.item1, email='foo@bar.com'
|
||||
)
|
||||
|
||||
# Check that there is no "cache mixup" even across multiple runs
|
||||
qa = QuotaAvailability(count_waitinglist=False)
|
||||
qa.queue(self.quota)
|
||||
qa.compute()
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 1)
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=True)
|
||||
qa.queue(self.quota)
|
||||
qa.compute(allow_cache=True)
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_ORDERED, 0)
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=True)
|
||||
qa.queue(self.quota)
|
||||
qa.compute(allow_cache=True)
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_ORDERED, 0)
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=False)
|
||||
qa.queue(self.quota)
|
||||
qa.compute()
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 1)
|
||||
|
||||
# Rebuild cache required
|
||||
self.quota.size = 5
|
||||
self.quota.save()
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=True)
|
||||
qa.queue(self.quota)
|
||||
qa.compute(allow_cache=True)
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_ORDERED, 0)
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=False)
|
||||
qa.queue(self.quota)
|
||||
qa.compute(allow_cache=True)
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 1)
|
||||
|
||||
self.quota.rebuild_cache()
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=True)
|
||||
qa.queue(self.quota)
|
||||
qa.compute(allow_cache=True)
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 4)
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=False)
|
||||
qa.queue(self.quota)
|
||||
qa.compute(allow_cache=True)
|
||||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 5)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_waitinglist_variation_fulfilled(self):
|
||||
self.quota.variations.add(self.var1)
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
from django.utils import translation
|
||||
from django_scopes import scopes_disabled
|
||||
from fakeredis import FakeConnection
|
||||
from xdist.dsession import DSession
|
||||
|
||||
from pretix.testutils.mock import get_redis_connection
|
||||
|
||||
CRASHED_ITEMS = set()
|
||||
|
||||
|
||||
@@ -74,3 +78,38 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_locale():
|
||||
translation.activate("en")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fakeredis_client(monkeypatch):
|
||||
with override_settings(
|
||||
HAS_REDIS=True,
|
||||
REAL_CACHE_USED=True,
|
||||
CACHES={
|
||||
'redis': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': 'redis://127.0.0.1',
|
||||
'OPTIONS': {
|
||||
'connection_class': FakeConnection
|
||||
}
|
||||
},
|
||||
'redis_session': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': 'redis://127.0.0.1',
|
||||
'OPTIONS': {
|
||||
'connection_class': FakeConnection
|
||||
}
|
||||
},
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': 'redis://127.0.0.1',
|
||||
'OPTIONS': {
|
||||
'connection_class': FakeConnection
|
||||
}
|
||||
},
|
||||
}
|
||||
):
|
||||
redis = get_redis_connection("default", True)
|
||||
redis.flushall()
|
||||
monkeypatch.setattr('django_redis.get_redis_connection', get_redis_connection, raising=False)
|
||||
yield redis
|
||||
|
||||
@@ -640,7 +640,10 @@ class ItemsTest(ItemFormTest):
|
||||
prop = self.event1.item_meta_properties.create(name="Foo")
|
||||
self.item2.meta_values.create(property=prop, value="Bar")
|
||||
|
||||
doc = self.get_doc('/control/event/%s/%s/items/add?copy_from=%d' % (self.orga1.slug, self.event1.slug, self.item2.pk))
|
||||
data = extract_form_fields(doc.select("form")[0])
|
||||
self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
|
||||
**data,
|
||||
'name_0': 'Intermediate',
|
||||
'default_price': '23.00',
|
||||
'tax_rate': '19.00',
|
||||
|
||||
Reference in New Issue
Block a user