Compare commits

...

41 Commits

Author SHA1 Message Date
Raphael Michel
173cf751da Add search index model 2019-02-14 18:11:25 +01:00
Raphael Michel
b3debdfb55 Order list: Add filter for canceled with and without paid fee 2019-02-14 10:15:55 +01:00
Raphael Michel
abb770a8e7 Prevent events from being set to None through the API 2019-02-14 10:15:55 +01:00
Raphael Michel
72a2d0da35 Search in e-mail adresses during checkin 2019-02-14 10:15:55 +01:00
Raphael Michel
937cec53f7 Optimize queries for pdf_data=true 2019-02-14 10:15:55 +01:00
Raphael Michel
6e4af5da64 Perform order search on database replica 2019-02-14 10:15:02 +01:00
Raphael Michel
7ed35e06ba Allow to configure a database replica 2019-02-14 10:14:23 +01:00
Raphael Michel
55841ea660 Make sure total is calculated as a Decimal 2019-02-12 16:27:37 +01:00
Raphael Michel
78544cdb30 Implement a strong locking check to avoid race conditions during payment 2019-02-12 16:24:32 +01:00
Martin Gross
37183aced7 Disable Autocomplete for Date/Time-fields 2019-02-12 16:16:12 +01:00
Raphael Michel
a7d3cb134c Fix a token mismatch 2019-02-12 15:40:06 +01:00
Raphael Michel
da8f7f163f Check-in API: Include position data 2019-02-12 15:40:06 +01:00
Raphael Michel
89d612beed Fix bug in checkinlist view 2019-02-11 16:39:50 +01:00
Raphael Michel
f23de7e2c0 Order change: Allow to ignore quotas 2019-02-11 16:15:54 +01:00
Raphael Michel
d073007fd7 Order change: Allow to keep price when changing items 2019-02-11 16:15:13 +01:00
Raphael Michel
d9d1c83218 Optimize the check-in list view 2019-02-11 15:58:39 +01:00
Raphael Michel
ae9b8bafb8 Add missing migration 2019-02-08 15:33:26 +01:00
Raphael Michel
cbf5c2ec1d Fix ZeroDivisionError if a voucher tag is given to a voucher with max_usages=0
Fix PRETIXEU-V7
2019-02-08 13:59:05 +01:00
Raphael Michel
17392f3ef4 Store subevent data for invoice lines 2019-02-08 13:56:04 +01:00
Raphael Michel
bf36ad009f Don't request a refund if there's actually no money involved 2019-02-08 11:27:09 +01:00
Martin Gross
ca9e4823e2 Fix wrong CSV-Checkinlist link 2019-02-08 10:47:17 +01:00
Raphael Michel
d505422e0f Order overview: Make table easier to read 2019-02-07 15:28:51 +01:00
Raphael Michel
33c43ce482 Add columns lines to PDF overview export 2019-02-07 15:21:17 +01:00
Raphael Michel
f273cf4960 Fix misaligned PDF report 2019-02-07 15:21:12 +01:00
Raphael Michel
afdf09eeb4 Merge branch 'master' of github.com:pretix/pretix 2019-02-07 15:02:14 +01:00
Raphael Michel
01e5872f61 Update jsonfallback again 2019-02-07 15:01:55 +01:00
Maximilian Hils
14cc31c810 Fix payment instruction display. (#1161) 2019-02-07 14:30:26 +01:00
Raphael Michel
2972129547 Sentry: Fix a bug leading to it ignoring *everything* 2019-02-06 11:16:38 +01:00
Raphael Michel
ec4227651a Do not try to reduce voucher usage below 0 2019-02-06 10:35:54 +01:00
Raphael Michel
77950de588 Voucher bulk delete: Remove cart positions as well 2019-02-06 10:28:26 +01:00
Raphael Michel
187576eee5 Fix a ProtectedError in cart handling
FIx PRETIXEU-TR
2019-02-06 10:25:53 +01:00
Raphael Michel
0e513a0985 Require specific jsonfallback version 2019-02-06 09:59:44 +01:00
Raphael Michel
1cde728ffe Order search: Add missing field to .only() call 2019-02-06 09:52:11 +01:00
Raphael Michel
76893caffc Sentry: Ignore django.security.DisallowedHost 2019-02-05 18:15:21 +01:00
Raphael Michel
a539999c04 Sentry: Do not report retried celery tasks 2019-02-05 17:19:18 +01:00
Raphael Michel
b9c570b3d8 Sentry: Tune log levels 2019-02-05 16:35:40 +01:00
Raphael Michel
48b399424a Delete voucher even if it is contained in carts
Fix PRETIXEU-R1
2019-02-05 15:47:11 +01:00
Raphael Michel
1c73f000a9 Fix TypeError
PRETIXEU-T6
2019-02-05 15:00:58 +01:00
Raphael Michel
d0721165c1 Add distinct call back in in some cases 2019-02-05 12:10:27 +01:00
Raphael Michel
bed0a0ceeb Switch from raven to sentry_sdk 2019-02-05 11:25:58 +01:00
Raphael Michel
b53ee1dc1d Bump version to 2.5.0.dev0 2019-02-04 16:06:43 +01:00
44 changed files with 582 additions and 115 deletions

View File

@@ -125,6 +125,23 @@ Example::
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
Database replica settings
-------------------------
If you use a replicated database setup, pretix expects that the default database connection always points to the primary database node.
Routing read queries to a replica on databse layer is **strongly** discouraged since this can lead to inaccurate such as more tickets
being sold than are actually available.
However, pretix can still make use of a database replica to keep some expensive queries with that can tolerate some latency from your
primary database, such as backend search queries. The ``replica`` configuration section can have the same settings as the ``database``
section (except for the ``backend`` setting) and will default back to the ``database`` settings for all values that are not given. This
way, you just need to specify the settings that are different for the replica.
Example::
[replica]
host=192.168.0.2
URLs
----

View File

@@ -565,7 +565,10 @@ Order position endpoints
Content-Type: application/json
{
"status": "ok"
"status": "ok",
"position": {
}
}
**Example response with required questions**:
@@ -576,7 +579,10 @@ Order position endpoints
Content-Type: text/json
{
"status": "incomplete"
"status": "incomplete",
"position": {
},
"questions": [
{
"id": 1,
@@ -621,6 +627,9 @@ Order position endpoints
{
"status": "error",
"reason": "unpaid",
"position": {
}
}
Possible error reasons:

View File

@@ -1 +1 @@
__version__ = "2.4.0"
__version__ = "2.5.0.dev0"

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
@@ -108,7 +109,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
plugins = validated_data.pop('plugins', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
event = super().create(validated_data)
# Meta data
@@ -122,6 +123,7 @@ class EventSerializer(I18nAwareModelSerializer):
# Plugins
if plugins is not None:
event.set_active_plugins(plugins)
event.save(update_fields=['plugins'])
return event

View File

@@ -16,7 +16,9 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
from pretix.base.models import (
Checkin, CheckinList, Event, Order, OrderPosition,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
)
@@ -201,12 +203,39 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
).prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
)
).select_related('item', 'variation', 'order', 'addon_to')
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
@@ -251,6 +280,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except RequiredQuestionsError as e:
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
@@ -259,10 +290,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
return Response({
'status': 'error',
'reason': e.code,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self):

View File

@@ -26,8 +26,8 @@ from pretix.api.serializers.order import (
OrderRefundSerializer, OrderSerializer,
)
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Invoice, Order, OrderPayment,
OrderPosition, OrderRefund, Quota, TeamAPIToken,
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
@@ -83,6 +83,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item__category', 'addon_to',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
)
)
@@ -391,6 +392,7 @@ class OrderPositionFilter(FilterSet):
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
@@ -435,11 +437,33 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
}
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
qs = OrderPosition.objects.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
)
else:
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
return qs
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)

