mirror of
https://github.com/pretix/pretix.git
synced 2026-07-31 09:15:08 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4873300710 | ||
|
|
95916fbce2 | ||
|
|
0383810895 | ||
|
|
5efef8c9a5 |
Generated
+3
-3
@@ -2584,9 +2584,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
+4
-4
@@ -52,10 +52,10 @@ dependencies = [
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.7.*",
|
||||
"django-phonenumber-field==8.5.*",
|
||||
"django-phonenumber-field==8.4.*",
|
||||
"django-querytagger==0.0.3",
|
||||
"django-redis==7.0.*",
|
||||
"django-scopes==2.1.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.7.*",
|
||||
"djangorestframework==3.17.*",
|
||||
"dnspython==2.8.*",
|
||||
@@ -98,7 +98,7 @@ dependencies = [
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2026072401",
|
||||
"tlds>=2026041800",
|
||||
"tqdm==4.*",
|
||||
"ua-parser==1.0.*",
|
||||
"vobject==0.9.*",
|
||||
@@ -112,7 +112,7 @@ dev = [
|
||||
"aiohttp==3.14.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.37.*",
|
||||
"fakeredis==2.36.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==8.0.*",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -51,18 +52,10 @@ class BaseSecurityProfile:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
"""
|
||||
Priority for ordering, higher will come first.
|
||||
"""
|
||||
return 100
|
||||
|
||||
|
||||
class FullAccessSecurityProfile(BaseSecurityProfile):
|
||||
identifier = 'full'
|
||||
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
||||
priority = 1000
|
||||
|
||||
def is_allowed(self, request):
|
||||
return True
|
||||
@@ -202,15 +195,13 @@ def get_all_security_profiles():
|
||||
if _ALL_PROFILES:
|
||||
return _ALL_PROFILES
|
||||
|
||||
types = []
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_device_security_profile.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types.append(r)
|
||||
types[r.identifier] = r
|
||||
else:
|
||||
types.append(ret)
|
||||
types.sort(key=lambda el: el.priority, reverse=True)
|
||||
types = {r.identifier: r for r in types}
|
||||
types[ret.identifier] = ret
|
||||
_ALL_PROFILES = types
|
||||
return types
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ from rest_framework.exceptions import (
|
||||
NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.mixins import UpdateModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
@@ -712,7 +711,7 @@ class SeatFilter(FilterSet):
|
||||
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
||||
|
||||
|
||||
class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SeatSerializer
|
||||
queryset = Seat.objects.none()
|
||||
write_permission = 'event.settings.general:write'
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# 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 ipaddress
|
||||
import logging
|
||||
import smtplib
|
||||
import socket
|
||||
@@ -42,7 +43,6 @@ from pretix.base.templatetags.rich_text import (
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
from pretix.helpers.ssrf import should_block_access
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -57,6 +57,8 @@ logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
T = TypeVar("T", bound=EmailBackend)
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||
try:
|
||||
@@ -252,9 +254,16 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||
is_private, msg = should_block_access(sa)
|
||||
if is_private:
|
||||
raise socket.error(msg)
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
if ip_addr.is_multicast:
|
||||
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
raise socket.error(f"Request to local address {sa[0]} blocked")
|
||||
if ip_addr.is_private:
|
||||
raise socket.error(f"Request to private address {sa[0]} blocked")
|
||||
if check_ip4 in _cgnat_net:
|
||||
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
|
||||
@@ -40,12 +40,11 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
||||
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from ..exporter import MultiSheetListExporter, OrganizerLevelExportMixin
|
||||
from ..models import Membership
|
||||
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
||||
from ..signals import register_multievent_data_exporters
|
||||
|
||||
|
||||
class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
|
||||
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'customerlist'
|
||||
verbose_name = gettext_lazy('Customer accounts')
|
||||
category = pgettext_lazy('export_category', 'Customer accounts')
|
||||
@@ -55,20 +54,13 @@ class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
|
||||
def get_required_organizer_permission(cls) -> str:
|
||||
return 'organizer.customers:write'
|
||||
|
||||
@property
|
||||
def sheets(self):
|
||||
return (
|
||||
('customers', _('Customers')),
|
||||
('memberships', _('Memberships')),
|
||||
)
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
return OrderedDict(
|
||||
[]
|
||||
)
|
||||
|
||||
def iterate_customers(self, form_data):
|
||||
def iterate_list(self, form_data):
|
||||
qs = self.organizer.customers.prefetch_related('provider')
|
||||
|
||||
headers = [
|
||||
@@ -117,52 +109,6 @@ class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
|
||||
]
|
||||
yield row
|
||||
|
||||
def iterate_memberships(self, form_data):
|
||||
qs = Membership.objects.filter(
|
||||
customer__organizer=self.organizer
|
||||
).prefetch_related('membership_type').select_related('customer', 'granted_in', 'granted_in__order')
|
||||
|
||||
headers = [
|
||||
_('Customer ID'),
|
||||
_('External identifier'),
|
||||
_('Email'),
|
||||
_('Test mode'),
|
||||
_('Canceled'),
|
||||
_('Membership type'),
|
||||
_('Purchase ticket'),
|
||||
_('Start date'),
|
||||
_('Start time'),
|
||||
_('End date'),
|
||||
_('End time'),
|
||||
_('Name'),
|
||||
]
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
|
||||
if name_scheme and len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(_('Name') + ': ' + str(label))
|
||||
yield headers
|
||||
|
||||
tz = get_current_timezone()
|
||||
for obj in qs:
|
||||
row = [
|
||||
obj.customer.identifier,
|
||||
obj.customer.external_identifier,
|
||||
obj.customer.email or '',
|
||||
_('Yes') if obj.testmode else _('No'),
|
||||
_('Yes') if obj.canceled else _('No'),
|
||||
str(obj.membership_type.name),
|
||||
f'{obj.granted_in.order.code}-{obj.granted_in.positionid}' if obj.granted_in else None,
|
||||
obj.date_start.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
obj.date_start.astimezone(tz).strftime('%H:%M'),
|
||||
obj.date_end.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
obj.date_end.astimezone(tz).strftime('%H:%M'),
|
||||
obj.attendee_name or '',
|
||||
]
|
||||
if name_scheme and len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
row.append(obj.attendee_name_parts.get(k, ''))
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_customers'.format(self.organizer.slug)
|
||||
|
||||
|
||||
@@ -1121,7 +1121,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
for q in question_cache.values():
|
||||
if q.required and q.type == Question.TYPE_BOOLEAN:
|
||||
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
|
||||
d['question_%d' % q.pk] = None
|
||||
del d['question_%d' % q.pk]
|
||||
|
||||
return d
|
||||
|
||||
|
||||
@@ -497,16 +497,8 @@ class RejectInvalidInputMiddleware(MiddlewareMixin):
|
||||
if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']:
|
||||
raise BadRequest("Invalid characters in input.")
|
||||
if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded":
|
||||
try:
|
||||
post_data = request.POST.lists()
|
||||
except BadRequest:
|
||||
# Reading request.POST wasn't possible, probably an invalid charset. Django will crash once we actually
|
||||
# use request.POST, but if we don't, let's not crash it (required for some weird payment provider
|
||||
# webhooks, e.g. computop).
|
||||
pass
|
||||
else:
|
||||
if any("\x00" in value for key, value_list in post_data for value in value_list):
|
||||
raise BadRequest("Invalid characters in input.")
|
||||
if any("\x00" in value for key, value_list in request.POST.lists() for value in value_list):
|
||||
raise BadRequest("Invalid characters in input.")
|
||||
|
||||
|
||||
class CustomCommonMiddleware(CommonMiddleware):
|
||||
|
||||
@@ -238,9 +238,6 @@ class EventMixin:
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
# If we get rid of the shim, verify that
|
||||
# https://github.com/py-vobject/vobject/issues/117#issuecomment-5045645314
|
||||
# has been released and included
|
||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||
|
||||
@property
|
||||
|
||||
@@ -508,20 +508,20 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
@classmethod
|
||||
def annotate_overpayments(cls, qs, results=True, refunds=True, sums=False):
|
||||
payment_sum = OrderPayment.objects.with_scopes_disabled().filter(
|
||||
payment_sum = OrderPayment.objects.filter(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(s=Sum('amount')).values('s')
|
||||
refund_sum = OrderRefund.objects.with_scopes_disabled().filter(
|
||||
refund_sum = OrderRefund.objects.filter(
|
||||
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_CREATED),
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(s=Sum('amount')).values('s')
|
||||
external_refund = OrderRefund.objects.with_scopes_disabled().filter(
|
||||
external_refund = OrderRefund.objects.filter(
|
||||
state=OrderRefund.REFUND_STATE_EXTERNAL,
|
||||
order=OuterRef('pk')
|
||||
)
|
||||
pending_refund = OrderRefund.objects.with_scopes_disabled().filter(
|
||||
pending_refund = OrderRefund.objects.filter(
|
||||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
|
||||
order=OuterRef('pk')
|
||||
)
|
||||
@@ -2072,17 +2072,6 @@ class OrderPayment(models.Model):
|
||||
"""
|
||||
return '{}-P-{}'.format(self.order.code, self.local_id)
|
||||
|
||||
@property
|
||||
def global_id(self):
|
||||
"""
|
||||
The global ID of this payment, constructed by the organizer slug, event slug, and the full id.
|
||||
"""
|
||||
return "{organizer}-{event}-{full_id}".format(
|
||||
organizer=self.order.organizer.slug.upper(),
|
||||
event=self.order.event.slug.upper(),
|
||||
full_id=self.full_id,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.local_id:
|
||||
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
||||
@@ -2283,17 +2272,6 @@ class OrderRefund(models.Model):
|
||||
"""
|
||||
return '{}-R-{}'.format(self.order.code, self.local_id)
|
||||
|
||||
@property
|
||||
def global_id(self):
|
||||
"""
|
||||
The global ID of this refund, constructed by the organizer slug, event slug, and the full id.
|
||||
"""
|
||||
return "{organizer}-{event}-{full_id}".format(
|
||||
organizer=self.order.organizer.slug.upper(),
|
||||
event=self.order.event.slug.upper(),
|
||||
full_id=self.full_id,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.local_id:
|
||||
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
||||
@@ -2308,12 +2286,9 @@ class OrderRefund(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def ActivePositionManager(**scope):
|
||||
class InnerClass(ScopedManager(**scope).__class__):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(canceled=False)
|
||||
|
||||
return InnerClass()
|
||||
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(canceled=False)
|
||||
|
||||
|
||||
class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
@@ -2403,7 +2378,7 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
canceled = models.BooleanField(default=False)
|
||||
|
||||
all = ScopedManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
@@ -2622,7 +2597,7 @@ class OrderPosition(AbstractPosition):
|
||||
)
|
||||
|
||||
all = ScopedManager(organizer='organizer')
|
||||
objects = ActivePositionManager(organizer='organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -39,7 +39,6 @@ from urllib.parse import urlencode
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
|
||||
)
|
||||
@@ -60,7 +59,7 @@ from pretix.base.models import (
|
||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
|
||||
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
|
||||
User, Voucher,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.timeframes import (
|
||||
@@ -3008,91 +3007,3 @@ class OutgoingMailFilterForm(FilterForm):
|
||||
qs = qs.order_by("-created", "-pk")
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class LogFilterForm(FilterForm):
|
||||
source = forms.ChoiceField(
|
||||
label=_('Source'),
|
||||
choices=[
|
||||
('', _('All sources')),
|
||||
('team', _('Team actions')),
|
||||
('customer', _('Customer actions')),
|
||||
('device', _('Device actions')),
|
||||
],
|
||||
required=False
|
||||
)
|
||||
device = SafeModelChoiceField(
|
||||
label=_('Device'),
|
||||
empty_label=_('All devices'),
|
||||
queryset=Device.objects.none(),
|
||||
required=False
|
||||
)
|
||||
user_email = forms.EmailField(
|
||||
label=_('User email'),
|
||||
widget=forms.EmailInput(
|
||||
attrs={"placeholder": _('All users')}
|
||||
),
|
||||
required=False
|
||||
)
|
||||
action_type = forms.CharField(
|
||||
widget=forms.HiddenInput,
|
||||
required=False,
|
||||
)
|
||||
content_type = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
widget=forms.HiddenInput,
|
||||
required=False,
|
||||
)
|
||||
object = forms.IntegerField(
|
||||
widget=forms.HiddenInput,
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['device'].queryset = self.organizer.devices.all().order_by('device_id')
|
||||
self.fields['device'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.devices.select2', kwargs={
|
||||
'organizer': self.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('All devices'),
|
||||
}
|
||||
)
|
||||
self.fields['device'].widget.choices = self.fields['device'].choices
|
||||
self.fields['device'].label = _('Device')
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
if fdata.get('source') == 'team':
|
||||
qs = qs.filter(user__isnull=False)
|
||||
elif fdata.get('source') == 'device':
|
||||
qs = qs.filter(device__isnull=False)
|
||||
elif fdata.get('source') == 'customer':
|
||||
qs = qs.filter(user__isnull=True, device__isnull=True)
|
||||
|
||||
if fdata.get('device'):
|
||||
qs = qs.filter(device_id=fdata['device'].pk)
|
||||
|
||||
if fdata.get('action_type'):
|
||||
qs = qs.filter(action_type__in=fdata['action_type'].split(','))
|
||||
|
||||
if fdata.get('user_email'):
|
||||
try:
|
||||
user = User.objects.get(email=fdata['user_email'].lower())
|
||||
qs = qs.filter(user=user)
|
||||
except User.DoesNotExist:
|
||||
# Do not actively leak info that this user does not exist. Yes, this will likely have a
|
||||
# timing attack possibility, but with the magic that database query planners do,
|
||||
# it's probably impossible to avoid that.
|
||||
qs = qs.none()
|
||||
|
||||
if fdata.get('content_type'):
|
||||
qs = qs.filter(content_type=fdata.get('content_type'))
|
||||
|
||||
if fdata.get('object'):
|
||||
qs = qs.filter(object_id=fdata.get('object'))
|
||||
return qs
|
||||
|
||||
@@ -562,7 +562,9 @@ class OrderPositionChangeForm(forms.Form):
|
||||
if instance.addon_to_id:
|
||||
del self.fields['operation_split']
|
||||
|
||||
if not instance.seat and not instance._seat_allowed:
|
||||
if not instance.seat and not (
|
||||
instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists()
|
||||
):
|
||||
del self.fields['seat']
|
||||
|
||||
choices = [
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import functools
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
@@ -70,15 +69,6 @@ OVERVIEW_BANLIST = [
|
||||
]
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=32)
|
||||
def object_id_to_string(model_class, **kwargs):
|
||||
# The cache is thread-local and persists requests, but it's small enough that we can accept that
|
||||
try:
|
||||
return str(model_class.objects.get(**kwargs))
|
||||
except model_class.DoesNotExist:
|
||||
return "?"
|
||||
|
||||
|
||||
class OrderChangeLogEntryType(OrderLogEntryType):
|
||||
prefix = _('The order has been changed:')
|
||||
|
||||
@@ -103,10 +93,10 @@ class OrderItemChanged(OrderChangeLogEntryType):
|
||||
def display_prefixed(self, event: Event, logentry: LogEntry, data):
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(object_id_to_string(ItemVariation, item__event_id=event.pk, pk=data['old_variation']))
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
|
||||
new_item = str(event.items.get(pk=data['new_item']))
|
||||
if data['new_variation']:
|
||||
new_item += ' - ' + str(object_id_to_string(ItemVariation, item__event_id=event.pk, pk=data['new_variation']))
|
||||
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
|
||||
return _('Position #{posid}: {old_item} ({old_price}) changed to {new_item} ({new_price}).').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_item=old_item, new_item=new_item,
|
||||
@@ -163,14 +153,14 @@ class OrderTaxRuleChanged(OrderChangeLogEntryType):
|
||||
if 'positionid' in data:
|
||||
return _('Tax rule of position #{posid} changed from {old_rule} to {new_rule}.').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_rule=object_id_to_string(TaxRule, pk=data['old_taxrule']) if data['old_taxrule'] else '–',
|
||||
new_rule=object_id_to_string(TaxRule, pk=data['new_taxrule']),
|
||||
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '–',
|
||||
new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
|
||||
)
|
||||
elif 'fee' in data:
|
||||
return _('Tax rule of fee #{fee} changed from {old_rule} to {new_rule}.').format(
|
||||
fee=data.get('fee', '?'),
|
||||
old_rule=object_id_to_string(TaxRule, pk=data['old_taxrule']) if data['old_taxrule'] else '–',
|
||||
new_rule=object_id_to_string(TaxRule, pk=data['new_taxrule']),
|
||||
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '–',
|
||||
new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
|
||||
)
|
||||
|
||||
|
||||
@@ -214,7 +204,7 @@ class OrderCanceled(OrderChangeLogEntryType):
|
||||
def display_prefixed(self, event: Event, logentry: LogEntry, data):
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + object_id_to_string(ItemVariation, pk=data['old_variation'])
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
|
||||
return _('Position #{posid} ({old_item}, {old_price}) canceled.').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_item=old_item,
|
||||
@@ -229,7 +219,7 @@ class OrderPositionAdded(OrderChangeLogEntryType):
|
||||
def display_prefixed(self, event: Event, logentry: LogEntry, data):
|
||||
item = str(event.items.get(pk=data['item']))
|
||||
if data['variation']:
|
||||
item += ' - ' + object_id_to_string(ItemVariation, item__event_id=event.pk, pk=data['variation'])
|
||||
item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation']))
|
||||
if data['addon_to']:
|
||||
addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to'])
|
||||
return _('Position #{posid} created: {item} ({price}) as an add-on to position #{addon_to}.').format(
|
||||
@@ -293,7 +283,7 @@ class OrderChangedSplit(OrderChangeLogEntryType):
|
||||
def display_prefixed(self, event: Event, logentry: LogEntry, data):
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + object_id_to_string(ItemVariation, pk=data['old_variation'])
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
|
||||
url = reverse('control:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
@@ -349,7 +339,6 @@ class OrderChangedSplitFrom(OrderLogEntryType):
|
||||
'pretix.event.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
|
||||
})
|
||||
class CheckinErrorLogEntryType(OrderLogEntryType):
|
||||
|
||||
def display(self, logentry: LogEntry, data):
|
||||
return self.display_plain(self.plain, logentry, data)
|
||||
|
||||
@@ -364,7 +353,10 @@ class CheckinErrorLogEntryType(OrderLogEntryType):
|
||||
event = logentry.event
|
||||
|
||||
if 'list' in data and event:
|
||||
data['list'] = object_id_to_string(CheckinList, event_id=event.id, pk=data['list'])
|
||||
try:
|
||||
data['list'] = event.checkin_lists.get(pk=data.get('list')).name
|
||||
except CheckinList.DoesNotExist:
|
||||
data['list'] = _("(unknown)")
|
||||
else:
|
||||
data['list'] = _("(unknown)")
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% if has_overpaid_orders %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
|
||||
You should review the cases and consider refunding the overpaid amount to the user.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
|
||||
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_refunds %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending refunds</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_cancellation_requests %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>requested cancellations</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
|
||||
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_approvals %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending approvals</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
|
||||
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_orders_with_full_payment %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
|
||||
because no quota was left at the time their payment arrived. You should review the cases and consider
|
||||
either refunding the customer or creating more space.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
|
||||
class="btn btn-primary">{% trans "Show affected orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_sync_problems %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
Orders in this event could not be <strong>synced to an external system</strong> as configured.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.datasync.failedjobs" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary">{% trans "Show sync problems" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -14,7 +14,6 @@
|
||||
{% else %}
|
||||
{{ request.event.get_date_range_display }}
|
||||
{% endif %}
|
||||
<span id="warnings_loading" class="fa fa-cog fa-spin"></span>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="helper-space-below">
|
||||
@@ -32,7 +31,63 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div id="warnings_target"></div>
|
||||
{% if has_overpaid_orders %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
|
||||
You should review the cases and consider refunding the overpaid amount to the user.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
|
||||
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_refunds %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending refunds</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_cancellation_requests %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>requested cancellations</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
|
||||
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_approvals %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending approvals</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
|
||||
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_orders_with_full_payment %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
|
||||
because no quota was left at the time their payment arrived. You should review the cases and consider
|
||||
either refunding the customer or creating more space.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
|
||||
class="btn btn-primary">{% trans "Show affected orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_sync_problems %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
Orders in this event could not be <strong>synced to an external system</strong> as configured.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.datasync.failedjobs" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary">{% trans "Show sync problems" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% eventsignal request.event "pretix.control.signals.event_dashboard_top" request=request %}
|
||||
|
||||
{% if request.event.has_subevents %}
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
{% block title %}{% trans "Event logs" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Event logs" %}</h1>
|
||||
{% include "pretixcontrol/fragment_log_filter_form.html" %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
||||
<p>
|
||||
<select name="user" class="form-control">
|
||||
<option value="">{% trans "All actions" %}</option>
|
||||
<option value="yes" {% if request.GET.user == "yes" %}selected="selected"{% endif %}>
|
||||
{% trans "Team actions" %}
|
||||
</option>
|
||||
<option value="no" {% if request.GET.user == "no" %}selected="selected"{% endif %}>
|
||||
{% trans "Customer actions" %}
|
||||
</option>
|
||||
{% for up in userlist %}
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}"
|
||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for d in devicelist %}
|
||||
{% if d.device__id %}
|
||||
<option value="d-{{ d.device__id }}"
|
||||
{% if "d-" in request.GET.user and request.GET.user|slice:"2:" == d.device__id|slugify %}selected="selected"{% endif %}>
|
||||
{{ d.device__name }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
<li class="list-group-item logentry">
|
||||
@@ -66,5 +95,5 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "pretixcontrol/pagination_huge.html" %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Filter" %}
|
||||
</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.source %}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.device %}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.user_email %}
|
||||
</div>
|
||||
</div>
|
||||
{{ filter_form.action_type }}
|
||||
{{ filter_form.content_type }}
|
||||
{{ filter_form.object }}
|
||||
{% if filter_form.is_valid and filter_form.cleaned_data.action_type %}
|
||||
{% icon "filter" %} <code>{{ filter_form.cleaned_data.action_type }}</code>
|
||||
{% endif %}
|
||||
{% if filter_form.is_valid and filter_form.cleaned_data.content_type and filter_form.cleaned_data.object %}
|
||||
{% icon "filter" %} {% trans "Specific object selected" %}
|
||||
{% endif %}
|
||||
<div class="text-right flip">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -833,7 +833,7 @@
|
||||
<strong>{% trans "Pending total" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
|
||||
<strong>{{ pending_sum|money:event.currency }}</strong>
|
||||
<strong>{{ order.pending_sum|money:event.currency }}</strong>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,24 @@
|
||||
{% block title %}{% trans "Organizer logs" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Organizer logs" %}</h1>
|
||||
{% include "pretixcontrol/fragment_log_filter_form.html" %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
||||
<p>
|
||||
<select name="user" class="form-control">
|
||||
<option value="">{% trans "All actions" %}</option>
|
||||
{% for up in userlist %}
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}"
|
||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
<li class="list-group-item logentry">
|
||||
@@ -64,5 +81,5 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "pretixcontrol/pagination_huge.html" %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if e.voucher %}
|
||||
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}?next={{ request.get_full_path|urlencode }}">
|
||||
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}">
|
||||
{{ e.voucher }}
|
||||
</a>
|
||||
{% elif not e.voucher and e.availability.0 == 100 and e.availability.1|default_if_none:"none" != "none" %}
|
||||
|
||||
@@ -273,8 +273,7 @@ urlpatterns = [
|
||||
re_path(r'^$', dashboards.event_index, name='event.index'),
|
||||
re_path(r'^qrcode.(?P<filetype>(png|jpeg|gif|svg))$', event.EventQRCode.as_view(), name='event.qrcode'),
|
||||
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
|
||||
re_path(r'^dashboard/partials/logs$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
||||
re_path(r'^dashboard/partials/warnings$', dashboards.event_index_warnings_lazy, name='event.index.warnings'),
|
||||
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
||||
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
|
||||
re_path(r'^transfer_session/$', event.EventTransferSession.as_view(), name='event.transfer_session'),
|
||||
re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'),
|
||||
|
||||
@@ -484,17 +484,10 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Checkin.all.filter(
|
||||
list_id__in=self.request.event.checkin_lists.values_list('id', flat=True),
|
||||
list__event=self.request.event,
|
||||
).select_related(
|
||||
'position', 'position__order', 'position__item', 'position__variation', 'position__subevent'
|
||||
).prefetch_related(
|
||||
# For events with huge numbers of check-ins, prefetch_related is a lot more efficient than
|
||||
# select_related, since it just needs to query one page size of positions instead of joining
|
||||
# multiple huge tables during result computation
|
||||
Prefetch(
|
||||
'position',
|
||||
queryset=OrderPosition.all.select_related(
|
||||
'order', 'item', 'variation', 'subevent',
|
||||
)
|
||||
),
|
||||
'list', 'gate', 'device'
|
||||
)
|
||||
if self.filter_form.is_valid():
|
||||
|
||||
@@ -379,42 +379,6 @@ def event_index(request, organizer, event):
|
||||
'comment_form': CommentForm(initial={'comment': request.event.comment}, readonly=not can_change_event_settings),
|
||||
}
|
||||
|
||||
ctx['timeline'] = [
|
||||
{
|
||||
'date': t.datetime.astimezone(request.event.timezone).date(),
|
||||
'entry': t,
|
||||
'time': t.datetime.astimezone(request.event.timezone)
|
||||
}
|
||||
for t in timeline_for_event(request.event, subevent)
|
||||
]
|
||||
ctx['today'] = now().astimezone(request.event.timezone).date()
|
||||
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
|
||||
resp = render(request, 'pretixcontrol/event/index.html', ctx)
|
||||
return resp
|
||||
|
||||
|
||||
def event_index_widgets_lazy(request, organizer, event):
|
||||
subevent = None
|
||||
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
|
||||
i = request.GET.get("subevent", "")
|
||||
try:
|
||||
subevent = request.event.subevents.get(pk=i)
|
||||
except SubEvent.DoesNotExist:
|
||||
pass
|
||||
|
||||
widgets = []
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
||||
widgets.extend(result)
|
||||
|
||||
return build_json_response(widgets)
|
||||
|
||||
|
||||
def event_index_warnings_lazy(request, organizer, event):
|
||||
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
|
||||
request=request)
|
||||
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
|
||||
'event.settings.general:write', request=request)
|
||||
ctx = {}
|
||||
ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(request.event.orders).filter(
|
||||
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
|
||||
@@ -438,11 +402,35 @@ def event_index_warnings_lazy(request, organizer, event):
|
||||
| Q(failed_attempts__gt=0)
|
||||
).exists()
|
||||
|
||||
return render(
|
||||
request,
|
||||
'pretixcontrol/event/dashboard_partial_warnings.html',
|
||||
ctx
|
||||
)
|
||||
ctx['timeline'] = [
|
||||
{
|
||||
'date': t.datetime.astimezone(request.event.timezone).date(),
|
||||
'entry': t,
|
||||
'time': t.datetime.astimezone(request.event.timezone)
|
||||
}
|
||||
for t in timeline_for_event(request.event, subevent)
|
||||
]
|
||||
ctx['today'] = now().astimezone(request.event.timezone).date()
|
||||
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
|
||||
resp = render(request, 'pretixcontrol/event/index.html', ctx)
|
||||
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
|
||||
return resp
|
||||
|
||||
|
||||
def event_index_widgets_lazy(request, organizer, event):
|
||||
subevent = None
|
||||
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
|
||||
i = request.GET.get("subevent", "")
|
||||
try:
|
||||
subevent = request.event.subevents.get(pk=i)
|
||||
except SubEvent.DoesNotExist:
|
||||
pass
|
||||
|
||||
widgets = []
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
||||
widgets.extend(result)
|
||||
|
||||
return build_json_response(widgets)
|
||||
|
||||
|
||||
def event_index_log_lazy(request, organizer, event):
|
||||
@@ -477,7 +465,7 @@ def event_index_log_lazy(request, organizer, event):
|
||||
|
||||
return render(
|
||||
request,
|
||||
'pretixcontrol/event/dashboard_partial_logs.html',
|
||||
'pretixcontrol/event/logs_embed.html',
|
||||
{
|
||||
'logs': qs[:5]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import conditional_escape, format_html
|
||||
@@ -119,9 +119,8 @@ from ...helpers.compat import CompatDeleteView
|
||||
from ...helpers.format import (
|
||||
PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from ..forms.filter import LogFilterForm
|
||||
from ..logdisplay import OVERVIEW_BANLIST
|
||||
from . import CreateView, LargeResultSetPaginator, PaginationMixin, UpdateView
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1254,7 +1253,6 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
||||
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/event/logs.html'
|
||||
model = LogEntry
|
||||
paginator_class = LargeResultSetPaginator
|
||||
context_object_name = 'logs'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -1284,20 +1282,32 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
]
|
||||
qs = qs.filter(content_type__in=allowed_types)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
if self.request.GET.get('user') == 'yes':
|
||||
qs = qs.filter(user__isnull=False)
|
||||
elif self.request.GET.get('user') == 'no':
|
||||
qs = qs.filter(user__isnull=True)
|
||||
elif self.request.GET.get('user', '').startswith('d-'):
|
||||
qs = qs.filter(device_id=self.request.GET.get('user')[2:])
|
||||
elif self.request.GET.get('user'):
|
||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||
|
||||
if self.request.GET.get('action_type'):
|
||||
qs = qs.filter(action_type=self.request.GET['action_type'])
|
||||
|
||||
if self.request.GET.get('content_type'):
|
||||
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
|
||||
|
||||
if self.request.GET.get('object'):
|
||||
qs = qs.filter(object_id=self.request.GET.get('object'))
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
|
||||
ctx['devicelist'] = self.request.event.logentry_set.order_by('device__name').distinct().values('device__id', 'device__name')
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
|
||||
|
||||
|
||||
class EventComment(EventPermissionRequiredMixin, View):
|
||||
permission = 'event.settings.general:write'
|
||||
@@ -1568,7 +1578,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
|
||||
return ctx
|
||||
|
||||
|
||||
class QuickSetupView(EventPermissionRequiredMixin, FormView):
|
||||
class QuickSetupView(FormView):
|
||||
template_name = 'pretixcontrol/event/quick_setup.html'
|
||||
permission = 'event.settings.general:write'
|
||||
form_class = QuickSetupForm
|
||||
|
||||
@@ -81,7 +81,7 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, CachedTicket, Checkin, GiftCard, Invoice, InvoiceAddress, Item,
|
||||
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
|
||||
ScheduledEventExport, SeatCategoryMapping, generate_secret,
|
||||
ScheduledEventExport, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
@@ -564,11 +564,10 @@ class OrderDetail(OrderView):
|
||||
})
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
|
||||
|
||||
pending_sum = self.order.pending_sum
|
||||
ctx['overpaid'] = pending_sum * -1
|
||||
ctx['overpaid'] = self.order.pending_sum * -1
|
||||
ctx['download_buttons'] = self.download_buttons
|
||||
ctx['payment_refund_sum'] = self.order.payment_refund_sum
|
||||
ctx['pending_sum'] = pending_sum
|
||||
ctx['pending_sum'] = self.order.pending_sum
|
||||
ctx['uncancelled_invoice'] = self.order.invoices.exclude(
|
||||
Exists(self.order.invoices.filter(refers=OuterRef('pk'), is_cancellation=True))
|
||||
).exclude(is_cancellation=True).first()
|
||||
@@ -602,9 +601,8 @@ class OrderDetail(OrderView):
|
||||
).prefetch_related(
|
||||
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
Prefetch('all_checkins', queryset=Checkin.all.select_related('list', 'gate').order_by('datetime')),
|
||||
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device').order_by('datetime')),
|
||||
Prefetch('subevent', queryset=self.request.event.subevents.all()),
|
||||
).order_by('positionid')
|
||||
|
||||
positions = []
|
||||
@@ -1987,37 +1985,20 @@ class OrderChange(OrderView):
|
||||
def fees(self):
|
||||
fees = list(self.order.fees.all())
|
||||
for f in fees:
|
||||
f.form = OrderFeeChangeForm(
|
||||
prefix='of-{}'.format(f.pk),
|
||||
instance=f,
|
||||
data=self.request.POST if self.request.method == "POST" else None
|
||||
)
|
||||
f.form = OrderFeeChangeForm(prefix='of-{}'.format(f.pk), instance=f,
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
return fees
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
positions = list(self.order.positions.select_related(
|
||||
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
|
||||
'seat',
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
'subevent',
|
||||
queryset=self.request.event.subevents.all(),
|
||||
),
|
||||
'granted_memberships',
|
||||
'issued_gift_cards',
|
||||
'addons',
|
||||
).annotate(
|
||||
_seat_allowed=Exists(SeatCategoryMapping.objects.filter(subevent=OuterRef("subevent"), product=OuterRef("item")))
|
||||
))
|
||||
'seat', 'subevent',
|
||||
).prefetch_related('granted_memberships', 'issued_gift_cards'))
|
||||
for p in positions:
|
||||
p.form = OrderPositionChangeForm(
|
||||
prefix='op-{}'.format(p.pk),
|
||||
instance=p,
|
||||
items=self.items,
|
||||
initial={'seat': p.seat.seat_guid if p.seat else None},
|
||||
data=self.request.POST if self.request.method == "POST" else None
|
||||
)
|
||||
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
|
||||
initial={'seat': p.seat.seat_guid if p.seat else None},
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
return positions
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -113,8 +113,7 @@ from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
||||
from pretix.control.forms.filter import (
|
||||
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
|
||||
LogFilterForm, OrganizerFilterForm, ReusableMediaFilterForm,
|
||||
TeamFilterForm,
|
||||
OrganizerFilterForm, ReusableMediaFilterForm, TeamFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import ExporterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
@@ -134,7 +133,7 @@ from pretix.control.permissions import (
|
||||
organizer_permission_required,
|
||||
)
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||
from pretix.helpers import OF_SELF, GroupConcat
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
@@ -2664,7 +2663,6 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/organizers/logs.html'
|
||||
permission = 'organizer.settings.general:write'
|
||||
model = LogEntry
|
||||
paginator_class = LargeResultSetPaginator
|
||||
context_object_name = 'logs'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -2674,21 +2672,16 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||
).order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if self.request.GET.get('action_type'):
|
||||
qs = qs.filter(action_type=self.request.GET['action_type'])
|
||||
if self.request.GET.get('user'):
|
||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
|
||||
|
||||
|
||||
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = MembershipType
|
||||
|
||||
@@ -46,7 +46,6 @@ from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import BadRequest, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -96,20 +95,6 @@ class RecentAuthenticationRequiredMixin:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
||||
if tdelta > self.max_time:
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
# It's not useful to return a 302 redirect on a XMLHttpRequest request, because
|
||||
# the XMLHttpRequest is unable to detect redirects.
|
||||
return HttpResponse(
|
||||
"Authentication required",
|
||||
status=401,
|
||||
headers={
|
||||
# Appending ?next= is handled by client, because it should be the top-level context url,
|
||||
# not the URL called in the background
|
||||
"X-Login-Url": reverse('control:user.reauth')
|
||||
}
|
||||
)
|
||||
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -351,8 +350,6 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
||||
return self.request.GET.get("next")
|
||||
return reverse('control:event.vouchers', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# 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 ipaddress
|
||||
import socket
|
||||
import sys
|
||||
import types
|
||||
@@ -41,7 +42,8 @@ from urllib3.util.connection import (
|
||||
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
||||
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
from pretix.helpers.ssrf import should_block_access
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def monkeypatch_vobject_performance():
|
||||
@@ -148,9 +150,16 @@ def monkeypatch_urllib3_ssrf_protection():
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
is_private, msg = should_block_access(sa)
|
||||
if is_private:
|
||||
raise HTTPError(msg)
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
if ip_addr.is_multicast:
|
||||
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
||||
if ip_addr.is_private:
|
||||
raise HTTPError(f"Request to private address {sa[0]} blocked")
|
||||
if check_ip4 in _cgnat_net:
|
||||
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
|
||||
@@ -28,7 +28,7 @@ from django.contrib.auth import login as auth_login
|
||||
from django.contrib.gis import geoip2
|
||||
from django.core.cache import cache
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import localtime, now, override
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_countries.fields import Country
|
||||
from geoip2.errors import AddressNotFoundError
|
||||
@@ -167,13 +167,13 @@ def handle_login_source(user, request):
|
||||
})
|
||||
if user.known_login_sources.count() > 1:
|
||||
# Do not send on first login or first login after introduction of this feature:
|
||||
with language(user.locale), override(user.timezone):
|
||||
with language(user.locale):
|
||||
mail(
|
||||
user.email,
|
||||
_('New sign-in to your account'),
|
||||
'pretixcontrol/email/login_notice.txt',
|
||||
{
|
||||
'when': date_format(localtime(src.last_seen), 'DATETIME_FORMAT'),
|
||||
'when': date_format(src.last_seen, 'DATETIME_FORMAT'),
|
||||
'agent': src.agent_type,
|
||||
'os': src.os_type,
|
||||
# ua-parser returns "Other" for unidentified desktop devices.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import ipaddress
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def should_block_access(sa):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
if ip_addr.is_multicast:
|
||||
return True, f"Request to multicast address {sa[0]} blocked"
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
return True, f"Request to local address {sa[0]} blocked"
|
||||
if ip_addr.is_private:
|
||||
return True, f"Request to private address {sa[0]} blocked"
|
||||
if check_ip4 in _cgnat_net:
|
||||
return True, f"Request to RFC 6598 address {sa[0]} blocked"
|
||||
|
||||
return False, None
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-20 08:01+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2026-07-13 12:35+0000\n"
|
||||
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ar/>\n"
|
||||
"Language: ar\n"
|
||||
|
||||
@@ -4,16 +4,16 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-27 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-06-20 17:00+0000\n"
|
||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"da/>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
|
||||
">\n"
|
||||
"Language: da\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html
|
||||
@@ -177,7 +177,7 @@ msgstr "Spansk (Latinamerika)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Thai"
|
||||
msgstr "Thai"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Turkish"
|
||||
@@ -436,7 +436,7 @@ msgstr ""
|
||||
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
|
||||
#, python-format
|
||||
msgid "You've been invited to join %(organizer)s"
|
||||
msgstr "Du er blevet inviteret til at tilslutte dig %(organizer)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
|
||||
msgid "This user already has been invited for this team."
|
||||
@@ -458,8 +458,9 @@ msgid "Medium connected to other event"
|
||||
msgstr "Medie er tilknyttet et andet arrangement"
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "Du kan ikke bytte en medium mod en anden medium."
|
||||
msgstr "Du kan ikke annullere denne bestilling."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
@@ -4055,32 +4056,43 @@ msgid "Users"
|
||||
msgstr "Brugere"
|
||||
|
||||
#: pretix/base/models/auth.py
|
||||
#, fuzzy
|
||||
#| msgid "Changes to your account at {organizer}"
|
||||
msgid "Changes to your account"
|
||||
msgstr "Ændringer på din konto"
|
||||
msgstr "Ændringer på din konto hos {organizer}"
|
||||
|
||||
#: pretix/base/models/auth.py
|
||||
#, python-brace-format
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "to confirm changing your email address from {old_email}\n"
|
||||
#| "to {new_email}, use the following code:"
|
||||
msgid ""
|
||||
"To change your email address from {old_email} to {new_email}, use the "
|
||||
"following code:"
|
||||
msgstr ""
|
||||
"Hvis du vil ændre din e-mailadresse fra {old_email} til {new_email}, skal du "
|
||||
"bruge følgende kode:"
|
||||
"For at bekræfte ændringen af din e-mailadresse fra {old_email}\n"
|
||||
"til {new_email} skal du bruge følgende kode:"
|
||||
|
||||
#: pretix/base/models/auth.py
|
||||
#, python-brace-format
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid ""
|
||||
#| "to confirm changing your email address from {old_email}\n"
|
||||
#| "to {new_email}, use the following code:"
|
||||
msgid ""
|
||||
"To verify your email address {email} on {instance}, use the following code:"
|
||||
msgstr ""
|
||||
"Brug følgende kode til at bekræfte din e-mailadresse {email} på {instance}:"
|
||||
"For at bekræfte ændringen af din e-mailadresse fra {old_email}\n"
|
||||
"til {new_email} skal du bruge følgende kode:"
|
||||
|
||||
#: pretix/base/models/auth.py
|
||||
#, fuzzy
|
||||
msgid "Your confirmation code"
|
||||
msgstr "Din bekræftelseskode"
|
||||
msgstr "Bekræftelsestekst"
|
||||
|
||||
#: pretix/base/models/auth.py
|
||||
#, fuzzy
|
||||
msgid "Reset your password"
|
||||
msgstr "Nulstil din adgangskode"
|
||||
msgstr "Nulstil adgangskode"
|
||||
|
||||
#: pretix/base/models/checkin.py
|
||||
msgid "All products (including newly created ones)"
|
||||
@@ -4241,16 +4253,22 @@ msgid "Check-in annulled"
|
||||
msgstr "Check-in annulleret"
|
||||
|
||||
#: pretix/base/models/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "Ticket already used"
|
||||
msgid "Ticket already exchanged"
|
||||
msgstr "Billetten er allerede ombyttet"
|
||||
msgstr "Billet allerede i brug"
|
||||
|
||||
#: pretix/base/models/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media"
|
||||
msgid "Reusable medium invalid"
|
||||
msgstr "Genanvendeligt medium er ugyldigt"
|
||||
msgstr "Genanvendelige medier"
|
||||
|
||||
#: pretix/base/models/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists"
|
||||
msgstr "Det genanvendelige medium findes allerede"
|
||||
msgstr "Genanvendelig medietype"
|
||||
|
||||
#: pretix/base/models/customers.py
|
||||
msgid "Provider name"
|
||||
@@ -4825,8 +4843,10 @@ msgstr ""
|
||||
"begge dele."
|
||||
|
||||
#: pretix/base/models/event.py
|
||||
#, fuzzy
|
||||
#| msgid "The bundled item must belong to the same event as the item."
|
||||
msgid "Property and event must belong to the same organizer."
|
||||
msgstr "Ejendommen og arrangementet skal tilhøre den samme arrangør."
|
||||
msgstr "Artikelpakken skal tilhøre samme arrangement som selve artiklen."
|
||||
|
||||
#: pretix/base/models/event.py pretix/base/models/organizer.py
|
||||
msgid "Link text"
|
||||
@@ -5062,40 +5082,42 @@ msgid "Show product with info on why it’s unavailable"
|
||||
msgstr "Vis produktet med oplysninger om, hvorfor det ikke er tilgængeligt"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Don't use re-usable media, use regular one-off tickets"
|
||||
msgid "Don't use reusable media, use regular one-off tickets"
|
||||
msgstr "Brug ikke genanvendelige medier, men almindelige engangsbilletter"
|
||||
msgstr "Brug ikke genanvendelige medier, brug almindelige engangsbilletter"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Require a previously unknown medium to be newly added"
|
||||
msgstr "Påkræv, at et førhen ukendt medie bliver tilføjet fornylig"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Require an existing medium to be re-used"
|
||||
msgid "Require an existing medium to be reused, replacing any previous tickets"
|
||||
msgstr ""
|
||||
"Påkræv, at et eksisterende medium genbruges, hvorved eventuelle tidligere "
|
||||
"billetter erstattes"
|
||||
msgstr "Påkræv genanvendelse af et eksisterende medie"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Require either an existing or a new medium to be used"
|
||||
msgid ""
|
||||
"Require either an existing or a new medium to be used, replacing any "
|
||||
"previous tickets"
|
||||
msgstr ""
|
||||
"Påkræv, at der anvendes enten et eksisterende eller et nyt medium, som "
|
||||
"erstatter eventuelle tidligere billetter"
|
||||
msgstr "Påkræv, at der enten bruges et eksisterende eller et nyt medie"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Require an existing medium to be re-used"
|
||||
msgid "Require an existing medium to be reused, adding to any previous tickets"
|
||||
msgstr ""
|
||||
"Påkræv, at et eksisterende medium genbruges og tilføjes samtlige tidligere "
|
||||
"billetter"
|
||||
msgstr "Påkræv genanvendelse af et eksisterende medie"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Require either an existing or a new medium to be used"
|
||||
msgid ""
|
||||
"Require either an existing or a new medium to be used, adding to any "
|
||||
"previous tickets"
|
||||
msgstr ""
|
||||
"Påkræv, at der anvendes enten et eksisterende eller et nyt medie, hvilket "
|
||||
"tilføjes til eventuelle tidligere billetter"
|
||||
msgstr "Påkræv, at der enten bruges et eksisterende eller et nyt medie"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Category"
|
||||
|
||||
@@ -8,16 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-27 17:00+0000\n"
|
||||
"PO-Revision-Date: 2024-09-23 18:00+0000\n"
|
||||
"Last-Translator: Albizuri <oier@puntu.eus>\n"
|
||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"eu/>\n"
|
||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/eu/"
|
||||
">\n"
|
||||
"Language: eu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
"X-Generator: Weblate 5.7.2\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html
|
||||
@@ -13559,8 +13559,10 @@ msgstr ""
|
||||
#: pretix/base/templates/pretixbase/email/order_details.html
|
||||
#: pretix/presale/templates/pretixpresale/event/base.html
|
||||
#: pretix/presale/templates/pretixpresale/organizers/base.html
|
||||
#, fuzzy
|
||||
#| msgid "Contact:"
|
||||
msgid "Contact"
|
||||
msgstr "Harremanetarako"
|
||||
msgstr "Kontaktua:"
|
||||
|
||||
#: pretix/base/templates/pretixbase/email/shred_completed.txt
|
||||
#, fuzzy, python-format
|
||||
@@ -18283,7 +18285,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html
|
||||
msgid "Tickets"
|
||||
msgstr "Sarrerak"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/navigation.py
|
||||
#: pretix/control/templates/pretixcontrol/event/tax.html
|
||||
@@ -33874,11 +33876,12 @@ msgid "Show full-size image of %(item)s"
|
||||
msgstr "Erakutsi %(item)s-ren irudi osoa"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Upcoming events"
|
||||
msgid "%(amount)s× in your cart"
|
||||
msgid_plural "%(amount)s× in your cart"
|
||||
msgstr[0] "%(amount)s× zure saskian"
|
||||
msgstr[1] "%(amount)s× zure saskian"
|
||||
msgstr[0] "Hurrengo ekitaldiak"
|
||||
msgstr[1] "Hurrengo ekitaldiak"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html
|
||||
#: pretix/presale/templates/pretixpresale/event/voucher.html
|
||||
@@ -34122,14 +34125,18 @@ msgstr ""
|
||||
"Kontuan izan oraindik zure ordainketaren zain gaudela prozesua osatzeko."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Please bookmark or save the link to this exact page if you want to access "
|
||||
#| "your order later. We also sent you an email containing the link to the "
|
||||
#| "address you specified."
|
||||
msgid ""
|
||||
"Please bookmark or save the link to this exact page if you want to access "
|
||||
"your order later. We also sent you an email to the address you specified "
|
||||
"containing the link to this page."
|
||||
msgstr ""
|
||||
"Mesedez, gorde orri honetarako esteka zure eskaera geroago ikusi nahi "
|
||||
"baduzu. E-mail bat ere bidali dizugu zehaztu duzun helbidera, orrialde "
|
||||
"honetarako estekarekin."
|
||||
"baduzu. E-mail bat ere bidali dizugu, zehaztutako helbidera lotura duena."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
#, fuzzy
|
||||
|
||||
+4891
-7500
File diff suppressed because it is too large
Load Diff
@@ -73,9 +73,7 @@ from pretix.plugins.banktransfer.payment import BankTransfer
|
||||
from pretix.plugins.banktransfer.refund_export import (
|
||||
build_sepa_xml, get_refund_export_csv,
|
||||
)
|
||||
from pretix.plugins.banktransfer.tasks import (
|
||||
notify_incomplete_payment, process_banktransfers,
|
||||
)
|
||||
from pretix.plugins.banktransfer.tasks import process_banktransfers
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.banktransfer')
|
||||
|
||||
@@ -167,11 +165,6 @@ class ActionView(View):
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
|
||||
trans.order.refresh_from_db()
|
||||
if trans.order.pending_sum > Decimal('0.00') and trans.order.status == Order.STATUS_PENDING:
|
||||
notify_incomplete_payment(trans.order)
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'ok',
|
||||
})
|
||||
|
||||
@@ -111,6 +111,7 @@ class Paypal(BasePaymentProvider):
|
||||
('client_id',
|
||||
forms.CharField(
|
||||
label=_('Client ID'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
help_text=format_html(
|
||||
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
|
||||
|
||||
@@ -230,15 +230,10 @@ class OrderMailForm(BaseMailForm):
|
||||
self.fields['attach_tickets'].help_text = _("Attachment of tickets is disabled in this event's email "
|
||||
"settings.")
|
||||
|
||||
choices = [
|
||||
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||
('valid_if_pending', _('payment pending but already confirmed')),
|
||||
('na', _('payment pending (except unapproved or already confirmed)')),
|
||||
('pa', _('approval pending')),
|
||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
||||
(Order.STATUS_EXPIRED, _("Expired")),
|
||||
]
|
||||
|
||||
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
||||
choices.insert(0, ('valid_if_pending', _('payment pending but already confirmed')))
|
||||
choices.insert(0, ('na', _('payment pending (except unapproved or already confirmed)')))
|
||||
choices.insert(0, ('pa', _('approval pending')))
|
||||
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
|
||||
choices.append(
|
||||
('overdue', _('pending with payment overdue'))
|
||||
@@ -387,15 +382,11 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
self._set_field_placeholders('subject', ['event', 'order', 'event_or_subevent'])
|
||||
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'], rich=True)
|
||||
|
||||
choices = [
|
||||
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||
('n__valid_if_pending', _('payment pending but already confirmed')),
|
||||
('n__not_pending_approval_and_not_valid_if_pending',
|
||||
_('payment pending (except unapproved or already confirmed)')),
|
||||
('n__pending_approval', _('approval pending')),
|
||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
||||
(Order.STATUS_EXPIRED, _("Expired")),
|
||||
]
|
||||
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
||||
choices.insert(0, ('n__valid_if_pending', _('payment pending but already confirmed')))
|
||||
choices.insert(0, ('n__not_pending_approval_and_not_valid_if_pending',
|
||||
_('payment pending (except unapproved or already confirmed)')))
|
||||
choices.insert(0, ('n__pending_approval', _('approval pending')))
|
||||
if not self.event.settings.get('payment_term_expire_automatically', as_type=bool):
|
||||
choices.append(
|
||||
('n__pending_overdue', _('pending with payment overdue'))
|
||||
|
||||
@@ -15,12 +15,8 @@ $(function () {
|
||||
if ($("#logs_target").length == 0) {
|
||||
return;
|
||||
}
|
||||
$.get("dashboard/partials/logs", function (data) {
|
||||
$("#logs_target").html(data)
|
||||
add_log_expand_handlers($("#logs_target"))
|
||||
});
|
||||
$.get("dashboard/partials/warnings", function (data) {
|
||||
$("#warnings_loading").remove()
|
||||
$("#warnings_target").html(data)
|
||||
$.get("logs/embed", function (data) {
|
||||
$("#logs_target").html(data)
|
||||
add_log_expand_handlers($("#logs_target"))
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,18 +60,8 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-button]', info, error)
|
||||
}
|
||||
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
|
||||
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
|
||||
// The latter can cause issues with custom CSS as HTML structure would change.
|
||||
// app.mount(element)
|
||||
// observer.observe(element, { attributes: true })
|
||||
const fragment = document.createDocumentFragment()
|
||||
app.mount(fragment)
|
||||
for (const attr of element.attributes) {
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value)
|
||||
}
|
||||
observer.observe(fragment.firstChild, { attributes: true })
|
||||
element.parentNode.replaceChild(fragment, element)
|
||||
app.mount(element)
|
||||
observer.observe(element, { attributes: true })
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -56,18 +56,8 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-widget]', info, error)
|
||||
}
|
||||
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
|
||||
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
|
||||
// The latter can cause issues with custom CSS as HTML structure would change.
|
||||
// app.mount(element)
|
||||
// observer.observe(element, { attributes: true })
|
||||
const fragment = document.createDocumentFragment()
|
||||
app.mount(fragment)
|
||||
for (const attr of element.attributes) {
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value)
|
||||
}
|
||||
observer.observe(fragment.firstChild, { attributes: true })
|
||||
element.parentNode.replaceChild(fragment, element)
|
||||
app.mount(element)
|
||||
observer.observe(element, { attributes: true })
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -1164,22 +1164,6 @@ def test_event_update_seating(token_client, organizer, event, item, seatingplan)
|
||||
assert all(seat['product'] == item.pk for seat in resp.data['results'])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_no_create_or_delete_seats(token_client, organizer, event, item, seatingplan):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/seats/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
'blocked': False,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 405
|
||||
resp = token_client.delete(
|
||||
'/api/v1/organizers/{}/events/{}/seats/3/'.format(organizer.slug, event.slug),
|
||||
)
|
||||
assert resp.status_code == 405
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_update_seating_invalid_product(token_client, organizer, event, item, seatingplan):
|
||||
resp = token_client.patch(
|
||||
|
||||
@@ -90,7 +90,6 @@ event_urls = [
|
||||
"delete/",
|
||||
"dangerzone/",
|
||||
"cancel/",
|
||||
"quickstart/",
|
||||
"settings/",
|
||||
"settings/plugins",
|
||||
"settings/payment",
|
||||
@@ -312,7 +311,6 @@ event_permission_urls = [
|
||||
("event.settings.general:write", "live/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "delete/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "dangerzone/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "quickstart/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "settings/", 200, HTTP_GET),
|
||||
# ("event.settings.payment:write", "settings/payment", 200, HTTP_GET), GET allowed also with other permissions
|
||||
("event.settings.payment:write", "settings/payment", 200, HTTP_POST),
|
||||
|
||||
@@ -50,7 +50,6 @@ def env():
|
||||
datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=23,
|
||||
sales_channel=o.sales_channels.get(identifier="web"),
|
||||
email='dummy@dummy.dummy'
|
||||
)
|
||||
o2 = Order.objects.create(
|
||||
code='6789Z', event=event,
|
||||
@@ -130,7 +129,7 @@ def test_assign_order_unknown(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_assign_order_amount_incorrect(env, client, mailoutbox):
|
||||
def test_assign_order_amount_incorrect(env, client):
|
||||
job = BankImportJob.objects.create(event=env[0])
|
||||
trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo',
|
||||
state=BankTransaction.STATE_NOMATCH,
|
||||
@@ -142,7 +141,6 @@ def test_assign_order_amount_incorrect(env, client, mailoutbox):
|
||||
assert r['status'] == 'ok'
|
||||
trans.refresh_from_db()
|
||||
assert trans.state == BankTransaction.STATE_VALID
|
||||
assert len(mailoutbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
Reference in New Issue
Block a user