mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
9 Commits
subevent-e
...
v1.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae485a77d6 | ||
|
|
29e17d2284 | ||
|
|
6b95aca3f7 | ||
|
|
f227319f29 | ||
|
|
e63b85ccd6 | ||
|
|
bf3398e5f6 | ||
|
|
765d98464e | ||
|
|
66bdf9ab79 | ||
|
|
da830ef0a4 |
@@ -2,6 +2,7 @@
|
||||
cd /pretix/src
|
||||
export DJANGO_SETTINGS_MODULE=production_settings
|
||||
export DATA_DIR=/data/
|
||||
export HOME=/pretix
|
||||
NUM_WORKERS=10
|
||||
|
||||
if [ ! -d /data/logs ]; then
|
||||
|
||||
@@ -162,7 +162,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
-v /var/run/mysqld:/var/run/mysqld \
|
||||
pretix/standalone all
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -145,7 +145,7 @@ to get a better plain text representation of your text. Note however, that for
|
||||
security reasons you can only use the following HTML elements::
|
||||
|
||||
a, abbr, acronym, b, br, code, div, em, h1, h2,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, span, strong,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
|
||||
table, tbody, td, thead, tr, ul
|
||||
|
||||
Additionally, only the following attributes are allowed on them::
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.10.0"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
@@ -17,7 +17,18 @@ class CheckinList(LoggedModel):
|
||||
|
||||
@staticmethod
|
||||
def annotate_with_numbers(qs, event):
|
||||
from . import Order, OrderPosition
|
||||
"""
|
||||
Modifies a queryset of checkin lists by annotating it with the number of order positions and
|
||||
checkins associated with it.
|
||||
"""
|
||||
# Import here to prevent circular import
|
||||
from . import Order, OrderPosition, Item
|
||||
|
||||
# This is the mother of all subqueries. Sorry. I try to explain it, at least?
|
||||
# First, we prepare a subquery that for every check-in that belongs to a paid-order
|
||||
# position and to the list in question. Then, we check that it also belongs to the
|
||||
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
|
||||
# since we filtered by lists).
|
||||
cqs = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status=Order.STATUS_PAID,
|
||||
@@ -30,6 +41,11 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now for the hard part: getting all order positions that contribute to this list. This
|
||||
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
|
||||
# lists that contain all the products of the event. This is the simpler one, it basically
|
||||
# looks like the check-in counter above.
|
||||
pqs_all = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
@@ -41,10 +57,16 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now we need a subquery for the case of checkin lists that are limited to certain
|
||||
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
|
||||
# with the products table and we'd get duplicate rows in the output with different annotations
|
||||
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
|
||||
# to retrieve all of those items and then check if the item_id is IN this subquery result.
|
||||
pqs_limited = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
item__in=OuterRef('limit_products')
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
@@ -54,6 +76,9 @@ class CheckinList(LoggedModel):
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
|
||||
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
|
||||
# we want to display a progress bar.
|
||||
return qs.annotate(
|
||||
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
|
||||
position_count=Coalesce(Case(
|
||||
|
||||
@@ -769,6 +769,9 @@ class OrderPosition(AbstractPosition):
|
||||
'order_code': order.code
|
||||
})
|
||||
|
||||
# Delete afterwards. Deleting in between might cause deletion of things related to add-ons
|
||||
# due to the deletion cascade.
|
||||
for cartpos in cp:
|
||||
cartpos.delete()
|
||||
return ops
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ ALLOWED_TAGS = [
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'pre',
|
||||
# Update doc/user/markdown.rst if you change this!
|
||||
]
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@ class FilterForm(forms.Form):
|
||||
required=False
|
||||
)
|
||||
|
||||
def get_order_by(self):
|
||||
o = self.cleaned_data.get('ordering')
|
||||
if o.startswith('-'):
|
||||
return '-' + self.orders[o[1:]]
|
||||
else:
|
||||
return self.orders[o]
|
||||
|
||||
|
||||
class OrderFilterForm(FilterForm):
|
||||
query = forms.CharField(
|
||||
@@ -139,7 +146,7 @@ class OrderFilterForm(FilterForm):
|
||||
qs = qs.filter(status=s)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
@@ -276,7 +283,7 @@ class SubEventFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
return qs
|
||||
|
||||
@@ -309,7 +316,7 @@ class OrganizerFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
return qs
|
||||
|
||||
@@ -390,7 +397,7 @@ class EventFilterForm(FilterForm):
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.orders[fdata.get('ordering')])
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -192,14 +192,6 @@ def shop_state_widget(sender, **kwargs):
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def checkin_widget(sender, subevent=None, **kwargs):
|
||||
size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p')
|
||||
checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False)
|
||||
|
||||
# if this setting is False, we check only items for admission
|
||||
if not sender.settings.ticket_download_nonadm:
|
||||
size_qs = size_qs.filter(item__admission=True)
|
||||
checked_qs = checked_qs.filter(item__admission=True)
|
||||
|
||||
widgets = []
|
||||
qs = sender.checkin_lists.filter(subevent=subevent)
|
||||
qs = CheckinList.annotate_with_numbers(qs, sender)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,8 @@ libsass
|
||||
django-otp==0.3.*
|
||||
python-u2flib-server==4.*
|
||||
django-formtools==2.0
|
||||
celery==4.0.2
|
||||
kombu==4.0.2
|
||||
celery==4.1.*
|
||||
kombu==4.1.*
|
||||
django-statici18n==1.3.*
|
||||
inlinestyler==0.2.*
|
||||
BeautifulSoup4
|
||||
|
||||
@@ -81,8 +81,8 @@ setup(
|
||||
'django-otp==0.3.*',
|
||||
'python-u2flib-server==4.*',
|
||||
'django-formtools==2.0',
|
||||
'celery==4.0.2',
|
||||
'kombu==4.0.2',
|
||||
'celery==4.1.*',
|
||||
'kombu==4.1.*',
|
||||
'django-statici18n==1.3.*',
|
||||
'inlinestyler==0.2.*',
|
||||
'BeautifulSoup4',
|
||||
|
||||
@@ -13,8 +13,9 @@ from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import (
|
||||
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
|
||||
OrderPosition, Organizer, Question, Quota, User, Voucher, WaitingListEntry,
|
||||
CachedFile, CartPosition, CheckinList, Event, Item, ItemCategory,
|
||||
ItemVariation, Order, OrderPosition, Organizer, Question, Quota, User,
|
||||
Voucher, WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
@@ -1066,3 +1067,66 @@ class CachedFileTestCase(TestCase):
|
||||
assert f.read().strip() == "file_content"
|
||||
cf.delete()
|
||||
assert not default_storage.exists(cf.file.name)
|
||||
|
||||
|
||||
class CheckinListTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
cls.event = Event.objects.create(
|
||||
organizer=cls.organizer, name='Dummy', slug='dummy',
|
||||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||||
)
|
||||
cls.item1 = cls.event.items.create(name="Ticket", default_price=12)
|
||||
cls.item2 = cls.event.items.create(name="Shirt", default_price=6)
|
||||
cls.cl_all = cls.event.checkin_lists.create(
|
||||
name='All', all_products=True
|
||||
)
|
||||
cls.cl_both = cls.event.checkin_lists.create(
|
||||
name='Both', all_products=False
|
||||
)
|
||||
cls.cl_both.limit_products.add(cls.item1)
|
||||
cls.cl_both.limit_products.add(cls.item2)
|
||||
cls.cl_tickets = cls.event.checkin_lists.create(
|
||||
name='Tickets', all_products=False
|
||||
)
|
||||
cls.cl_tickets.limit_products.add(cls.item1)
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=cls.event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PAID,
|
||||
datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=Decimal("30"), payment_provider='banktransfer', locale='en'
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=cls.item1,
|
||||
variation=None,
|
||||
price=Decimal("12"),
|
||||
)
|
||||
op2 = OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=cls.item1,
|
||||
variation=None,
|
||||
price=Decimal("12"),
|
||||
)
|
||||
op3 = OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=cls.item2,
|
||||
variation=None,
|
||||
price=Decimal("6"),
|
||||
)
|
||||
op2.checkins.create(list=cls.cl_tickets)
|
||||
op3.checkins.create(list=cls.cl_both)
|
||||
|
||||
def test_annotated(self):
|
||||
lists = list(CheckinList.annotate_with_numbers(self.event.checkin_lists.order_by('name'), self.event))
|
||||
assert lists == [self.cl_all, self.cl_both, self.cl_tickets]
|
||||
assert lists[0].checkin_count == 0
|
||||
assert lists[0].position_count == 3
|
||||
assert lists[0].percent == 0
|
||||
assert lists[1].checkin_count == 1
|
||||
assert lists[1].position_count == 3
|
||||
assert lists[1].percent == 33
|
||||
assert lists[2].checkin_count == 1
|
||||
assert lists[2].position_count == 2
|
||||
assert lists[2].percent == 50
|
||||
|
||||
@@ -73,6 +73,33 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
def test_addon_questions(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
required=True
|
||||
)
|
||||
q1.items.add(self.ticket)
|
||||
q1.items.add(self.workshop1)
|
||||
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
|
||||
price_included=True)
|
||||
cp1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cp1.answers.create(question=q1, answer='12')
|
||||
cp2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.workshop1, addon_to=cp1,
|
||||
price=0, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cp2.answers.create(question=q1, answer='12')
|
||||
|
||||
self._set_session('payment', 'banktransfer')
|
||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
self.assertEqual(OrderPosition.objects.filter(item=self.ticket).first().answers.first().answer, '12')
|
||||
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).first().answers.first().answer, '12')
|
||||
|
||||
def test_questions(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
|
||||
Reference in New Issue
Block a user