View File

@@ -209,7 +209,7 @@ class MultiSheetListExporter(ListExporter):
sheet, f = form_data.get('_format').split(':')
if f == 'default':
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
elif f == 'csv-excel':
elif f == 'excel':
return self._render_sheet_csv(form_data, sheet, dialect='excel')
elif f == 'semicolon':
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';')

View File

@@ -559,6 +559,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Tax value'),
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
@@ -598,6 +599,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
l.tax_value,
l.tax_rate,
l.tax_name,
date_format(l.event_date_from, "SHORT_DATE_FORMAT") if l.event_date_from else "",
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,

View File

@@ -90,6 +90,8 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control splitdatetimepart')
time_attrs.setdefault('class', 'form-control splitdatetimepart')
date_attrs.setdefault('autocomplete', 'off')
time_attrs.setdefault('autocomplete', 'off')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'

View File

@@ -0,0 +1,30 @@
from django.core.management.base import BaseCommand
from django.core.paginator import Paginator
from pretix.base.models import Order, OrderSearchIndex
class Command(BaseCommand):
help = "Rebuild search index"
def add_arguments(self, parser):
parser.add_argument(
'--clean', action='store_true', dest='clean',
help="Clear search index before run.",
)
def iter_pages(self, qs):
paginator = Paginator(qs, 500)
for index in range(paginator.num_pages):
yield paginator.get_page(index + 1)
def handle(self, *args, **options):
if options.get('clean'):
OrderSearchIndex.objects.all().delete()
qs = Order.objects.select_related('event', 'event__organizer', 'invoice_address').prefetch_related('all_positions', 'payments')
for page in self.iter_pages(qs):
if options.get('clean'):
OrderSearchIndex.objects.bulk_create([o.index() for o in page])
else:
for o in page:
o.index()

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.1 on 2019-02-08 14:32
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0108_auto_20190201_1527'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='event_date_from',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='invoiceline',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 2.1 on 2019-02-14 10:41
import django.db.models.deletion
from django.db import migrations, models
def create_extension(apps, schema_editor):
if 'postgresql' in schema_editor.connection.settings_dict['ENGINE']:
schema_editor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;")
def create_trigram_index(apps, schema_editor):
if 'postgresql' in schema_editor.connection.settings_dict['ENGINE']:
schema_editor.execute("CREATE INDEX pretixbase_ordersearchindex_s ON pretixbase_ordersearchindex USING gin (search_body gin_trgm_ops);")
schema_editor.execute("CREATE INDEX pretixbase_ordersearchindex_spp ON pretixbase_ordersearchindex USING gin (payment_providers gin_trgm_ops);")
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0109_auto_20190208_1432'),
]
operations = [
migrations.RunPython(create_extension),
migrations.CreateModel(
name='OrderSearchIndex',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('search_body', models.TextField()),
('payment_providers', models.TextField()),
],
),
migrations.AddField(
model_name='ordersearchindex',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event'),
),
migrations.AddField(
model_name='ordersearchindex',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order', unique=True),
),
migrations.AddField(
model_name='ordersearchindex',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer'),
),
migrations.RunPython(create_trigram_index)
]

