forked from CGM_Public/pretix_original
Add pluggable ticket secret generators (#1809)
This commit is contained in:
@@ -37,6 +37,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('GET', 'api-v1:checkinlistpos-list'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
@@ -61,6 +62,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
|
||||
@@ -98,6 +100,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
|
||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
|
||||
|
||||
@@ -95,19 +95,41 @@ class TimeZoneField(ChoiceField):
|
||||
)
|
||||
|
||||
|
||||
class ValidKeysField(Field):
|
||||
def to_representation(self, value):
|
||||
return value.cache.get_or_set(
|
||||
'ticket_secret_valid_keys',
|
||||
lambda: self._get(value),
|
||||
120
|
||||
)
|
||||
|
||||
def _get(self, value):
|
||||
return {
|
||||
'pretix_sig1': [
|
||||
value.settings.ticket_secrets_pretix_sig1_pubkey
|
||||
] if value.settings.ticket_secrets_pretix_sig1_pubkey else []
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(self.context['request'], 'event'):
|
||||
self.fields.pop('valid_keys')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -21,7 +21,7 @@ from pretix.base.models import (
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
@@ -1209,3 +1209,10 @@ class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||
order = OrderRefund(order=self.context['order'], payment=p, **validated_data)
|
||||
order.save()
|
||||
return order
|
||||
|
||||
|
||||
class RevokedTicketSecretSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RevokedTicketSecret
|
||||
fields = ('id', 'secret', 'created')
|
||||
|
||||
@@ -39,6 +39,7 @@ event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
event_router.register(r'orders', order.OrderViewSet)
|
||||
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
||||
event_router.register(r'invoices', order.InvoiceViewSet)
|
||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
|
||||
@@ -278,13 +278,23 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
|
||||
if len(revoked_matches) == 0 or not force:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
raise Http404()
|
||||
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
raise Http404()
|
||||
|
||||
given_answers = {}
|
||||
if 'answers' in self.request.data:
|
||||
@@ -325,6 +335,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': e.code,
|
||||
'force': force,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk
|
||||
|
||||
@@ -89,7 +89,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
@@ -26,15 +26,18 @@ from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
|
||||
OrderPaymentSerializer, OrderPositionSerializer,
|
||||
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
|
||||
PriceCalcSerializer, SimulatedOrderSerializer,
|
||||
PriceCalcSerializer, RevokedTicketSecretSerializer,
|
||||
SimulatedOrderSerializer,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
|
||||
TeamAPIToken, generate_position_secret, generate_secret,
|
||||
TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import RevokedTicketSecret
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||
@@ -483,8 +486,9 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
order = self.get_object()
|
||||
order.secret = generate_secret()
|
||||
for op in order.all_positions.all():
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
assign_ticket_secret(
|
||||
request.event, op, force_invalidate=True, save=True
|
||||
)
|
||||
order.save(update_fields=['secret'])
|
||||
CachedTicket.objects.filter(order_position__order=order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=order).delete()
|
||||
@@ -1298,3 +1302,26 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class RevokedSecretFilter(FilterSet):
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = RevokedTicketSecret
|
||||
fields = []
|
||||
|
||||
|
||||
class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = RevokedTicketSecretSerializer
|
||||
queryset = RevokedTicketSecret.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('-created',)
|
||||
ordering_fields = ('created', 'secret')
|
||||
filterset_class = RevokedSecretFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return RevokedTicketSecret.objects.filter(event=self.request.event)
|
||||
|
||||
@@ -482,7 +482,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
field=models.CharField(db_index=True, default="invalid", max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
|
||||
@@ -17,6 +17,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
field=models.CharField(default="invalid", max_length=64),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_position_secret, max_length=64),
|
||||
field=models.CharField(db_index=True, default="invalid", max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
|
||||
29
src/pretix/base/migrations/0165_auto_20201015_1924.py
Normal file
29
src/pretix/base/migrations/0165_auto_20201015_1924.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-15 19:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0164_subevent_last_modified'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, max_length=64),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RevokedTicketSecret',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('secret', models.TextField(db_index=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revoked_secrets', to='pretixbase.Event')),
|
||||
('position', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='revoked_secrets', to='pretixbase.OrderPosition')),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0166_auto_20201015_2029.py
Normal file
18
src/pretix/base/migrations/0166_auto_20201015_2029.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-15 20:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0165_auto_20201015_1924'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='secret',
|
||||
field=models.CharField(db_index=True, max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -662,7 +662,14 @@ class Event(EventMixin, LoggedModel):
|
||||
s.product = item_map[s.product_id]
|
||||
s.save()
|
||||
|
||||
skip_settings = (
|
||||
'ticket_secrets_pretix_sig1_pubkey',
|
||||
'ticket_secrets_pretix_sig1_privkey',
|
||||
)
|
||||
for s in other.settings._objects.all():
|
||||
if s.key in skip_settings:
|
||||
continue
|
||||
|
||||
s.object = self
|
||||
s.pk = None
|
||||
if s.value.startswith('file://'):
|
||||
@@ -754,6 +761,31 @@ class Event(EventMixin, LoggedModel):
|
||||
renderers[pp.identifier] = pp
|
||||
return renderers
|
||||
|
||||
@cached_property
|
||||
def ticket_secret_generators(self) -> dict:
|
||||
"""
|
||||
Returns a dictionary of cached initialized ticket secret generators mapped by their identifiers.
|
||||
"""
|
||||
from ..signals import register_ticket_secret_generators
|
||||
|
||||
responses = register_ticket_secret_generators.send(self)
|
||||
renderers = {}
|
||||
for receiver, response in responses:
|
||||
if not isinstance(response, list):
|
||||
response = [response]
|
||||
for p in response:
|
||||
pp = p(self)
|
||||
renderers[pp.identifier] = pp
|
||||
return renderers
|
||||
|
||||
@property
|
||||
def ticket_secret_generator(self):
|
||||
"""
|
||||
Returns the currently configured ticket secret generator.
|
||||
"""
|
||||
tsgs = self.ticket_secret_generators
|
||||
return tsgs[self.settings.ticket_secret_generator]
|
||||
|
||||
def get_data_shredders(self) -> dict:
|
||||
"""
|
||||
Returns a dictionary of initialized data shredders mapped by their identifiers.
|
||||
|
||||
@@ -57,8 +57,7 @@ def generate_secret():
|
||||
|
||||
|
||||
def generate_position_secret():
|
||||
# Exclude o,0,1,i,l to avoid confusion with bad fonts/printers
|
||||
return get_random_string(length=settings.ENTROPY['ticket_secret'], allowed_chars='abcdefghjkmnpqrstuvwxyz23456789')
|
||||
raise TypeError("Function no longer exists, use secret generators")
|
||||
|
||||
|
||||
class Order(LockModel, LoggedModel):
|
||||
@@ -1938,7 +1937,7 @@ class OrderPosition(AbstractPosition):
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
|
||||
secret = models.CharField(max_length=255, null=False, blank=False, db_index=True)
|
||||
web_secret = models.CharField(max_length=32, default=generate_secret, db_index=True)
|
||||
pseudonymization_id = models.CharField(
|
||||
max_length=16,
|
||||
@@ -2031,13 +2030,18 @@ class OrderPosition(AbstractPosition):
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
|
||||
if self.tax_rate is None:
|
||||
self._calculate_tax()
|
||||
self.order.touch()
|
||||
if not self.pk:
|
||||
while OrderPosition.all.filter(secret=self.secret,
|
||||
order__event__organizer_id=self.order.event.organizer_id).exists():
|
||||
self.secret = generate_position_secret()
|
||||
while not self.secret or OrderPosition.all.filter(
|
||||
secret=self.secret, order__event__organizer_id=self.order.event.organizer_id
|
||||
).exists():
|
||||
assign_ticket_secret(
|
||||
event=self.order.event, position=self, force_invalidate=True, save=False
|
||||
)
|
||||
|
||||
if not self.pseudonymization_id:
|
||||
self.assign_pseudonymization_id()
|
||||
@@ -2326,6 +2330,18 @@ class CancellationRequest(models.Model):
|
||||
refund_as_giftcard = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class RevokedTicketSecret(models.Model):
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='revoked_secrets')
|
||||
position = models.ForeignKey(
|
||||
OrderPosition,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='revoked_secrets',
|
||||
null=True,
|
||||
)
|
||||
secret = models.TextField(db_index=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedTicket)
|
||||
def cachedticket_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
|
||||
@@ -48,7 +48,9 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
("secret", {
|
||||
"label": _("Ticket code (barcode content)"),
|
||||
"editor_sample": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
|
||||
"evaluate": lambda orderposition, order, event: orderposition.secret
|
||||
"evaluate": lambda orderposition, order, event: (
|
||||
orderposition.secret[:30] + "…" if len(orderposition.secret) > 32 else orderposition.secret
|
||||
)
|
||||
}),
|
||||
("order", {
|
||||
"label": _("Order code"),
|
||||
@@ -427,8 +429,13 @@ class Renderer:
|
||||
elif content == 'pseudonymization_id':
|
||||
content = op.pseudonymization_id
|
||||
|
||||
level = 'H'
|
||||
if len(content) > 32:
|
||||
level = 'M'
|
||||
if len(content) > 128:
|
||||
level = 'L'
|
||||
reqs = float(o['size']) * mm
|
||||
qrw = QrCodeWidget(content, barLevel='H', barHeight=reqs, barWidth=reqs)
|
||||
qrw = QrCodeWidget(content, barLevel=level, barHeight=reqs, barWidth=reqs)
|
||||
d = Drawing(reqs, reqs)
|
||||
d.add(qrw)
|
||||
qr_x = float(o['left']) * mm
|
||||
|
||||
11
src/pretix/base/secretgenerators/pretix_sig1.proto
Normal file
11
src/pretix/base/secretgenerators/pretix_sig1.proto
Normal file
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "eu.pretix.libpretixsync.crypto.sig1";
|
||||
option java_outer_classname = "TicketProtos";
|
||||
|
||||
message Ticket {
|
||||
string seed = 1;
|
||||
int64 item = 2;
|
||||
int64 variation = 3;
|
||||
int64 subevent = 4;
|
||||
}
|
||||
93
src/pretix/base/secretgenerators/pretix_sig1_pb2.py
Normal file
93
src/pretix/base/secretgenerators/pretix_sig1_pb2.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: pretix_sig1.proto
|
||||
|
||||
from google.protobuf import (
|
||||
descriptor as _descriptor, message as _message, reflection as _reflection,
|
||||
symbol_database as _symbol_database,
|
||||
)
|
||||
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='pretix_sig1.proto',
|
||||
package='',
|
||||
syntax='proto3',
|
||||
serialized_options=b'\n\026eu.pretix.secrets.sig1B\014TicketProtos',
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x11pretix_sig1.proto\"I\n\x06Ticket\x12\x0c\n\x04seed\x18\x01 \x01(\t\x12\x0c\n\x04item\x18\x02 \x01(\x03\x12\x11\n\tvariation\x18\x03 \x01(\x03\x12\x10\n\x08subevent\x18\x04 \x01(\x03\x42&\n\x16\x65u.pretix.secrets.sig1B\x0cTicketProtosb\x06proto3'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
_TICKET = _descriptor.Descriptor(
|
||||
name='Ticket',
|
||||
full_name='Ticket',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='seed', full_name='Ticket.seed', index=0,
|
||||
number=1, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='item', full_name='Ticket.item', index=1,
|
||||
number=2, type=3, cpp_type=2, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='variation', full_name='Ticket.variation', index=2,
|
||||
number=3, type=3, cpp_type=2, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='subevent', full_name='Ticket.subevent', index=3,
|
||||
number=4, type=3, cpp_type=2, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=21,
|
||||
serialized_end=94,
|
||||
)
|
||||
|
||||
DESCRIPTOR.message_types_by_name['Ticket'] = _TICKET
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
Ticket = _reflection.GeneratedProtocolMessageType('Ticket', (_message.Message,), {
|
||||
'DESCRIPTOR' : _TICKET,
|
||||
'__module__' : 'pretix_sig1_pb2'
|
||||
# @@protoc_insertion_point(class_scope:Ticket)
|
||||
})
|
||||
_sym_db.RegisterMessage(Ticket)
|
||||
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
202
src/pretix/base/secrets.py
Normal file
202
src/pretix/base/secrets.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import base64
|
||||
import struct
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.serialization.base import (
|
||||
Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Item, ItemVariation, SubEvent
|
||||
from pretix.base.secretgenerators import pretix_sig1_pb2
|
||||
from pretix.base.signals import register_ticket_secret_generators
|
||||
|
||||
|
||||
class BaseTicketSecretGenerator:
|
||||
"""
|
||||
This is the base class to be used for all ticket secret generators.
|
||||
"""
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name for this generator. This should be short but self-explanatory.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
A short and unique identifier for this renderer. This should only contain lowercase letters
|
||||
and in most cases will be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
@property
|
||||
def use_revocation_list(self):
|
||||
"""
|
||||
If this attribute is set to ``True``, the system will set all no-longer-used secrets on a revocation list.
|
||||
This is not required for pretix' default method of just using random identifiers as ticket secrets
|
||||
since all ticket scans will be compared to the database. However, if your secret generation method
|
||||
is designed to allow offline verification without a ticket database, all invalidated/replaced
|
||||
secrets as well as all secrets of canceled tickets will need to go to a revocation list.
|
||||
"""
|
||||
return False
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
current_secret: str = None, force_invalidate=False) -> str:
|
||||
"""
|
||||
Generate a new secret for a ticket with product ``item``, variation ``variation``, subevent ``subevent``,
|
||||
and the current secret ``current_secret`` (if any).
|
||||
|
||||
The result must be a string that should only contain the characters ``A-Za-z0-9+/=``.
|
||||
|
||||
The algorithm is expected to conform to the following rules:
|
||||
|
||||
If ``force_invalidate`` is set to ``True``, the method MUST return a different secret than ``current_secret``,
|
||||
such that ``current_secret`` can get revoked.
|
||||
|
||||
If ``force_invalidate`` is set to ``False`` and ``item``, ``variation`` and ``subevent`` have the same value
|
||||
as when ``current_secret`` was generated, then this method MUST return ``current_secret`` unchanged.
|
||||
|
||||
If ``force_invalidate`` is set to ``False`` and ``item``, ``variation`` and ``subevent`` have a different value
|
||||
as when ``current_secret`` was generated, then this method MAY OR MAY NOT return ``current_secret`` unchanged,
|
||||
depending on the semantics of the method.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class RandomTicketSecretGenerator(BaseTicketSecretGenerator):
|
||||
verbose_name = _('Random (default, works with all pretix apps)')
|
||||
identifier = 'random'
|
||||
use_revocation_list = False
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
current_secret: str = None, force_invalidate=False):
|
||||
if current_secret and not force_invalidate:
|
||||
return current_secret
|
||||
return get_random_string(
|
||||
length=settings.ENTROPY['ticket_secret'],
|
||||
# Exclude o,0,1,i,l to avoid confusion with bad fonts/printers
|
||||
allowed_chars='abcdefghjkmnpqrstuvwxyz23456789'
|
||||
)
|
||||
|
||||
|
||||
class Sig1TicketSecretGenerator(BaseTicketSecretGenerator):
|
||||
"""
|
||||
Secret generator for signed QR codes.
|
||||
|
||||
QR-code format:
|
||||
|
||||
- 1 Byte with the version of the scheme, currently 0x01
|
||||
- 2 Bytes length of the payload (big-endian) => n
|
||||
- 2 Bytes length of the signature (big-endian) => m
|
||||
- n Bytes payload (with protobuf encoding)
|
||||
- m Bytes ECDSA signature of Sign(payload)
|
||||
|
||||
The resulting string is REVERSED, to avoid all secrets of same length beginning with the same 10
|
||||
characters, which would make it impossible to search for secrets manually.
|
||||
"""
|
||||
verbose_name = _('pretix signature scheme 1 (for very large events, does not work with pretixSCAN on iOS and '
|
||||
'changes semantics of offline scanning – please refer to documentation or support for details)')
|
||||
identifier = 'pretix_sig1'
|
||||
use_revocation_list = True
|
||||
|
||||
def _generate_keys(self):
|
||||
privkey = Ed25519PrivateKey.generate()
|
||||
pubkey = privkey.public_key()
|
||||
self.event.settings.ticket_secrets_pretix_sig1_privkey = base64.b64encode(privkey.private_bytes(
|
||||
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
|
||||
)).decode()
|
||||
self.event.settings.ticket_secrets_pretix_sig1_pubkey = base64.b64encode(pubkey.public_bytes(
|
||||
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
|
||||
)).decode()
|
||||
|
||||
def _sign_payload(self, payload):
|
||||
if not self.event.settings.ticket_secrets_pretix_sig1_privkey:
|
||||
self._generate_keys()
|
||||
privkey = load_pem_private_key(
|
||||
base64.b64decode(self.event.settings.ticket_secrets_pretix_sig1_privkey), None, Backend()
|
||||
)
|
||||
signature = privkey.sign(payload)
|
||||
return (
|
||||
bytes([0x01])
|
||||
+ struct.pack(">H", len(payload))
|
||||
+ struct.pack(">H", len(signature))
|
||||
+ payload
|
||||
+ signature
|
||||
)
|
||||
|
||||
def _parse(self, secret):
|
||||
try:
|
||||
rawbytes = base64.b64decode(secret[::-1])
|
||||
if rawbytes[0] != 1:
|
||||
raise ValueError('Invalid version')
|
||||
|
||||
payload_len = struct.unpack(">H", rawbytes[1:3])[0]
|
||||
sig_len = struct.unpack(">H", rawbytes[3:5])[0]
|
||||
payload = rawbytes[5:5 + payload_len]
|
||||
signature = rawbytes[5 + payload_len:5 + payload_len + sig_len]
|
||||
pubkey = load_pem_public_key(
|
||||
base64.b64decode(self.event.settings.ticket_secrets_pretix_sig1_pubkey), Backend()
|
||||
)
|
||||
pubkey.verify(signature, payload)
|
||||
t = pretix_sig1_pb2.Ticket()
|
||||
t.ParseFromString(payload)
|
||||
return t
|
||||
except:
|
||||
return None
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
current_secret: str = None, force_invalidate=False):
|
||||
if current_secret and not force_invalidate:
|
||||
ticket = self._parse(current_secret)
|
||||
if ticket:
|
||||
unchanged = (
|
||||
ticket.item == item.pk and
|
||||
ticket.variation == (variation.pk if variation else 0) and
|
||||
ticket.subevent == (subevent.pk if subevent else 0)
|
||||
)
|
||||
if unchanged:
|
||||
return current_secret
|
||||
|
||||
t = pretix_sig1_pb2.Ticket()
|
||||
t.seed = get_random_string(9)
|
||||
t.item = item.pk
|
||||
t.variation = variation.pk if variation else 0
|
||||
t.subevent = subevent.pk if subevent else 0
|
||||
payload = t.SerializeToString()
|
||||
result = base64.b64encode(self._sign_payload(payload)).decode()[::-1]
|
||||
return result
|
||||
|
||||
|
||||
@receiver(register_ticket_secret_generators, dispatch_uid="ticket_generator_default")
|
||||
def recv_classic(sender, **kwargs):
|
||||
return [RandomTicketSecretGenerator, Sig1TicketSecretGenerator]
|
||||
|
||||
|
||||
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
||||
gen = event.ticket_secret_generator
|
||||
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
||||
force_invalidate = True
|
||||
secret = gen.generate_secret(
|
||||
item=position.item,
|
||||
variation=position.variation,
|
||||
subevent=position.subevent,
|
||||
current_secret=position.secret,
|
||||
force_invalidate=force_invalidate
|
||||
)
|
||||
changed = position.secret != secret
|
||||
if position.secret and changed and gen.use_revocation_list:
|
||||
position.revoked_secrets.create(event=event, secret=position.secret)
|
||||
position.secret = secret
|
||||
if save and changed:
|
||||
position.save()
|
||||
@@ -32,13 +32,13 @@ from pretix.base.models import (
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemBundle
|
||||
from pretix.base.models.orders import (
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_position_secret,
|
||||
generate_secret,
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_qualified,
|
||||
@@ -371,7 +371,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
position.canceled = True
|
||||
position.save(update_fields=['canceled'])
|
||||
assign_ticket_secret(
|
||||
event=order.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
position.save(update_fields=['canceled', 'secret'])
|
||||
new_fee = cancellation_fee
|
||||
for fee in order.fees.all():
|
||||
if keep_fees and fee in keep_fees:
|
||||
@@ -406,6 +409,9 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
order.save(update_fields=['status', 'cancellation_date'])
|
||||
|
||||
for position in order.positions.all():
|
||||
assign_ticket_secret(
|
||||
event=order.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=True
|
||||
)
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
|
||||
@@ -1564,6 +1570,9 @@ class OrderChangeManager:
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SeatOperation):
|
||||
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
|
||||
@@ -1575,6 +1584,9 @@ class OrderChangeManager:
|
||||
'new_seat_id': op.seat.pk if op.seat else None,
|
||||
})
|
||||
op.position.seat = op.seat
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
|
||||
@@ -1586,7 +1598,9 @@ class OrderChangeManager:
|
||||
'new_price': op.position.price
|
||||
})
|
||||
op.position.subevent = op.subevent
|
||||
op.position.save()
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
op.position.price_before_voucher = max(
|
||||
op.position.price,
|
||||
@@ -1597,6 +1611,7 @@ class OrderChangeManager:
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.AddFeeOperation):
|
||||
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
|
||||
'fee': op.fee.pk,
|
||||
@@ -1675,7 +1690,10 @@ class OrderChangeManager:
|
||||
opa.canceled = True
|
||||
if opa.voucher:
|
||||
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
opa.save(update_fields=['canceled'])
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
opa.save(update_fields=['canceled', 'secret'])
|
||||
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
@@ -1687,7 +1705,10 @@ class OrderChangeManager:
|
||||
op.position.canceled = True
|
||||
if op.position.voucher:
|
||||
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
op.position.save(update_fields=['canceled'])
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
op.position.save(update_fields=['canceled', 'secret'])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
@@ -1709,8 +1730,9 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
split_positions.append(op.position)
|
||||
elif isinstance(op, self.RegenerateSecretOperation):
|
||||
op.position.secret = generate_position_secret()
|
||||
op.position.save()
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=True, save=True
|
||||
)
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
'order': self.order.pk})
|
||||
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
|
||||
@@ -1743,7 +1765,9 @@ class OrderChangeManager:
|
||||
'new_order': split_order.code,
|
||||
})
|
||||
op.order = split_order
|
||||
op.secret = generate_position_secret()
|
||||
assign_ticket_secret(
|
||||
self.event, position=op, force_invalidate=True,
|
||||
)
|
||||
op.save()
|
||||
|
||||
try:
|
||||
|
||||
@@ -374,6 +374,10 @@ DEFAULTS = {
|
||||
'default': 'classic',
|
||||
'type': str,
|
||||
},
|
||||
'ticket_secret_generator': {
|
||||
'default': 'random',
|
||||
'type': str,
|
||||
},
|
||||
'reservation_time': {
|
||||
'default': '30',
|
||||
'type': int,
|
||||
|
||||
@@ -216,6 +216,16 @@ subclass of pretix.base.invoice.BaseInvoiceRenderer or a list of these
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_ticket_secret_generators = EventPluginSignal(
|
||||
providing_args=[]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to get all known ticket secret generators. Receivers should return a
|
||||
subclass of ``pretix.base.secrets.BaseTicketSecretGenerator`` or a list of these
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_data_shredders = EventPluginSignal(
|
||||
providing_args=[]
|
||||
)
|
||||
|
||||
@@ -1069,6 +1069,20 @@ class TicketSettingsForm(SettingsForm):
|
||||
'ticket_download_pending',
|
||||
'ticket_download_require_validated_email',
|
||||
]
|
||||
ticket_secret_generator = forms.ChoiceField(
|
||||
label=_("Ticket code generator"),
|
||||
help_text=_("For advanced users, usually does not need to be changed."),
|
||||
required=True,
|
||||
widget=forms.RadioSelect,
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.get('obj')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['ticket_secret_generator'].choices = [
|
||||
(r.identifier, r.verbose_name) for r in event.ticket_secret_generators.values()
|
||||
]
|
||||
|
||||
def prepare_fields(self):
|
||||
# See clean()
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
<legend>{% trans "Download time" %}</legend>
|
||||
{% bootstrap_field form.ticket_download_date layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Ticket codes" %}</legend>
|
||||
{% bootstrap_field form.ticket_secret_generator layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -40,13 +40,14 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedFile, CachedTicket, Checkin, Invoice,
|
||||
InvoiceAddress, Item, ItemVariation, LogEntry, Order, QuestionAnswer,
|
||||
Quota, generate_position_secret, generate_secret,
|
||||
Quota, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
)
|
||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.cancelevent import cancel_event
|
||||
from pretix.base.services.export import export
|
||||
@@ -1644,8 +1645,9 @@ class OrderContactChange(OrderView):
|
||||
changed = True
|
||||
self.order.secret = generate_secret()
|
||||
for op in self.order.all_positions.all():
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
assign_ticket_secret(
|
||||
self.request.event, position=op, force_invalidate=True, save=True
|
||||
)
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
|
||||
self.order.log_action('pretix.event.order.secret.changed', user=self.request.user)
|
||||
|
||||
|
||||
@@ -732,7 +732,7 @@ $(function () {
|
||||
|
||||
$("button[data-toggle=qrcode]").click(function (e) {
|
||||
e.preventDefault();
|
||||
var $current = $(".qr-code-overlay[data-qrcode=" + $(this).attr("data-qrcode") + "]");
|
||||
var $current = $(".qr-code-overlay[data-qrcode='" + $(this).attr("data-qrcode") + "']");
|
||||
if ($current.length) {
|
||||
$(".qr-code-overlay").attr("data-qrcode", "").slideUp(200);
|
||||
return false;
|
||||
|
||||
@@ -68,3 +68,5 @@ arabic-reshaper==2.0.15 # Support for Aabic in reportlab
|
||||
packaging
|
||||
tlds>=2020041600
|
||||
text-unidecode==1.*
|
||||
protobuf==3.13.*
|
||||
cryptography
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[flake8]
|
||||
ignore = N802,W503,E402,C901,E722,W504,E252,N812,N806
|
||||
max-line-length = 160
|
||||
exclude = migrations,.ropeproject,static,mt940.py,_static,build,make_testdata.py,*/testutils/settings.py,tests/settings.py,pretix/base/models/__init__.py
|
||||
exclude = migrations,.ropeproject,static,mt940.py,_static,build,make_testdata.py,*/testutils/settings.py,tests/settings.py,pretix/base/models/__init__.py,pretix/base/secretgenerators/pretix_sig1_pb2.py
|
||||
max-complexity = 11
|
||||
|
||||
[isort]
|
||||
|
||||
@@ -154,7 +154,9 @@ setup(
|
||||
'arabic-reshaper==2.0.15', # Support for Arabic in reportlab
|
||||
'packaging',
|
||||
'tlds>=2020041600',
|
||||
'text-unidecode==1.*'
|
||||
'text-unidecode==1.*',
|
||||
'protobuf==3.13.*',
|
||||
'cryptography',
|
||||
],
|
||||
extras_require={
|
||||
'dev': [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from unittest import mock
|
||||
@@ -153,8 +154,10 @@ def test_event_list_filter(token_client, organizer, event):
|
||||
@pytest.mark.django_db
|
||||
def test_event_get(token_client, organizer, event):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug))
|
||||
res = copy.copy(TEST_EVENT_RES)
|
||||
res["valid_keys"] = {"pretix_sig1": []}
|
||||
assert resp.status_code == 200
|
||||
assert TEST_EVENT_RES == resp.data
|
||||
assert res == resp.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -646,15 +649,6 @@ def test_event_update_plugins(token_client, organizer, event, free_item, free_qu
|
||||
assert resp.content.decode() == '{"plugins":["Unknown plugin: \'pretix.plugins.test\'."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_detail(token_client, organizer, event, team):
|
||||
team.all_events = True
|
||||
team.save()
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert TEST_EVENT_RES == resp.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_delete(token_client, organizer, event):
|
||||
resp = token_client.delete('/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug))
|
||||
|
||||
@@ -4552,3 +4552,18 @@ def test_orderposition_price_calculation_reverse_charge(token_client, organizer,
|
||||
'tax_rule': taxrule.pk,
|
||||
'tax': Decimal('0.00')
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revoked_secret_list(token_client, organizer, event):
|
||||
r = event.revoked_secrets.create(secret="abcd")
|
||||
res = {
|
||||
"id": r.id,
|
||||
"secret": "abcd",
|
||||
"created": r.created.isoformat().replace("+00:00", "Z")
|
||||
}
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/revokedsecrets/'.format(
|
||||
organizer.slug, event.slug,
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
@@ -25,6 +25,8 @@ event_urls = [
|
||||
event_permission_sub_urls = [
|
||||
('get', 'can_change_event_settings', 'settings/', 200),
|
||||
('patch', 'can_change_event_settings', 'settings/', 200),
|
||||
('get', 'can_view_orders', 'revokedsecrets/', 200),
|
||||
('get', 'can_view_orders', 'revokedsecrets/1/', 404),
|
||||
('get', 'can_view_orders', 'orders/', 200),
|
||||
('get', 'can_view_orders', 'orderpositions/', 200),
|
||||
('delete', 'can_change_orders', 'orderpositions/1/', 404),
|
||||
|
||||
@@ -675,6 +675,24 @@ class OrderCancelTests(TestCase):
|
||||
assert self.order.invoices.count() == 3
|
||||
assert not self.order.invoices.last().is_cancellation
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_paid_with_fee_change_secret(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
s = self.op1.secret
|
||||
self.order.fees.create(fee_type=OrderFee.FEE_TYPE_SHIPPING, value=2.5)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.total = 48.5
|
||||
self.order.save()
|
||||
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
|
||||
self.op1.voucher = self.event.vouchers.create(item=self.ticket, redeemed=1)
|
||||
self.op1.save()
|
||||
cancel_order(self.order.pk, cancellation_fee=2.5)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PAID
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.canceled
|
||||
assert self.op1.secret != s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_auto_refund_possible(self):
|
||||
p1 = self.order.payments.create(
|
||||
@@ -857,6 +875,7 @@ class OrderChangeManagerTests(TestCase):
|
||||
def test_change_subevent_success(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
s = self.op1.secret
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
SubEventItem.objects.create(subevent=se2, item=self.ticket, price=12)
|
||||
@@ -872,6 +891,30 @@ class OrderChangeManagerTests(TestCase):
|
||||
assert self.op1.subevent == se2
|
||||
assert self.op1.price == Decimal('23.00')
|
||||
assert self.order.total == self.op1.price + self.op2.price
|
||||
assert self.op1.secret == s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_subevent_success_change_secret(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
SubEventItem.objects.create(subevent=se2, item=self.ticket, price=12)
|
||||
s = self.op1.secret
|
||||
self.op1.subevent = se1
|
||||
self.op1.save()
|
||||
self.quota.subevent = se2
|
||||
self.quota.save()
|
||||
|
||||
self.ocm.change_subevent(self.op1, se2)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
self.order.refresh_from_db()
|
||||
assert self.op1.subevent == se2
|
||||
assert self.op1.price == Decimal('23.00')
|
||||
assert self.order.total == self.op1.price + self.op2.price
|
||||
assert self.op1.secret != s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_subevent_with_price_success(self):
|
||||
@@ -919,7 +962,9 @@ class OrderChangeManagerTests(TestCase):
|
||||
self.ocm.change_item(self.op1, self.shirt, None)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_item_keep_price(self):
|
||||
def test_change_new_secret_by_scheme(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
s = self.op1.secret
|
||||
p = self.op1.price
|
||||
self.ocm.change_item(self.op1, self.shirt, None)
|
||||
self.ocm.commit()
|
||||
@@ -929,6 +974,21 @@ class OrderChangeManagerTests(TestCase):
|
||||
assert self.op1.price == p
|
||||
assert self.op1.tax_value == Decimal('3.67')
|
||||
assert self.op1.tax_rule == self.shirt.tax_rule
|
||||
assert self.op1.secret != s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_item_keep_price(self):
|
||||
p = self.op1.price
|
||||
s = self.op1.secret
|
||||
self.ocm.change_item(self.op1, self.shirt, None)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
self.order.refresh_from_db()
|
||||
assert self.op1.item == self.shirt
|
||||
assert self.op1.price == p
|
||||
assert self.op1.tax_value == Decimal('3.67')
|
||||
assert self.op1.tax_rule == self.shirt.tax_rule
|
||||
assert self.op1.secret == s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_item_change_price_before_voucher(self):
|
||||
@@ -1011,6 +1071,7 @@ class OrderChangeManagerTests(TestCase):
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_success(self):
|
||||
s = self.op1.secret
|
||||
self.ocm.cancel(self.op1)
|
||||
self.ocm.commit()
|
||||
self.order.refresh_from_db()
|
||||
@@ -1018,6 +1079,20 @@ class OrderChangeManagerTests(TestCase):
|
||||
assert self.order.total == self.op2.price
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.canceled
|
||||
assert self.op1.secret == s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_success_changed_secret(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
s = self.op1.secret
|
||||
self.ocm.cancel(self.op1)
|
||||
self.ocm.commit()
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.positions.count() == 1
|
||||
assert self.order.total == self.op2.price
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.canceled
|
||||
assert self.op1.secret != s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_with_addon(self):
|
||||
@@ -2278,6 +2353,9 @@ class OrderChangeManagerTests(TestCase):
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_clear_out_order(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
op = self.order.positions.first()
|
||||
s = op.secret
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
self.order.payments.create(amount=self.order.total, state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
@@ -2290,6 +2368,27 @@ class OrderChangeManagerTests(TestCase):
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.total == Decimal('0.00')
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
assert op.secret == s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_clear_out_order_change_secrets(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
op = self.order.positions.first()
|
||||
s = op.secret
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
self.order.payments.create(amount=self.order.total, state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
provider='manual')
|
||||
cancel_order(self.order, cancellation_fee=Decimal('5.00'))
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.total == Decimal('5.00')
|
||||
self.ocm.cancel_fee(self.order.fees.get())
|
||||
self.ocm.commit()
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.total == Decimal('0.00')
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
op.refresh_from_db()
|
||||
assert op.secret != s
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_auto_change_payment_fee(self):
|
||||
|
||||
Reference in New Issue
Block a user