View File

@@ -7,6 +7,7 @@ from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
)
from .index import OrderSearchIndex
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,

View File

@@ -0,0 +1,11 @@
from django.db import models
DELIMITER = "\x1F"
class OrderSearchIndex(models.Model):
order = models.ForeignKey('Order', unique=True, null=False, on_delete=models.CASCADE)
event = models.ForeignKey('Event', null=False, on_delete=models.CASCADE)
organizer = models.ForeignKey('Organizer', null=False, on_delete=models.CASCADE)
search_body = models.TextField()
payment_providers = models.TextField()

View File

@@ -212,6 +212,10 @@ class InvoiceLine(models.Model):
:type tax_rate: decimal.Decimal
:param tax_name: The name of the applied tax rate
:type tax_name: str
:param subevent: The subevent this line refers to
:type subevent: SubEvent
:param event_date_from: Event date of the (sub)event at the time the invoice was created
:type event_date_from: datetime
"""
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
position = models.PositiveIntegerField(default=0)
@@ -220,6 +224,8 @@ class InvoiceLine(models.Model):
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
tax_name = models.CharField(max_length=190)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True)
@property
def net_value(self):

View File

@@ -693,6 +693,45 @@ class Order(LockModel, LoggedModel):
continue
yield op
def index(self, save=True):
from .index import OrderSearchIndex
indexed_strings = [
self.code,
self.full_code,
self.email,
self.comment,
]
try:
indexed_strings.append(self.invoice_address.name_cached)
indexed_strings.append(self.invoice_address.company)
except InvoiceAddress.DoesNotExist:
pass
for p in self.all_positions.all():
indexed_strings.append(p.attendee_name_cached)
indexed_strings.append(p.attendee_email)
indexed_strings.append(p.secret)
pprovs = set()
for p in self.payments.all():
pprovs.add(p.provider)
if save:
return OrderSearchIndex.objects.update_or_create(
order=self,
defaults={
'event': self.event,
'organizer': self.event.organizer,
'search_body': '\x1E'.join([str(v) for v in indexed_strings if v]),
'payment_providers': '\x1E' + '\x1E'.join([str(v) for v in pprovs if v]) + '\x1E',
}
)[0]
else:
return OrderSearchIndex(
order=self,
event=self.event,
organizer=self.event.organizer,
search_body='\x1E'.join([str(v) for v in indexed_strings if v]),
payment_providers='\x1E' + '\x1E'.join([str(v) for v in pprovs if v]) + '\x1E',
)
def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
@@ -1098,9 +1137,23 @@ class OrderPayment(models.Model):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
self.state = self.PAYMENT_STATE_CONFIRMED
self.payment_date = now()
self.save()
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
# Race condition detected, this payment is already confirmed
return
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
locked_instance.payment_date = now()
locked_instance.info = self.info # required for backwards compatibility
locked_instance.save(update_fields=['state', 'payment_date', 'info'])
# Do a cheap manual "refresh from db" on non-complex fields
for field in self._meta.concrete_fields:
if not field.is_relation:
setattr(self, field.attname, getattr(locked_instance, field.attname))
self.refresh_from_db()
self.order.log_action('pretix.event.order.payment.confirmed', {
'local_id': self.local_id,

View File

@@ -788,9 +788,9 @@ class ManualPayment(BasePaymentProvider):
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order))
return msg
def order_pending_render(self, request, order) -> str:
def payment_pending_render(self, request, payment) -> str:
return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(order))
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order))
)

View File

@@ -371,7 +371,7 @@ class CartManager:
# could cancel each other out quota-wise). However, we are not taking this performance
# penalty for now as there is currently no outside interface that would allow building
# such a transaction.
for cp in self.positions.all():
for cp in self.positions.filter(addon_to__isnull=True):
self._operations.append(self.RemoveOperation(position=cp))
def set_addons(self, addons):
@@ -653,6 +653,7 @@ class CartManager:
op.position.price = op.price.gross
op.position.save()
elif available_count == 0:
op.position.addons.all().delete()
op.position.delete()
else:
raise AssertionError("ExtendOperation cannot affect more than one item")

View File

@@ -142,6 +142,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
InvoiceLine.objects.create(
position=i, invoice=invoice, description=desc,
gross_value=p.price, tax_value=p.tax_value,
subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
)

View File

@@ -10,6 +10,7 @@ from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.db import transaction
from django.db.models import F, Max, Q, Sum
from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
@@ -255,7 +256,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
if send_mail:
try:
@@ -323,7 +324,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
with order.event.lock():
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
position.save(update_fields=['canceled'])
for fee in order.fees.all():
@@ -354,7 +355,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee})
@@ -814,12 +815,17 @@ class OrderChangeManager:
self.notify = notify
self._invoice_dirty = False
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation], keep_price=False):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
invoice_address=self._invoice_address)
if keep_price:
price = TaxedPrice(gross=position.price, net=position.price - position.tax_value,
tax=position.tax_value, rate=position.tax_rate,
name=position.tax_rule.name)
else:
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
invoice_address=self._invoice_address)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
@@ -1067,7 +1073,7 @@ class OrderChangeManager:
})
opa.canceled = True
if opa.voucher:
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
opa.save(update_fields=['canceled'])
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': op.position.pk,
@@ -1079,7 +1085,7 @@ class OrderChangeManager:
})
op.position.canceled = True
if op.position.voucher:
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
op.position.save(update_fields=['canceled'])
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
@@ -1300,7 +1306,7 @@ class OrderChangeManager:
except SendMailException:
logger.exception('Order changed email could not be sent')
def commit(self):
def commit(self, check_quotas=True):
if self._committed:
# an order change can only be committed once
raise OrderError(error_messages['internal'])
@@ -1317,7 +1323,8 @@ class OrderChangeManager:
with self.order.event.lock():
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID):
raise OrderError(self.error_messages['not_pending_or_paid'])
self._check_quotas()
if check_quotas:
self._check_quotas()
self._check_complete_cancel()
self._perform_operations()
self._recalculate_total_and_payment_fee()
@@ -1409,7 +1416,7 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
else:
elif refund_amount != Decimal('0.00'):
notify_admin = True
if notify_admin:

View File

@@ -117,7 +117,7 @@ def get_tickets_for_order(order):
if p.multi_download_enabled:
try:
if len(order.positions_with_tickets) == 0:
if len(list(order.positions_with_tickets)) == 0:
continue
ct = CachedCombinedTicket.objects.filter(
order=order, provider=p.identifier, file__isnull=False

View File

@@ -8,13 +8,13 @@
<h1>{% trans "Internal Server Error" %}</h1>
<p>{% trans "We had trouble processing your request." %}</p>
<p>{% trans "If this problem persists, please contact us." %}</p>
{% if request.sentry.id %}
{% if sentry_event_id %}
<p>
{% blocktrans trimmed %}
If you contact us, please send us the following code:
{% endblocktrans %}
<br>
{{ request.sentry.id }}
{{ sentry_event_id }}
</p>
{% endif %}
<p>{{ exception }}</p>

View File

@@ -7,6 +7,7 @@ from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id
def csrf_failure(request, reason=""):
@@ -65,5 +66,6 @@ def server_error(request):
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
return HttpResponseServerError(template.render({
'request': request
'request': request,
'sentry_event_id': last_event_id(),
}))

View File

@@ -207,10 +207,11 @@ class EventOrderFilterForm(OrderFilterForm):
(Order.STATUS_EXPIRED, _('Expired')),
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
(Order.STATUS_CANCELED, _('Canceled')),
('cp', _('Canceled (or with paid fee)')),
('pa', _('Approval pending')),
('overpaid', _('Overpaid')),
('underpaid', _('Underpaid')),
('pendingpaid', _('Pending (but fully paid)'))
('pendingpaid', _('Pending (but fully paid)')),
),
required=False,
)
@@ -244,10 +245,10 @@ class EventOrderFilterForm(OrderFilterForm):
qs = super().filter_qs(qs)
if fdata.get('item'):
qs = qs.filter(all_positions__item=fdata.get('item'), all_positions__canceled=False)
qs = qs.filter(all_positions__item=fdata.get('item'), all_positions__canceled=False).distinct()
if fdata.get('subevent'):
qs = qs.filter(all_positions__subevent=fdata.get('subevent'), all_positions__canceled=False)
qs = qs.filter(all_positions__subevent=fdata.get('subevent'), all_positions__canceled=False).distinct()
if fdata.get('question') and fdata.get('answer') is not None:
q = fdata.get('question')
@@ -297,6 +298,15 @@ class EventOrderFilterForm(OrderFilterForm):
status=Order.STATUS_PENDING,
require_approval=True
)
elif fdata.get('status') == 'cp':
s = OrderPosition.objects.filter(
order=OuterRef('pk')
)
qs = qs.annotate(
has_pc=Exists(s)
).filter(
Q(status=Order.STATUS_PAID, has_pc=False) | Q(status=Order.STATUS_CANCELED)
)
return qs

View File

@@ -177,6 +177,10 @@ class OtherOperationsForm(forms.Form):
'Send an email to the customer notifying that their order has been changed.'
)
)
ignore_quotas = forms.BooleanField(
label=_('Allow to overbook quotas when performing this operation'),
required=False,
)
def __init__(self, *args, **kwargs):
kwargs.pop('order')
@@ -286,6 +290,7 @@ class OrderPositionChangeForm(forms.Form):
('secret', 'Regenerate secret'),
)
)
change_product_keep_price = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance')

View File

@@ -21,7 +21,7 @@
<span class="fa fa-download"></span>
{% trans "PDF" %}
</a>
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistcsv&checkinlistcsv-list={{ checkinlist.pk }}"
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlist&checkinlist-list={{ checkinlist.pk }}"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>
{% trans "CSV" %}

View File

@@ -95,6 +95,10 @@
{% trans "Change product to" %}
{% bootstrap_field position.form.itemvar layout='inline' %}
</label>
<label class="checkbox">
{{ position.form.change_product_keep_price }}
{% trans "Keep price the same" %}
</label>
</div>
{% if request.event.has_subevents %}
<div class="radio">
@@ -202,6 +206,9 @@
</div>
</div>
</div>
<div class="">
{% bootstrap_field other_form.ignore_quotas layout="" %}
</div>
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">

View File

@@ -32,7 +32,7 @@
<th>{% trans "Product" %}</th>
<th>{% trans "Canceled" %}¹</th>
<th>{% trans "Expired" %}</th>
<th colspan="3">{% trans "Purchased" %}</th>
<th colspan="3" class="text-center">{% trans "Purchased" %}</th>
</tr>
<tr>
<th></th>

View File

@@ -135,14 +135,33 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/checkin/lists.html'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related("limit_products")
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
if self.request.GET.get("subevent", "") != "":
s = self.request.GET.get("subevent", "")
qs = qs.filter(subevent_id=s)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
clists = list(ctx['checkinlists'])
# Optimization: Fetch expensive columns for this page only
annotations = {
a['pk']: a
for a in CheckinList.annotate_with_numbers(CheckinList.objects.filter(pk__in=[l.pk for l in clists]), self.request.event).values(
'pk', 'checkin_count', 'position_count', 'percent'
)
}
for cl in clists:
if cl.subevent:
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
cl.checkin_count = annotations.get(cl.pk, {}).get('checkin_count', 0)
cl.position_count = annotations.get(cl.pk, {}).get('position_count', 0)
cl.percent_count = annotations.get(cl.pk, {}).get('percent_count', 0)
ctx['checkinlists'] = clists
return ctx
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
model = CheckinList

View File

@@ -1229,7 +1229,7 @@ class OrderChange(OrderView):
variation = ItemVariation.objects.get(pk=varid, item=item)
else:
variation = None
ocm.change_item(p, item, variation)
ocm.change_item(p, item, variation, keep_price=p.form.cleaned_data['change_product_keep_price'])
elif p.form.cleaned_data['operation'] == 'price':
ocm.change_price(p, p.form.cleaned_data['price'])
elif p.form.cleaned_data['operation'] == 'subevent':
@@ -1259,7 +1259,7 @@ class OrderChange(OrderView):
messages.error(self.request, _('An error occurred. Please see the details below.'))
else:
try:
ocm.commit()
ocm.commit(check_quotas=not self.other_form.cleaned_data['ignore_quotas'])
except OrderError as e:
messages.error(self.request, str(e))
else:

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.db.models import Count, IntegerField, OuterRef, Q, Subquery
from django.utils.functional import cached_property
from django.views.generic import ListView
@@ -28,7 +29,7 @@ class OrderSearch(PaginationMixin, ListView):
annotated = {
o['pk']: o
for o in
Order.objects.filter(
Order.objects.using(settings.DATABASE_REPLICA).filter(
pk__in=[o.pk for o in ctx['orders']]
).annotate(
pcnt=Subquery(s, output_field=IntegerField())
@@ -45,7 +46,7 @@ class OrderSearch(PaginationMixin, ListView):
return ctx
def get_queryset(self):
qs = Order.objects.select_related('invoice_address')
qs = Order.objects.select_related('invoice_address').using(settings.DATABASE_REPLICA)
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
@@ -60,7 +61,7 @@ class OrderSearch(PaginationMixin, ListView):
return qs.only(
'id', 'invoice_address__name_cached', 'invoice_address__name_parts', 'code', 'event', 'email',
'datetime', 'total', 'status'
'datetime', 'total', 'status', 'require_approval'
).prefetch_related(
'event', 'event__organizer'
)

View File

@@ -105,7 +105,10 @@ class VoucherTags(EventPermissionRequiredMixin, TemplateView):
redeemed=Sum('redeemed')
)
for t in tags:
t['percentage'] = int((t['redeemed'] / t['total']) * 100)
if t['total'] == 0:
t['percentage'] = 0
else:
t['percentage'] = int((t['redeemed'] / t['total']) * 100)
ctx['tags'] = tags
return ctx
@@ -140,6 +143,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
messages.error(request, _('A voucher can not be deleted if it already has been redeemed.'))
else:
self.object.log_action('pretix.voucher.deleted', user=self.request.user)
self.object.cartposition_set.all().delete()
self.object.delete()
messages.success(request, _('The selected voucher has been deleted.'))
return HttpResponseRedirect(success_url)
@@ -344,6 +348,7 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
for obj in self.objects:
if obj.allow_delete():
obj.log_action('pretix.voucher.deleted', user=self.request.user)
obj.cartposition_set.all().delete()
obj.delete()
else:
obj.log_action('pretix.voucher.changed', user=self.request.user, data={

View File

@@ -12,6 +12,7 @@ from django.template.defaultfilters import floatformat
from django.utils.formats import date_format, localize
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import pgettext, pgettext_lazy, ugettext as _
from reportlab.lib import colors
from pretix.base.decimal import round_decimal
from pretix.base.exporter import BaseExporter
@@ -162,17 +163,21 @@ class OverviewReport(Report):
tstyledata = [
('SPAN', (1, 0), (2, 0)),
('SPAN', (3, 0), (4, 0)),
('SPAN', (5, 0), (6, 0)),
('SPAN', (7, 0), (-1, 0)),
('SPAN', (5, 0), (-1, 0)),
('SPAN', (5, 1), (6, 1)),
('SPAN', (7, 1), (8, 1)),
('SPAN', (9, 1), (10, 1)),
('SPAN', (11, 1), (12, 1)),
('ALIGN', (0, 0), (-1, 1), 'CENTER'),
('ALIGN', (1, 2), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTNAME', (0, 0), (-1, 1), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('FONTSIZE', (0, 0), (-1, -1), 9)
('FONTSIZE', (0, 0), (-1, -1), 9),
('LINEBEFORE', (1, 0), (1, -1), 1, colors.lightgrey),
('LINEBEFORE', (3, 0), (3, -1), 1, colors.lightgrey),
('LINEBEFORE', (5, 0), (5, -1), 1, colors.lightgrey),
('LINEBEFORE', (7, 1), (7, -1), 1, colors.lightgrey),
('LINEBEFORE', (9, 1), (9, -1), 1, colors.lightgrey),
]
story = [
@@ -192,7 +197,7 @@ class OverviewReport(Report):
'', '', '', '', ''
],
[
'', '', '', '', '', '', '', _('Pending'), '', _('Paid'), '', _('Total'), ''
'', '', '', '', '', _('Pending'), '', _('Paid'), '', _('Total'), ''
],
[
'',
@@ -201,7 +206,6 @@ class OverviewReport(Report):
_('#'), self.event.currency,
_('#'), self.event.currency,
_('#'), self.event.currency,
_('#'), self.event.currency,
],
]

View File

@@ -1,3 +1,5 @@
from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
@@ -460,7 +462,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
def _total_order_value(self):
total = get_cart_total(self.request)
total += sum([f.value for f in get_fees(self.request.event, self.request, total, self.invoice_address, None)])
return total
return Decimal(total)
@cached_property
def provider_forms(self):

View File

@@ -1,5 +1,6 @@
from collections import defaultdict
from datetime import datetime, timedelta
from decimal import Decimal
from functools import wraps
from itertools import groupby
@@ -198,7 +199,7 @@ def get_cart_total(request):
else:
request._cart_total_cache = CartPosition.objects.filter(
cart_id=get_or_create_cart_id(request), event=request.event
).aggregate(sum=Sum('price'))['sum'] or 0
).aggregate(sum=Sum('price'))['sum'] or Decimal('0.00')
return request._cart_total_cache

View File

@@ -121,7 +121,7 @@ def widget_js(request, lang, **kwargs):
try:
resp = HttpResponse(default_storage.open(fname).read(), content_type='text/javascript')
except:
logger.exception('Failed to open widget.js')
logger.critical('Failed to open widget.js')
if not resp:
data = generate_widget_js(lang).encode()

View File

@@ -1,50 +1,110 @@
from threading import Lock
import re
import weakref
from collections import OrderedDict
from raven.contrib.celery import SentryCeleryHandler
from raven.contrib.django.apps import RavenConfig
from raven.contrib.django.models import (
SentryDjangoHandler, client, get_client, install_middleware,
register_serializers,
)
from celery.exceptions import Retry
from sentry_sdk import Hub
from sentry_sdk.integrations.django import DjangoIntegration, _set_user_info
from sentry_sdk.utils import capture_internal_exceptions
_setup_lock = Lock()
_initialized = False
MASK = '*' * 8
KEYS = frozenset([
'password',
'secret',
'passwd',
'authorization',
'api_key',
'apikey',
'sentry_dsn',
'access_token',
'session',
])
VALUES_RE = re.compile(r'^(?:\d[ -]*?){13,16}$')
class CustomSentryDjangoHandler(SentryDjangoHandler):
def install_celery(self):
self.celery_handler = SentryCeleryHandler(client, ignore_expected=True).install()
def scrub_data(data):
if isinstance(data, dict):
for k, v in data.items():
if isinstance(k, bytes):
key = k.decode('utf-8', 'replace')
else:
key = k
key = key.lower()
data[k] = scrub_data(v)
for blk in KEYS:
if blk in key:
data[k] = MASK
elif isinstance(data, list):
for i, l in enumerate(list(data)):
data[i] = scrub_data(l)
elif isinstance(data, str):
if '=' in data:
# at this point we've assumed it's a standard HTTP query
# or cookie
if '&' in data:
delimiter = '&'
else:
delimiter = ';'
qd = scrub_data(OrderedDict(e.split('=', 1) if '=' in e else (e, None) for e in data.split(delimiter)))
return delimiter.join((k + '=' + v if v is not None else k) for k, v in qd.items())
if VALUES_RE.match(data):
return MASK
return data
def initialize():
global _initialized
def _make_event_processor(weak_request, integration):
def event_processor(event, hint):
request = weak_request()
if request is None:
return event
with _setup_lock:
if _initialized:
return
with capture_internal_exceptions():
_set_user_info(request, event)
request_info = event.setdefault("request", {})
request_info["cookies"] = dict(request.COOKIES)
_initialized = True
scrub_data(event.get("request", {}))
if 'exception' in event:
exc = event.get("exception", {})
for val in exc.get('values', []):
stack = val.get('stacktrace', {})
for frame in stack.get('frames', []):
scrub_data(frame['vars'])
return event
try:
register_serializers()
install_middleware(
'raven.contrib.django.middleware.SentryMiddleware',
(
'raven.contrib.django.middleware.SentryMiddleware',
'raven.contrib.django.middleware.SentryLogMiddleware'))
install_middleware(
'raven.contrib.django.middleware.DjangoRestFrameworkCompatMiddleware')
handler = CustomSentryDjangoHandler()
handler.install()
# instantiate client so hooks get registered
get_client() # NOQA
except Exception:
_initialized = False
return event_processor
class App(RavenConfig):
def ready(self):
initialize()
class PretixSentryIntegration(DjangoIntegration):
@staticmethod
def setup_once():
DjangoIntegration.setup_once()
from django.core.handlers.base import BaseHandler
old_get_response = BaseHandler.get_response
def sentry_patched_get_response(self, request):
hub = Hub.current
integration = hub.get_integration(DjangoIntegration)
if integration is not None:
with hub.configure_scope() as scope:
scope.add_event_processor(
_make_event_processor(weakref.ref(request), integration)
)
return old_get_response(self, request)
BaseHandler.get_response = sentry_patched_get_response
def ignore_retry(event, hint):
with capture_internal_exceptions():
if isinstance(hint["exc_info"][1], Retry):
return None
return event
def setup_custom_filters():
hub = Hub.current
with hub.configure_scope() as scope:
scope.add_event_processor(ignore_retry)

View File

@@ -1,6 +1,8 @@
import configparser
import logging
import os
import sys
from urllib.parse import urlparse
from kombu import Queue
@@ -90,6 +92,23 @@ DATABASES = {
} if 'mysql' in db_backend else {}
}
}
DATABASE_REPLICA = 'default'
if config.has_section('replica'):
DATABASE_REPLICA = 'replica'
DATABASES['replica'] = {
'ENGINE': 'django.db.backends.' + db_backend,
'NAME': config.get('replica', 'name', fallback=DATABASES['default']['NAME']),
'USER': config.get('replica', 'user', fallback=DATABASES['default']['USER']),
'PASSWORD': config.get('replica', 'password', fallback=DATABASES['default']['PASSWORD']),
'HOST': config.get('replica', 'host', fallback=DATABASES['default']['HOST']),
'PORT': config.get('replica', 'port', fallback=DATABASES['default']['PORT']),
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
'OPTIONS': db_options,
'TEST': {
'CHARSET': 'utf8mb4',
'COLLATION': 'utf8mb4_unicode_ci',
} if 'mysql' in db_backend else {}
}
STATIC_URL = config.get('urls', 'static', fallback='/static/')
@@ -564,15 +583,29 @@ LOGGING = {
}
if config.has_option('sentry', 'dsn'):
INSTALLED_APPS += [
'pretix.sentry.App',
]
RAVEN_CONFIG = {
'dsn': config.get('sentry', 'dsn'),
'transport': 'raven.transport.threaded_requests.ThreadedRequestsHTTPTransport',
'release': __version__,
'environment': SITE_URL,
}
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
from .sentry import PretixSentryIntegration, setup_custom_filters
sentry_sdk.init(
dsn=config.get('sentry', 'dsn'),
integrations=[
PretixSentryIntegration(),
CeleryIntegration(),
LoggingIntegration(
level=logging.INFO,
event_level=logging.CRITICAL
)
],
environment=SITE_URL,
release=__version__,
send_default_pii=False,
)
ignore_logger('pretix.base.tasks')
ignore_logger('django.security.DisallowedHost')
setup_custom_filters()
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

View File

@@ -1,4 +1,9 @@
.table-product-overview {
td:last-child, th:last-child {
border-right: 1px solid #ddd;
}
.item.categorized td:first-child {
padding-left: 20px;
}
@@ -9,7 +14,13 @@
padding-left: 40px;
}
td:not(:first-child),
th:not(:first-child) {
th:not(:first-child):not(.text-center) {
border-left: 1px solid #ddd;
padding-right: 10px !important;
padding-left: 10px !important;
}
td:not(:first-child),
th:not(:first-child):not(.text-center) {
text-align: right;
}
span.sum-net, span.sum-gross {
@@ -19,6 +30,14 @@
border-top: 2px solid #ddd;
}
}
@media(min-device-width: $screen-lg-min) {
.table-product-overview {
td:not(:first-child),
th:not(:first-child):not(.text-center) {
width: 150px;
}
}
}
#sumtoggle {
margin-top: 20px;

View File

@@ -29,14 +29,14 @@ csscompressor
django-markup
markdown<=2.2
bleach==2.*
raven
sentry-sdk==0.7.*
babel
django-i18nfield>=1.4.0
django-hijack>=2.1.10,<2.2.0
openpyxl
django-oauth-toolkit==1.2.*
oauthlib==2.1.*
django-jsonfallback
django-jsonfallback>=2.1.2
psycopg2-binary
# Stripe
stripe==2.0.*

View File

@@ -117,7 +117,7 @@ setup(
'django-markup',
'markdown<=2.2',
'bleach==2.*',
'raven',
'sentry-sdk==0.7.*',
'babel',
'paypalrestsdk==1.13.*',
'pycparser==2.13',
@@ -127,7 +127,7 @@ setup(
'chardet<3.1.0,>=3.0.2',
'mt-940==3.2',
'django-i18nfield>=1.4.0',
'django-jsonfallback',
'django-jsonfallback>=2.1.2',
'psycopg2-binary',
'vobject==0.9.*',
'pycountry',

View File

@@ -3,6 +3,7 @@ from decimal import Decimal
from unittest import mock
import pytest
from django.conf import settings
from django_countries.fields import Country
from pytz import UTC
@@ -150,7 +151,6 @@ def test_event_list(token_client, organizer, event):
def test_event_get(token_client, organizer, event):
resp = token_client.get('/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
print(resp.data)
assert TEST_EVENT_RES == resp.data
@@ -183,6 +183,7 @@ def test_event_create(token_client, organizer, event, meta_prop):
assert organizer.events.get(slug="2030").meta_values.filter(
property__name=meta_prop.name, value="Conference"
).exists()
assert organizer.events.get(slug="2030").plugins == settings.PRETIX_PLUGINS_DEFAULT
resp = token_client.post(
'/api/v1/organizers/{}/events/'.format(organizer.slug),

View File

@@ -678,6 +678,17 @@ class OrderChangeManagerTests(TestCase):
with self.assertRaises(OrderError):
self.ocm.change_item(self.op1, self.shirt, None)
def test_change_item_keep_price(self):
p = self.op1.price
tv = self.op1.tax_value
self.ocm.change_item(self.op1, self.shirt, None, keep_price=True)
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 == tv
def test_change_item_success(self):
self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.commit()
@@ -805,6 +816,14 @@ class OrderChangeManagerTests(TestCase):
self.op1.refresh_from_db()
assert self.op1.item == self.ticket
def test_quota_ignore(self):
q = self.event.quotas.create(name='Test', size=0)
q.items.add(self.shirt)
self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.commit(check_quotas=False)
self.op1.refresh_from_db()
assert self.op1.item == self.shirt
def test_quota_full_but_in_same(self):
q = self.event.quotas.create(name='Test', size=0)
q.items.add(self.shirt)

View File

@@ -1517,6 +1517,7 @@ def test_refund_paid_order_automatically_failed(client, env, monkeypatch):
p.info_data = {
'id': 'foo'
}
p.save()
p.confirm()
client.login(email='dummy@dummy.dummy', password='dummy')
@@ -1556,6 +1557,7 @@ def test_refund_paid_order_automatically(client, env, monkeypatch):
p.info_data = {
'id': 'foo'
}
p.save()
p.confirm()
client.login(email='dummy@dummy.dummy', password='dummy')