Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
90b5d721cb Run pytest with -v 2020-04-22 12:24:56 +02:00
235 changed files with 44953 additions and 57331 deletions

View File

@@ -4,12 +4,10 @@ on:
push:
branches: [ master ]
paths:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths:
- 'doc/**'
- 'src/pretix/locale/**'
jobs:

View File

@@ -5,12 +5,10 @@ on:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
jobs:
test:
@@ -66,7 +64,7 @@ jobs:
run: make all compress
- name: Run tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -v -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
- name: Upload coverage
uses: codecov/codecov-action@v1
with:

View File

@@ -92,7 +92,7 @@ Example::
``trust_x_forwarded_proto``
Specifies whether the ``X-Forwarded-Proto`` header can be trusted. Only set to ``on`` if you have a reverse
proxy that actively removes and re-adds the header to make sure the correct value is set.
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
Defaults to ``off``.

View File

@@ -30,9 +30,6 @@ position_count integer Number of ticke
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -51,11 +48,6 @@ rules object Custom check-in
The ``auto_checkin_sales_channels`` field has been added.
.. versionchanged:: 3.9
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
``allow_entry_after_exit``, and ``rules`` attributes have been added.
Endpoints
---------
@@ -97,9 +89,6 @@ Endpoints
"limit_products": [],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"rules": {},
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -144,9 +133,6 @@ Endpoints
"limit_products": [],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"rules": {},
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -243,8 +229,6 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -267,8 +251,6 @@ Endpoints
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -321,8 +303,6 @@ Endpoints
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -716,7 +696,6 @@ Order position endpoints
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
* ``already_redeemed`` - Ticket already has been redeemed
* ``product`` - Tickets with this product may not be scanned at this device
* ``rules`` - Check-in prevented by a user-defined rule
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch

View File

@@ -195,7 +195,6 @@ pseudonymization_id string A random ID, e.
checkins list of objects List of check-ins with this ticket
├ list integer Internal ID of the check-in list
├ datetime datetime Time of check-in
├ type string Type of scan (defaults to ``entry``)
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
@@ -252,10 +251,6 @@ pdf_data object Data object req
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
.. versionchanged:: 3.9
The ``checkin.type`` attribute has been added.
.. _order-payment-resource:
Order payment resource
@@ -418,7 +413,6 @@ List of all orders
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -581,7 +575,6 @@ Fetching individual orders
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -1478,7 +1471,6 @@ List of all order positions
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -1584,7 +1576,6 @@ Fetching individual positions
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}

View File

@@ -70,9 +70,6 @@ and ``checkin_list``.
only include the minimum amount of data necessary for you to fetch the changed objects from our
:ref:`rest-api` in an authenticated way.
.. warning:: In very rare cases, you could receive the same webhook notification twice. We try to avoid it, but we
prefer it over missing a notification.
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,94 +0,0 @@
.. _timeslots:
Use case: Time slots
====================
A more advanced use case of pretix is using pretix for time-slot-based access to an area with a limited visitor
capacity, such as a museum or other attraction. This guide will show you the quickest way to set up such an event
with pretix.
First of all, when creating your event, you need to select that your event represents an "event series":
.. thumbnail:: ../../../screens/event/create_step1.png
:align: center
:class: screenshot
You can click :ref:`here <subevents>` for a more general description of event series with pretix, but everything you
need to know is in this chapter as well.
General event setup
-------------------
Before you go further, set up your products that you want to sell for each time slot, such as different types of entry.
Creating slots
--------------
To create the time slots, you need to create a number of "dates" in the event series. Select "Dates" in the navigation
menu on the left side and click "Create many new dates". Then, first enter the pattern of your opening days. In the
example, the museum is open week Tuesday to Sunday. We recommend to create the slots for a few weeks at a time, but not
e.g. for a full year, since it will be more complicated to change things later.
.. thumbnail:: ../../../screens/event/timeslots_create.png
:align: center
:class: screenshot
Then, scroll to the times section and create your time slots. You can do any interval you like. If you have different
opening times on different week days, you will need to go through the creation process multiple times.
.. thumbnail:: ../../../screens/event/timeslots_create_2.png
:align: center
:class: screenshot
Scroll further down and create one or multiple quotas that define how many people can book a ticket for that time slot.
In this example, 50 people in total are allowed to enter within every slot:
.. thumbnail:: ../../../screens/event/timeslots_create_3.png
:align: center
:class: screenshot
Do **not** create a check-in list at this point. We will deal with this further below in the guide.
Now, press "Save" to create your slots.
.. warning:: If you create a lot of time slots at once, the server might need a few minutes to create them all in our
system. If you receive an error page because it took too long, please do not try again immediately but wait
for a few minutes. Most likely, the slots will be created successfully even though you saw an error.
Event settings
--------------
We recommend that you navigate to "Settings" > "General" > "Display" and set the settings "Default overview style"
to "Week calendar":
.. thumbnail:: ../../../screens/event/timeslots_settings_1.png
:align: center
:class: screenshot
Now, your ticket shop should give users a nice weekly overview over all time slots and their availability:
.. thumbnail:: ../../../screens/event/timeslots_presale.png
:align: center
:class: screenshot
Check-in
--------
If you want to scan people at the entrance of your event and only admit them at their designated time, we recommend
the following setup: Go to "Check-in" in the main navigation on the left and create a new check-in list. Give it a name
and do *not* choose a specific data. We will use one check-in list for all dates. Then, go to the "Advanced" tab at
the top and set up two restrictions to make sure people can only get in during the time slot they registered for.
You can create the rules exactly like shown in the following screenshot:
.. thumbnail:: ../../../screens/event/timeslots_checkinlists.png
:align: center
:class: screenshot
If you want, you can enter a tolerance of e.g. "10" if you want to be a little bit more relaxed and admit people up to
10 minutes before or after their time slot.
Now, download our `Android or Desktop app`_ and register it to your account. The app will ask you to select one the
time slots, but it does not matter, you can select any one of them and then select your newly created check-in list.
That's it, you're good to go!
.. _Android or Desktop app: https://pretix.eu/about/en/scan

View File

@@ -344,13 +344,3 @@ In addition to your normal conference quota, you need to create an unlimited quo
Then, head to the **Bundled products** tab of the "conference ticket" and add the "conference food" as a bundled product with a **designated price** of € 150.
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
You can find more use cases in these specialized guides:
More use cases
--------------
.. toctree::
:maxdepth: 1
guides/timeslots

View File

@@ -136,15 +136,10 @@ If you want to include all your public events, you can just reference your organ
<pretix-widget event="https://pretix.eu/demo/"></pretix-widget>
There is an optional ``style`` parameter that let's you choose between a monthly calendar view, a week view and a list
view. If you do not set it, the choice will be taken from your organizer settings::
There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
<pretix-widget event="https://pretix.eu/demo/series/" style="list"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="week"></pretix-widget>
If you have more than 100 events, the system might refuse to show a list view and always show a calendar for performance
reasons instead.
You can see an example here:

View File

@@ -58,6 +58,28 @@ method without creating a new order. If payment deadlines were dependent on the
forth could either allow someone to extend their deadline forever, or render someones order invalid by moving the date
back in the past.
How can I revert a check-in?
----------------------------
Neither our apps nor our web interface can currently undo the check-in of a tickets. We know that this is
inconvenient for some of you, but we have a good reason for it:
Our Desktop and Android apps both support an asynchronous mode in which they can scan tickets while staying
independent of their internet connection. When scanning with multiple devices, it can of course happen that two
devices scan the same ticket without knowing of the other scan. As soon as one of the devices regains connectivity, it
will upload its activity and the server marks the ticket as checked in -- regardless of the order in which the two
scans were made and uploaded (which could be two different orders).
If we'd provide a "check out" feature, it would not only be used to fix an accidental scan, but scan at entry and
exit to count the current number of people inside etc. In this case, the order of operations matters very much for them
to make sense and provide useful results. This makes implementing an asynchronous mode much more complicated.
In this trade off, we chose offline-capabilities over the check out feature. We plan on solving this problem in the
future, but we're not there yet.
If you're just *testing* the check-in capabilities and want to clean out everything for the real process, you can just
delete and re-create the check-in list.
Why does pretix not support any 1D (linear) bar codes?
------------------------------------------------------

View File

@@ -1,8 +1,8 @@
General settings
================
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The "Deadline"
and "Advanced" tabs of the page show a number of general settings that affect all payment methods:
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The upper part
of the page shows a number of general settings that affect all payment methods:
.. thumbnail:: ../../screens/event/settings_payment.png
:align: center

View File

@@ -1 +1 @@
__version__ = "3.9.0"
__version__ = "3.9.0.dev0"

View File

@@ -14,8 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules')
'include_pending', 'auto_checkin_sales_channels')
def validate(self, data):
data = super().validate(data)
@@ -29,7 +28,9 @@ class CheckinListSerializer(I18nAwareModelSerializer):
raise ValidationError(_('One or more items do not belong to this event.'))
if event.has_subevents:
if full_data.get('subevent') and event != full_data.get('subevent').event:
if not full_data.get('subevent'):
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != full_data.get('subevent').event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if full_data.get('subevent'):

View File

@@ -555,7 +555,6 @@ class EventSettingsSerializer(serializers.Serializer):
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'event_list_type',
'frontpage_text',
'attendee_names_asked',
'attendee_names_required',

View File

@@ -122,7 +122,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list', 'auto_checked_in', 'type')
fields = ('datetime', 'list', 'auto_checked_in')
class OrderDownloadsField(serializers.Field):

View File

@@ -41,8 +41,8 @@ class ConditionalListView:
return super().list(request, **kwargs)
lmd = request.event.logentry_set.filter(
content_type__model=self.get_queryset().model._meta.model_name,
content_type__app_label=self.get_queryset().model._meta.app_label,
content_type__model=self.queryset.model._meta.model_name,
content_type__app_label=self.queryset.model._meta.app_label,
).aggregate(
m=Max('datetime')
)['m']

View File

@@ -88,9 +88,8 @@ class CheckinListViewSet(viewsets.ModelViewSet):
pqs = OrderPosition.objects.filter(
order__event=clist.event,
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
subevent=clist.subevent,
)
if clist.subevent:
pqs = pqs.filter(subevent=clist.subevent)
if not clist.all_products:
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
@@ -202,13 +201,10 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
)
if self.checkinlist.subevent:
qs = qs.filter(
subevent=self.checkinlist.subevent
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
@@ -255,9 +251,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
@action(detail=True, methods=['POST'])
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
if type not in dict(Checkin.CHECKIN_TYPES):
raise ValidationError("Invalid check-in type.")
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object(ignore_status=True)
@@ -290,7 +283,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
canceled_supported=self.request.data.get('canceled_supported', False),
user=self.request.user,
auth=self.request.auth,
type=type,
)
except RequiredQuestionsError as e:
return Response({

View File

@@ -20,7 +20,6 @@ from pretix.base.models import (
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
@@ -534,17 +533,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
def availability(self, request, *args, **kwargs):
quota = self.get_object()
qa = QuotaAvailability()
qa.queue(quota)
qa.compute()
avail = qa.results[quota]
avail = quota.availability()
data = {
'paid_orders': qa.count_paid_orders[quota],
'pending_orders': qa.count_pending_orders[quota],
'blocking_vouchers': qa.count_vouchers[quota],
'cart_positions': qa.count_cart[quota],
'waiting_list': qa.count_pending_orders[quota],
'paid_orders': quota.count_paid_orders(),
'pending_orders': quota.count_pending_orders(),
'blocking_vouchers': quota.count_blocking_vouchers(),
'cart_positions': quota.count_in_cart(),
'waiting_list': quota.count_waiting_list_pending(),
'available_number': avail[1],
'available': avail[0] == Quota.AVAILABILITY_OK,
'total_size': quota.size,

View File

@@ -168,9 +168,9 @@ def register_default_webhook_events(sender, **kwargs):
)
@app.task(base=TransactionAwareTask, acks_late=True)
@app.task(base=TransactionAwareTask)
def notify_webhooks(logentry_id: int):
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.organizer:
return # We need to know the organizer
@@ -205,7 +205,7 @@ def notify_webhooks(logentry_id: int):
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
with scopes_disabled():

View File

@@ -14,7 +14,6 @@ from pretix.base.models import (
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import ListExporter, MultiSheetListExporter
@@ -335,9 +334,7 @@ class OrderListExporter(MultiSheetListExporter):
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers += [
_('Sales channel'), _('Order locale'),
]
headers.append(_('Sales channel'))
yield headers
@@ -420,10 +417,7 @@ class OrderListExporter(MultiSheetListExporter):
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.sales_channel,
order.locale
]
row.append(order.sales_channel)
yield row
def get_filename(self):
@@ -438,21 +432,10 @@ class PaymentListExporter(ListExporter):
def additional_form_fields(self):
return OrderedDict(
[
('payment_states',
forms.MultipleChoiceField(
label=_('Payment states'),
choices=OrderPayment.PAYMENT_STATES,
initial=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED],
required=False,
widget=forms.CheckboxSelectMultiple,
)),
('refund_states',
forms.MultipleChoiceField(
label=_('Refund states'),
choices=OrderRefund.REFUND_STATES,
initial=[OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_CREATED,
OrderRefund.REFUND_STATE_TRANSIT],
widget=forms.CheckboxSelectMultiple,
('successful_only',
forms.BooleanField(
label=_('Only successful payments'),
initial=True,
required=False
)),
]
@@ -468,13 +451,19 @@ class PaymentListExporter(ListExporter):
payments = OrderPayment.objects.filter(
order__event=self.event,
state__in=form_data.get('payment_states', [])
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
state__in=form_data.get('refund_states', [])
order__event=self.event
).order_by('created')
if form_data['successful_only']:
payments = payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
)
refunds = refunds.filter(
state=OrderRefund.REFUND_STATE_DONE,
)
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
headers = [
@@ -517,21 +506,16 @@ class QuotaListExporter(ListExporter):
]
yield headers
quotas = list(self.event.quotas.all())
qa = QuotaAvailability(full_results=True)
qa.queue(*quotas)
qa.compute()
for quota in quotas:
avail = qa.results[quota]
for quota in self.event.quotas.all():
avail = quota.availability()
row = [
quota.name,
_('Infinite') if quota.size is None else quota.size,
qa.count_paid_orders[quota],
qa.count_pending_orders[quota],
qa.count_vouchers[quota],
qa.count_cart[quota],
qa.count_waitinglist[quota],
quota.count_paid_orders(),
quota.count_pending_orders(),
quota.count_blocking_vouchers(),
quota.count_in_cart(),
quota.count_waiting_list_pending(),
_('Infinite') if avail[1] is None else avail[1]
]
yield row

View File

@@ -48,9 +48,6 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
super().__init__(*args, **kwargs)
SECRET_REDACTED = '*****'
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
auto_fields = []
@@ -76,12 +73,6 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)
def save(self):
for k, v in self.cleaned_data.items():
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
self.cleaned_data[k] = self.initial[k]
return super().save()
def get_new_filename(self, name: str) -> str:
from pretix.base.models import Event
@@ -120,32 +111,3 @@ class SafeSessionWizardView(SessionWizardView):
}
)
return context
class SecretKeySettingsWidget(forms.TextInput):
def __init__(self, attrs=None):
if attrs is None:
attrs = {}
attrs.update({
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
})
super().__init__(attrs)
def get_context(self, name, value, attrs):
if value:
value = SECRET_REDACTED
return super().get_context(name, value, attrs)
class SecretKeySettingsField(forms.CharField):
widget = SecretKeySettingsWidget
def has_changed(self, initial, data):
if data == SECRET_REDACTED:
return False
return super().has_changed(initial, data)
def run_validators(self, value):
if value == SECRET_REDACTED:
return
return super().run_validators(value)

View File

@@ -40,7 +40,7 @@ from pretix.base.settings import (
PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.countries import CachedCountries
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.i18n import get_format_without_seconds
@@ -205,13 +205,12 @@ def guess_country(event):
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
# TODO: does this actually work?
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale.upper() in valid_countries:
if locale in valid_countries:
country = Country(locale.upper())
return country
@@ -413,17 +412,11 @@ class BaseQuestionsForm(forms.Form):
initial=initial.options.all() if initial else None,
)
elif q.type == Question.TYPE_FILE:
field = ExtFileField(
field = forms.FileField(
label=label, required=required,
help_text=help_text,
initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
ext_whitelist=(
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
".bmp", ".tif", ".tiff"
),
max_size=10 * 1024 * 1024,
)
elif q.type == Question.TYPE_DATE:
field = forms.DateField(

View File

@@ -672,7 +672,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table
]))
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
pgettext(
@@ -680,7 +679,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
total=fmt(foreign_total)),
total=fmt(total)),
self.stylesheet['Fineprint']
))

View File

@@ -108,7 +108,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
@@ -274,7 +274,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
],
options={
'verbose_name': 'Organizer',

View File

@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
@@ -229,7 +229,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
],
options={
'ordering': ('name',),

View File

@@ -182,12 +182,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -23,12 +23,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -124,7 +124,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.RunPython(
code=merge_names,

View File

@@ -342,7 +342,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True,
field=models.SlugField(
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),

View File

@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',

View File

@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.RunPython(merge_names, migrations.RunPython.noop)
]

View File

@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',

View File

@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',

View File

@@ -97,7 +97,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True,
field=models.SlugField(
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be '
'unique among your events. We recommend some kind of abbreviation or a date with less than '
'10 characters that can be easily remembered, but you can also choose to use a random '
@@ -119,7 +119,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True,
field=models.SlugField(
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can '
'only be used once. This is being used in URLs to refer to your organizer accounts and your'
' events.',

View File

@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -1,52 +0,0 @@
# Generated by Django 3.0.5 on 2020-05-11 15:04
import django.db.models.deletion
import django_countries.fields
import jsonfallback.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0151_auto_20200421_0737'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='device',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.Device'),
),
migrations.AddField(
model_name='checkin',
name='forced',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkin',
name='type',
field=models.CharField(default='entry', max_length=100),
),
migrations.AddField(
model_name='checkinlist',
name='allow_entry_after_exit',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='checkinlist',
name='allow_multiple_entries',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkinlist',
name='rules',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterUniqueTogether(
name='checkin',
unique_together=set(),
),
]

View File

@@ -1,31 +0,0 @@
# Generated by Django 3.0.6 on 2020-05-28 19:53
import django_countries.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0152_auto_20200511_1504'),
]
operations = [
migrations.AddField(
model_name='seat',
name='x',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='seat',
name='y',
field=models.FloatField(null=True),
),
migrations.AlterField(
model_name='seat',
name='seat_guid',
field=models.CharField(db_index=True, max_length=190),
),
]

View File

@@ -1,9 +1,8 @@
from django.db import models
from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
from django.db.models import Exists, OuterRef
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from jsonfallback.fields import FallbackJSONField
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
@@ -20,15 +19,6 @@ class CheckinList(LoggedModel):
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order have not been paid.'))
allow_entry_after_exit = models.BooleanField(
verbose_name=_('Allow re-entering after an exit scan'),
default=True
)
allow_multiple_entries = models.BooleanField(
verbose_name=_('Allow multiple entries per ticket'),
help_text=_('Use this option to turn off warnings if a ticket is scanned a second time.'),
default=False
)
auto_checkin_sales_channels = MultiStringField(
default=[],
@@ -38,7 +28,6 @@ class CheckinList(LoggedModel):
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.')
)
rules = FallbackJSONField(default=dict, blank=True)
objects = ScopedManager(organizer='event__organizer')
@@ -51,43 +40,13 @@ class CheckinList(LoggedModel):
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [
Order.STATUS_PAID],
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
subevent=self.subevent
)
if self.subevent_id:
qs = qs.filter(subevent_id=self.subevent_id)
if not self.all_products:
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
return qs
@property
def inside_count(self):
return self.positions.annotate(
last_entry=Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_ENTRY,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
last_exit=Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_EXIT,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
).filter(
Q(last_entry__isnull=False)
& Q(
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
)
).count()
@property
def checkin_count(self):
return self.event.cache.get_or_set(
@@ -129,31 +88,20 @@ class CheckinList(LoggedModel):
class Checkin(models.Model):
"""
A check-in object is created when a person enters or exits the event.
A check-in object is created when a person enters the event.
"""
TYPE_ENTRY = 'entry'
TYPE_EXIT = 'exit'
CHECKIN_TYPES = (
(TYPE_ENTRY, _('Entry')),
(TYPE_EXIT, _('Exit')),
)
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins', on_delete=models.CASCADE)
datetime = models.DateTimeField(default=now)
nonce = models.CharField(max_length=190, null=True, blank=True)
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
forced = models.BooleanField(default=False)
device = models.ForeignKey(
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
)
auto_checked_in = models.BooleanField(default=False)
objects = ScopedManager(organizer='position__order__event__organizer')
class Meta:
ordering = (('-datetime'),)
unique_together = (('list', 'position'),)
def __repr__(self):
return "<Checkin: pos {} on list '{}' at {}>".format(
@@ -161,12 +109,12 @@ class Checkin(models.Model):
)
def save(self, **kwargs):
super().save(**kwargs)
self.position.order.touch()
self.list.event.cache.delete('checkin_count')
self.list.touch()
super().save(**kwargs)
def delete(self, **kwargs):
super().delete(**kwargs)
self.position.order.touch()
super().delete(**kwargs)
self.list.touch()

View File

@@ -12,7 +12,7 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
@@ -214,10 +214,8 @@ class EventMixin:
vars_reserved = set()
items_gone = set()
vars_gone = set()
r = getattr(self, '_quota_cache', {})
for q in self.active_quotas:
res = r[q] if q in r else q.availability(allow_cache=True)
res = q.availability(allow_cache=True)
if res[0] == Quota.AVAILABILITY_OK:
if q.active_items:
@@ -287,7 +285,7 @@ class Event(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Event name"),
)
slug = models.CharField(
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your "
@@ -391,17 +389,36 @@ class Event(EventMixin, LoggedModel):
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .seating import Seat
qs_annotated = Seat.annotated(self.seats, self.pk, None,
ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None,
minimal_distance=self.settings.seating_minimal_distance,
distance_only_within_row=self.settings.seating_distance_within_row)
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
event=self,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher:
vqs = vqs.exclude(pk=ignore_voucher.pk)
qs = self.seats.annotate(
has_order=Exists(
OrderPosition.objects.filter(
order__event=self,
seat_id=OuterRef('pk'),
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
)
),
has_cart=Exists(
CartPosition.objects.filter(
event=self,
seat_id=OuterRef('pk'),
expires__gte=now()
)
),
has_voucher=Exists(
vqs
)
).filter(has_order=False, has_cart=False, has_voucher=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@@ -608,29 +625,12 @@ class Event(EventMixin, LoggedModel):
q.dependency_question = question_map[q.dependency_question_id]
q.save(update_fields=['dependency_question'])
def _walk_rules(rules):
if isinstance(rules, dict):
for k, v in rules.items():
if k == 'lookup':
if v[0] == 'product':
v[1] = str(item_map.get(int(v[1]), 0).pk)
elif v[0] == 'variation':
v[1] = str(variation_map.get(int(v[1]), 0).pk)
else:
_walk_rules(v)
elif isinstance(rules, list):
for i in rules:
_walk_rules(i)
checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
items = list(cl.limit_products.all())
checkin_list_map[cl.pk] = cl
cl.pk = None
cl.event = self
rules = cl.rules
_walk_rules(rules)
cl.rules = rules
cl.save()
cl.log_action('pretix.object.cloned')
for i in items:
@@ -1042,15 +1042,39 @@ class SubEvent(EventMixin, LoggedModel):
).strip()
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .seating import Seat
qs_annotated = Seat.annotated(self.seats, self.event_id, self,
ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None,
minimal_distance=self.settings.seating_minimal_distance,
distance_only_within_row=self.settings.seating_distance_within_row)
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher:
vqs = vqs.exclude(pk=ignore_voucher.pk)
qs = self.seats.annotate(
has_order=Exists(
OrderPosition.objects.filter(
order__event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
)
),
has_cart=Exists(
CartPosition.objects.filter(
event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
expires__gte=now()
)
),
has_voucher=Exists(
vqs
)
).filter(has_order=False, has_cart=False, has_voucher=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@@ -1091,15 +1115,13 @@ class SubEvent(EventMixin, LoggedModel):
return not self.orderposition_set.exists()
def delete(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', False)
super().delete(*args, **kwargs)
if self.event and clear_cache:
if self.event:
self.event.cache.clear()
def save(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', False)
super().save(*args, **kwargs)
if self.event and clear_cache:
if self.event:
self.event.cache.clear()
@staticmethod

View File

@@ -7,10 +7,11 @@ from typing import Tuple
import dateutil.parser
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Q
from django.db.models import F, Func, Q, Sum
from django.utils import formats
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -24,6 +25,7 @@ from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.signals import quota_availability
from .event import Event, SubEvent
@@ -533,8 +535,6 @@ class Item(LoggedModel):
quotacounter[q] += b.count
for q, n in quotacounter.items():
if n == 0:
continue
a = q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
if a[1] is None:
continue
@@ -1350,7 +1350,6 @@ class Quota(LoggedModel):
self.event.cache.clear()
def save(self, *args, **kwargs):
# This is *not* called when the db-level cache is upated, since we use bulk_update there
clear_cache = kwargs.pop('clear_cache', True)
super().save(*args, **kwargs)
if self.event and clear_cache:
@@ -1385,8 +1384,6 @@ class Quota(LoggedModel):
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
and the second is the number of available tickets.
"""
from ..services.quotas import QuotaAvailability
if allow_cache and self.cache_is_hot() and count_waitinglist:
return self.cached_availability_state, self.cached_availability_number
@@ -1395,16 +1392,141 @@ class Quota(LoggedModel):
if _cache is not None and self.pk in _cache:
return _cache[self.pk]
qa = QuotaAvailability(count_waitinglist=count_waitinglist, early_out=False)
qa.queue(self)
qa.compute(now_dt=now_dt)
res = qa.results[self]
now_dt = now_dt or now()
res = self._availability(now_dt, count_waitinglist)
for recv, resp in quota_availability.send(sender=self.event, quota=self, result=res,
count_waitinglist=count_waitinglist):
res = resp
if res[0] <= Quota.AVAILABILITY_ORDERED and self.close_when_sold_out and not self.closed:
self.closed = True
self.save(update_fields=['closed'])
self.log_action('pretix.event.quota.closed')
self.event.cache.delete('item_quota_cache')
rewrite_cache = count_waitinglist and (
not self.cache_is_hot(now_dt) or res[0] > self.cached_availability_state
)
if rewrite_cache:
self.cached_availability_state = res[0]
self.cached_availability_number = res[1]
self.cached_availability_time = now_dt
if self.size is None:
self.cached_availability_paid_orders = self.count_paid_orders()
self.save(
update_fields=[
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
'cached_availability_paid_orders'
],
clear_cache=False,
using='default'
)
if _cache is not None:
_cache[self.pk] = res
_cache['_count_waitinglist'] = count_waitinglist
return res
def _availability(self, now_dt: datetime=None, count_waitinglist=True, ignore_closed=False):
now_dt = now_dt or now()
if self.closed and not ignore_closed:
return Quota.AVAILABILITY_ORDERED, 0
size_left = self.size
if size_left is None:
return Quota.AVAILABILITY_OK, None
paid_orders = self.count_paid_orders()
self.cached_availability_paid_orders = paid_orders
size_left -= paid_orders
if size_left <= 0:
return Quota.AVAILABILITY_GONE, 0
size_left -= self.count_pending_orders()
if size_left <= 0:
return Quota.AVAILABILITY_ORDERED, 0
size_left -= self.count_blocking_vouchers(now_dt)
if size_left <= 0:
return Quota.AVAILABILITY_ORDERED, 0
if count_waitinglist:
size_left -= self.count_waiting_list_pending()
if size_left <= 0:
return Quota.AVAILABILITY_ORDERED, 0
size_left -= self.count_in_cart(now_dt)
if size_left <= 0:
return Quota.AVAILABILITY_RESERVED, 0
return Quota.AVAILABILITY_OK, size_left
def count_blocking_vouchers(self, now_dt: datetime=None) -> int:
from pretix.base.models import Voucher
now_dt = now_dt or now()
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
func = 'MAX'
else: # NOQA
func = 'GREATEST'
return Voucher.objects.filter(
Q(event=self.event) & Q(subevent=self.subevent) &
Q(block_quota=True) &
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
Q(Q(self._position_lookup) | Q(quota=self))
).values('id').aggregate(
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
)['free'] or 0
def count_waiting_list_pending(self) -> int:
from pretix.base.models import WaitingListEntry
return WaitingListEntry.objects.filter(
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
self._position_lookup
).distinct().count()
def count_in_cart(self, now_dt: datetime=None) -> int:
from pretix.base.models import CartPosition
now_dt = now_dt or now()
return CartPosition.objects.filter(
Q(event=self.event) & Q(subevent=self.subevent) &
Q(expires__gte=now_dt) &
Q(
Q(voucher__isnull=True)
| Q(voucher__block_quota=False)
| Q(voucher__valid_until__lt=now_dt)
) &
self._position_lookup
).count()
def count_pending_orders(self) -> dict:
from pretix.base.models import Order, OrderPosition
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
).count()
def count_paid_orders(self):
from pretix.base.models import Order, OrderPosition
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
).count()
@cached_property
def _position_lookup(self) -> Q:
return (
( # Orders for items which do not have any variations
Q(variation__isnull=True) &
Q(item_id__in=Quota.items.through.objects.filter(quota_id=self.pk).values_list('item_id', flat=True))
) | ( # Orders for items which do have any variations
Q(variation__in=Quota.variations.through.objects.filter(quota_id=self.pk).values_list('itemvariation_id', flat=True))
)
)
class QuotaExceededException(Exception):
pass
@@ -1413,6 +1535,7 @@ class Quota(LoggedModel):
for variation in (variations or []):
if variation.item not in items:
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
break
@staticmethod
def clean_items(event, items, variations):

View File

@@ -22,7 +22,7 @@ class LogEntry(models.Model):
in the database. This uses django.contrib.contenttypes to allow a
relation to an arbitrary database object.
:param datetime: The timestamp of the logged action
:param datatime: The timestamp of the logged action
:type datetime: datetime
:param user: The user that performed the action
:type user: User

View File

@@ -1165,33 +1165,6 @@ class AbstractPosition(models.Model):
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
def address_format(self):
lines = [
self.attendee_name,
self.company,
self.street,
self.zipcode + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name
]
lines = [r.strip() for r in lines if r]
return '\n'.join(lines).strip()
class OrderPayment(models.Model):
"""

View File

@@ -32,7 +32,7 @@ class Organizer(LoggedModel):
settings_namespace = 'organizer'
name = models.CharField(max_length=200,
verbose_name=_("Name"))
slug = models.CharField(
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used "

View File

@@ -5,8 +5,7 @@ import jsonschema
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Exists, F, OuterRef, Q, Value
from django.db.models.functions import Power
from django.db.models import F, Q
from django.utils.deconstruct import deconstructible
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
@@ -42,7 +41,7 @@ class SeatingPlan(LoggedModel):
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
Category = namedtuple('Categrory', 'name')
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank row_label seat_label x y')
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank row_label seat_label')
def __str__(self):
return self.name
@@ -70,9 +69,7 @@ class SeatingPlan(LoggedModel):
# *will* have gaps. We chose this way over just sorting the seats and continuously enumerating them as an
# optimization, because this way we do not need to update the rank of very seat if we change a plan a little.
for zi, z in enumerate(self.layout_data['zones']):
zpos = (z['position']['x'], z['position']['y'])
for ri, r in enumerate(z['rows']):
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y'])
row_label = None
if r.get('row_label'):
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
@@ -101,9 +98,7 @@ class SeatingPlan(LoggedModel):
seat_label=seat_label,
zone=z['name'],
category=s['category'],
sorting_rank=rank,
x=rpos[0] + s['position']['x'],
y=rpos[1] + s['position']['y'],
sorting_rank=rank
)
@@ -135,8 +130,6 @@ class Seat(models.Model):
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
blocked = models.BooleanField(default=False)
sorting_rank = models.BigIntegerField(default=0)
x = models.FloatField(null=True)
y = models.FloatField(null=True)
class Meta:
ordering = ['sorting_rank', 'seat_guid']
@@ -160,68 +153,7 @@ class Seat(models.Model):
return self.name
return ', '.join(parts)
@classmethod
def annotated(cls, qs, event_id, subevent, ignore_voucher_id=None, minimal_distance=0,
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False):
from . import Order, OrderPosition, Voucher, CartPosition
vqs = Voucher.objects.filter(
event_id=event_id,
subevent=subevent,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher_id:
vqs = vqs.exclude(pk=ignore_voucher_id)
opqs = OrderPosition.objects.filter(
order__event_id=event_id,
subevent=subevent,
seat_id=OuterRef('pk'),
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
)
if ignore_order_id:
opqs = opqs.exclude(order_id=ignore_order_id)
cqs = CartPosition.objects.filter(
event_id=event_id,
subevent=subevent,
seat_id=OuterRef('pk'),
expires__gte=now()
)
if ignore_cart_id:
cqs = cqs.exclude(cart_id=ignore_cart_id)
qs_annotated = qs.annotate(
has_order=Exists(
opqs
),
has_cart=Exists(
cqs
),
has_voucher=Exists(
vqs
)
)
if minimal_distance > 0:
# TODO: Is there a more performant implementation on PostgreSQL using
# https://www.postgresql.org/docs/8.2/functions-geometry.html ?
sq_closeby = qs_annotated.annotate(
distance=(
Power(F('x') - OuterRef('x'), Value(2), output_field=models.FloatField()) +
Power(F('y') - OuterRef('y'), Value(2), output_field=models.FloatField())
)
).filter(
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
distance__lt=minimal_distance ** 2
)
if distance_only_within_row:
sq_closeby = sq_closeby.filter(row_name=OuterRef('row_name'))
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
return qs_annotated
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web',
ignore_distancing=False, distance_ignore_cart_id=None):
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web'):
from .orders import Order
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
@@ -241,32 +173,4 @@ class Seat(models.Model):
opqs = opqs.exclude(pk=ignore_orderpos.pk)
if ignore_voucher_id:
vqs = vqs.exclude(pk=ignore_voucher_id)
if opqs.exists() or (ignore_cart is not True and cpqs.exists()) or vqs.exists():
return False
if self.event.settings.seating_minimal_distance > 0 and not ignore_distancing:
ev = (self.subevent or self.event)
qs_annotated = Seat.annotated(ev.seats, self.event_id, self.subevent,
ignore_voucher_id=ignore_voucher_id,
minimal_distance=0,
ignore_order_id=ignore_orderpos.order_id if ignore_orderpos else None,
ignore_cart_id=(
distance_ignore_cart_id or
(ignore_cart.cart_id if ignore_cart else None)
))
qs_closeby_taken = qs_annotated.annotate(
distance=(
Power(F('x') - Value(self.x), Value(2), output_field=models.FloatField()) +
Power(F('y') - Value(self.y), Value(2), output_field=models.FloatField())
)
).exclude(pk=self.pk).filter(
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
distance__lt=self.event.settings.seating_minimal_distance ** 2
)
if self.event.settings.seating_distance_within_row:
qs_closeby_taken = qs_closeby_taken.filter(row_name=self.row_name)
if qs_closeby_taken.exists():
return False
return True
return not opqs.exists() and (ignore_cart is True or not cpqs.exists()) and not vqs.exists()

View File

@@ -18,6 +18,7 @@ from django.template.loader import get_template
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import Countries
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
@@ -33,7 +34,6 @@ from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.countries import CachedCountries
from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.views import get_cart, get_cart_total
@@ -286,7 +286,7 @@ class BasePaymentProvider:
('_restricted_countries',
forms.MultipleChoiceField(
label=_('Restrict to countries'),
choices=CachedCountries(),
choices=Countries(),
help_text=_('Only allow choosing this payment provider for invoice addresses in the selected '
'countries. If you don\'t select any country, all countries are allowed. This is only '
'enabled if the invoice address is required.'),
@@ -901,7 +901,7 @@ class ManualPayment(BasePaymentProvider):
('email_instructions', I18nFormField(
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use '
'mails. It should instruct the user on how to proceed with the payment. You can use'
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
@@ -909,7 +909,7 @@ class ManualPayment(BasePaymentProvider):
('pending_description', I18nFormField(
label=_('Payment process description for pending orders'),
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use '
'It should instruct the user on how to proceed with the payment. You can use'
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],

View File

@@ -109,24 +109,6 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
}),
("attendee_company", {
"label": _("Attendee company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
}),
('shipping_address', {
'label': _('Full attendee address'),
'editor_sample': _('John Doe\nSample company\nSesame Street 42\n12345 Any City\nAtlantis'),
'evaluate': lambda op, order, event: order.shipping_address.format() if hasattr(
order, 'shipping_address') else ''
}),
("attendee_country", {
"label": _("Attendee country"),
"editor_sample": 'Atlantis',
"evaluate": lambda op, order, ev: str(getattr(op.country, 'name', '')) or (
str(getattr(op.addon_to.country, 'name', '')) if op.addon_to else ''
)
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
@@ -223,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Sample city"),
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
}),
("attendee_company", {
"label": _("Attendee company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\nAdd-on 2"),

View File

@@ -81,8 +81,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
logger.exception('Order canceled email could not be sent to attendee')
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,),
acks_late=True)
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
send: bool=False, send_subject: dict=None, send_message: dict=None,

View File

@@ -25,7 +25,6 @@ from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.pricing import get_price
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons
@@ -729,14 +728,10 @@ class CartManager:
def _get_quota_availability(self):
quotas_ok = defaultdict(int)
qa = QuotaAvailability()
qa.queue(*[k for k, v in self._quota_diff.items() if v > 0])
qa.compute(now_dt=self.now_dt)
for quota, count in self._quota_diff.items():
if count <= 0:
quotas_ok[quota] = 0
break
avail = qa.results[quota]
avail = quota.availability(self.now_dt)
if avail[1] is not None and avail[1] < count:
quotas_ok[quota] = min(count, avail[1])
else:
@@ -889,9 +884,7 @@ class CartManager:
available_count = 0
if isinstance(op, self.AddOperation):
if op.seat and not op.seat.is_available(ignore_voucher_id=op.voucher.id if op.voucher else None,
sales_channel=self._sales_channel,
distance_ignore_cart_id=self.cart_id):
if op.seat and not op.seat.is_available(ignore_voucher_id=op.voucher.id if op.voucher else None, sales_channel=self._sales_channel):
available_count = 0
err = err or error_messages['seat_unavailable']

View File

@@ -1,87 +1,13 @@
from datetime import timedelta
import dateutil
from django.db import transaction
from django.db.models.functions import TruncDate
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import now, override
from django.utils.timezone import now
from django.utils.translation import gettext as _
from pretix.base.models import (
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
)
from pretix.base.signals import checkin_created, order_placed
from pretix.helpers.jsonlogic import Logic
def get_logic_environment(ev):
def build_time(t=None, value=None):
if t == "custom":
return dateutil.parser.parse(value)
elif t == 'date_from':
return ev.date_from
elif t == 'date_to':
return ev.date_to
elif t == 'date_admission':
return ev.date_admission or ev.date_from
def is_before(t1, t2, tolerance=None):
if tolerance:
return t1 < t2 + timedelta(minutes=float(tolerance))
else:
return t1 < t2
logic = Logic()
logic.add_operation('objectList', lambda *objs: list(objs))
logic.add_operation('lookup', lambda model, pk, str: int(pk))
logic.add_operation('inList', lambda a, b: a in b)
logic.add_operation('buildTime', build_time)
logic.add_operation('isBefore', is_before)
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
return logic
class LazyRuleVars:
def __init__(self, position, clist, dt):
self._position = position
self._clist = clist
self._dt = dt
def __getitem__(self, item):
if item[0] != '_' and hasattr(self, item):
return getattr(self, item)
raise KeyError()
@property
def now(self):
return self._dt
@property
def product(self):
return self._position.item_id
@property
def variation(self):
return self._position.variation_id
@cached_property
def entries_number(self):
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist).count()
@cached_property
def entries_today(self):
tz = self._clist.event.timezone
midnight = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
@cached_property
def entries_days(self):
tz = self._clist.event.timezone
with override(tz):
return self._position.checkins.filter(list=self._clist, type=Checkin.TYPE_ENTRY).annotate(
day=TruncDate('datetime')
).values('day').distinct().count()
class CheckInError(Exception):
@@ -136,7 +62,7 @@ def _save_answers(op, answers, given_answers):
@transaction.atomic
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY):
user=None, auth=None, canceled_supported=False):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -153,11 +79,18 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
"""
dt = datetime or now()
# Lock order positions
op = OrderPosition.all.select_for_update().get(pk=op.pk)
checkin_questions = list(
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
)
# Fetch order position with related objects
op = OrderPosition.all.select_related(
'item', 'variation', 'order', 'addon_to'
).prefetch_related(
'item__questions',
Prefetch(
'item__questions',
queryset=Question.objects.filter(ask_during_checkin=True),
to_attr='checkin_questions'
),
'answers'
).get(pk=op.pk)
if op.canceled or op.order.status not in (Order.STATUS_PAID, Order.STATUS_PENDING):
raise CheckInError(
@@ -165,25 +98,19 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'canceled' if canceled_supported else 'unpaid'
)
answers = {a.question: a for a in op.answers.all()}
require_answers = []
if checkin_questions:
answers = {a.question: a for a in op.answers.all()}
for q in checkin_questions:
if q not in given_answers and q not in answers:
require_answers.append(q)
for q in op.item.checkin_questions:
if q not in given_answers and q not in answers:
require_answers.append(q)
_save_answers(op, answers, given_answers)
_save_answers(op, answers, given_answers)
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
raise CheckInError(
_('This order position has an invalid product for this check-in list.'),
'product'
)
elif clist.subevent_id and op.subevent_id != clist.subevent_id:
raise CheckInError(
_('This order position has an invalid date for this check-in list.'),
'product'
)
elif op.order.status != Order.STATUS_PAID and not force and not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
@@ -197,56 +124,40 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'incomplete',
require_answers
)
else:
try:
ci, created = Checkin.objects.get_or_create(position=op, list=clist, defaults={
'datetime': dt,
'nonce': nonce,
})
except Checkin.MultipleObjectsReturned:
ci, created = Checkin.objects.filter(position=op, list=clist).last(), False
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
rule_data = LazyRuleVars(op, clist, dt)
logic = get_logic_environment(op.subevent or clist.event)
if not logic.apply(clist.rules, rule_data):
if created or (nonce and nonce == ci.nonce):
if created:
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': op.order.status != Order.STATUS_PAID,
'datetime': dt,
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
if not force:
raise CheckInError(
_('This entry is not permitted due to custom rules.'),
'rules'
_('This ticket has already been redeemed.'),
'already_redeemed',
)
device = None
if isinstance(auth, Device):
device = auth
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
last_ci is None or
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
)
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
return
if entry_allowed or force:
ci = Checkin.objects.create(
position=op,
type=type,
list=clist,
datetime=dt,
device=device,
nonce=nonce,
forced=force and not entry_allowed,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': force or op.order.status != Order.STATUS_PAID,
'first': False,
'forced': force,
'datetime': dt,
'type': type,
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
raise CheckInError(
_('This ticket has already been redeemed.'),
'already_redeemed',
)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
@@ -261,6 +172,5 @@ def order_placed(sender, **kwargs):
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
checkin_created.send(event, checkin=ci)
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
checkin_created.send(event, checkin=ci)

View File

@@ -250,7 +250,7 @@ class CustomEmail(EmailMultiAlternatives):
return super()._create_mime_attachment(content, mimetype)
@app.task(base=TransactionAwareTask, bind=True, acks_late=True)
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False, user=None,

View File

@@ -13,10 +13,10 @@ from pretix.celery_app import app
from pretix.helpers.urls import build_absolute_uri
@app.task(base=TransactionAwareTask, acks_late=True)
@app.task(base=TransactionAwareTask)
@scopes_disabled()
def notify(logentry_id: int):
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.event:
return # Ignore, we only have event-related notifications right now
types = get_all_notification_types(logentry.event)
@@ -66,7 +66,7 @@ def notify(logentry_id: int):
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
@app.task(base=ProfiledTask, acks_late=True)
@app.task(base=ProfiledTask)
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
logentry = LogEntry.all.get(id=logentry_id)
if logentry.event:

View File

@@ -44,7 +44,6 @@ from pretix.base.services.invoices import (
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
@@ -1375,7 +1374,7 @@ class OrderChangeManager:
for seat, diff in self._seatdiff.items():
if diff <= 0:
continue
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True) or diff > 1:
if not seat.is_available(sales_channel=self.order.sales_channel) or diff > 1:
raise OrderError(self.error_messages['seat_unavailable'].format(seat=seat.name))
if self.event.has_subevents:
@@ -1392,13 +1391,10 @@ class OrderChangeManager:
raise OrderError(self.error_messages['seat_subevent_mismatch'].format(seat=v['seat'].name))
def _check_quotas(self):
qa = QuotaAvailability()
qa.queue(*[k for k, v in self._quotadiff.items() if v > 0])
qa.compute()
for quota, diff in self._quotadiff.items():
if diff <= 0:
continue
avail = qa.results[quota]
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < diff):
raise OrderError(self.error_messages['quota'].format(name=quota.name))

View File

@@ -1,357 +1,15 @@
import sys
from collections import Counter, defaultdict
from datetime import timedelta
from django.conf import settings
from django.db.models import Count, F, Func, Max, Q, Sum
from django.db.models import Max, Q
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
CartPosition, Event, LogEntry, Order, OrderPosition, Quota, Voucher,
WaitingListEntry,
)
from pretix.base.models import Event, LogEntry
from pretix.celery_app import app
from ..signals import periodic_task, quota_availability
class QuotaAvailability:
"""
This special object allows so compute the availability of multiple quotas, even across events, and inspect their
results. The maximum number of SQL queries is constant and not dependent on the number of quotas.
Usage example::
qa = QuotaAvailability()
qa.queue(quota1, quota2, …)
qa.compute()
print(qa.results)
Properties you can access after computation.
* results (dict mapping quotas to availability tuples)
* count_paid_orders (dict mapping quotas to ints)
* count_paid_orders (dict mapping quotas to ints)
* count_pending_orders (dict mapping quotas to ints)
* count_vouchers (dict mapping quotas to ints)
* count_waitinglist (dict mapping quotas to ints)
* count_cart (dict mapping quotas to ints)
"""
def __init__(self, count_waitinglist=True, ignore_closed=False, full_results=False, early_out=True):
"""
Initialize a new quota availability calculator
:param count_waitinglist: If ``True`` (default), the waiting list is considered. If ``False``, it is ignored.
:param ignore_closed: Quotas have a ``closed`` state that always makes the quota return as sold out. If you set
``ignore_closed`` to ``True``, we will ignore this completely. Default is ``False``.
:param full_results: Usually, the computation is as efficient as possible, i.e. if after counting the sold
orders we already see that the quota is sold out, we're not going to count the carts,
since it does not matter. This also means that you will not be able to get that number from
``.count_cart``. If you want all parts to be calculated (i.e. because you want to show
statistics to the user), pass ``full_results`` and we'll skip that optimization.
items
:param early_out: Usually, if a quota is ``closed`` or if its ``size`` is ``None`` (i.e. unlimited), we will
not need database access to determine the availability and return it right away. If you set
this to ``False``, however, we will *still* count the number of orders, which is required to
keep the database-level quota cache up to date so backend overviews render quickly. If you
do not care about keeping the cache up to date, you can set this to ``False`` for further
performance improvements.
"""
self._queue = []
self._count_waitinglist = count_waitinglist
self._ignore_closed = ignore_closed
self._full_results = full_results
self._item_to_quotas = defaultdict(list)
self._var_to_quotas = defaultdict(list)
self._early_out = early_out
self._quota_objects = {}
self.results = {}
self.count_paid_orders = defaultdict(int)
self.count_pending_orders = defaultdict(int)
self.count_vouchers = defaultdict(int)
self.count_waitinglist = defaultdict(int)
self.count_cart = defaultdict(int)
self.sizes = {}
def queue(self, *quota):
self._queue += quota
def compute(self, now_dt=None):
now_dt = now_dt or now()
quotas = list(self._queue)
quotas_original = list(self._queue)
self._queue.clear()
if not quotas:
return
self._compute(quotas, now_dt)
for q in quotas_original:
for recv, resp in quota_availability.send(sender=q.event, quota=q, result=self.results[q],
count_waitinglist=self.count_waitinglist):
self.results[q] = resp
self._close(quotas)
self._write_cache(quotas, now_dt)
def _write_cache(self, quotas, now_dt):
events = {q.event for q in quotas}
update = []
for e in events:
e.cache.delete('item_quota_cache')
for q in quotas:
rewrite_cache = self._count_waitinglist and (
not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state
or q.cached_availability_paid_orders is None
)
if rewrite_cache:
q.cached_availability_state = self.results[q][0]
q.cached_availability_number = self.results[q][1]
q.cached_availability_time = now_dt
if q in self.count_paid_orders:
q.cached_availability_paid_orders = self.count_paid_orders[q]
update.append(q)
if update:
Quota.objects.using('default').bulk_update(update, [
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
'cached_availability_paid_orders'
], batch_size=50)
def _close(self, quotas):
for q in quotas:
if self.results[q][0] <= Quota.AVAILABILITY_ORDERED and q.close_when_sold_out and not q.closed:
q.closed = True
q.save(update_fields=['closed'])
q.log_action('pretix.event.quota.closed')
def _compute(self, quotas, now_dt):
# Quotas we want to look at now
self.sizes.update({q: q.size for q in quotas})
# Some helpful caches
self._quota_objects.update({q.pk: q for q in quotas})
# Compute result for closed or unlimited
self._compute_early_outs(quotas)
if self._early_out:
if not self._full_results:
quotas = [q for q in quotas if q not in self.results]
if not quotas:
return
size_left = Counter({q: (sys.maxsize if s is None else s) for q, s in self.sizes.items()})
for q in quotas:
self.count_paid_orders[q] = 0
self.count_pending_orders[q] = 0
self.count_cart[q] = 0
self.count_vouchers[q] = 0
self.count_waitinglist[q] = 0
# Fetch which quotas belong to which items and variations
q_items = Quota.items.through.objects.filter(
quota_id__in=[q.pk for q in quotas]
).values('quota_id', 'item_id')
for m in q_items:
self._item_to_quotas[m['item_id']].append(self._quota_objects[m['quota_id']])
q_vars = Quota.variations.through.objects.filter(
quota_id__in=[q.pk for q in quotas]
).values('quota_id', 'itemvariation_id')
for m in q_vars:
self._var_to_quotas[m['itemvariation_id']].append(self._quota_objects[m['quota_id']])
self._compute_orders(quotas, q_items, q_vars, size_left)
if not self._full_results:
quotas = [q for q in quotas if q not in self.results]
if not quotas:
return
self._compute_vouchers(quotas, q_items, q_vars, size_left, now_dt)
if not self._full_results:
quotas = [q for q in quotas if q not in self.results]
if not quotas:
return
self._compute_carts(quotas, q_items, q_vars, size_left, now_dt)
if self._count_waitinglist:
if not self._full_results:
quotas = [q for q in quotas if q not in self.results]
if not quotas:
return
self._compute_waitinglist(quotas, q_items, q_vars, size_left)
for q in quotas:
if q not in self.results:
if size_left[q] > 0:
self.results[q] = Quota.AVAILABILITY_OK, size_left[q]
else:
raise ValueError("inconclusive quota")
def _compute_orders(self, quotas, q_items, q_vars, size_left):
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
op_lookup = OrderPosition.objects.filter(
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
order__event_id__in=events,
).filter(seq).filter(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if self._quota_objects[i['quota_id']] in quotas})
).order_by().values('order__status', 'item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
for line in sorted(op_lookup, key=lambda li: li['order__status'], reverse=True): # p before n
if line['variation_id']:
qs = self._var_to_quotas[line['variation_id']]
else:
qs = self._item_to_quotas[line['item_id']]
for q in qs:
if q.subevent_id == line['subevent_id']:
size_left[q] -= line['c']
if line['order__status'] == Order.STATUS_PAID:
self.count_paid_orders[q] += line['c']
q.cached_availability_paid_orders = line['c']
elif line['order__status'] == Order.STATUS_PENDING:
self.count_pending_orders[q] += line['c']
if size_left[q] <= 0 and q not in self.results:
if line['order__status'] == Order.STATUS_PAID:
self.results[q] = Quota.AVAILABILITY_GONE, 0
else:
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
def _compute_vouchers(self, quotas, q_items, q_vars, size_left, now_dt):
events = {q.event_id for q in quotas}
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
func = 'MAX'
else: # NOQA
func = 'GREATEST'
subevents = {q.subevent_id for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
v_lookup = Voucher.objects.filter(
Q(event_id__in=events) &
seq &
Q(block_quota=True) &
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if
self._quota_objects[i['quota_id']] in quotas}
) | Q(
quota_id__in=[q.pk for q in quotas]
)
)
).order_by().values('subevent_id', 'item_id', 'quota_id', 'variation_id').annotate(
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
)
for line in v_lookup:
if line['variation_id']:
qs = self._var_to_quotas[line['variation_id']]
elif line['item_id']:
qs = self._item_to_quotas[line['item_id']]
else:
qs = [self._quota_objects[line['quota_id']]]
for q in qs:
if q.subevent_id == line['subevent_id']:
size_left[q] -= line['free']
self.count_vouchers[q] += line['free']
if q not in self.results and size_left[q] <= 0:
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
def _compute_carts(self, quotas, q_items, q_vars, size_left, now_dt):
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
cart_lookup = CartPosition.objects.filter(
Q(event_id__in=events) &
seq &
Q(expires__gte=now_dt) &
Q(
Q(voucher__isnull=True)
| Q(voucher__block_quota=False)
| Q(voucher__valid_until__lt=now_dt)
) &
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if self._quota_objects[i['quota_id']] in quotas}
)
)
).order_by().values('item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
for line in cart_lookup:
if line['variation_id']:
qs = self._var_to_quotas[line['variation_id']]
else:
qs = self._item_to_quotas[line['item_id']]
for q in qs:
if q.subevent_id == line['subevent_id']:
size_left[q] -= line['c']
self.count_cart[q] += line['c']
if q not in self.results and size_left[q] <= 0:
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
w_lookup = WaitingListEntry.objects.filter(
Q(event_id__in=events) &
Q(voucher__isnull=True) &
seq &
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(variation_id__in={i['itemvariation_id'] for i in q_vars if
self._quota_objects[i['quota_id']] in quotas})
)
).order_by().values('item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
for line in w_lookup:
if line['variation_id']:
qs = self._var_to_quotas[line['variation_id']]
else:
qs = self._item_to_quotas[line['item_id']]
for q in qs:
if q.subevent_id == line['subevent_id']:
size_left[q] -= line['c']
self.count_waitinglist[q] += line['c']
if q not in self.results and size_left[q] <= 0:
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
def _compute_early_outs(self, quotas):
for q in quotas:
if q.closed and not self._ignore_closed:
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
elif q.size is None:
self.results[q] = Quota.AVAILABILITY_OK, None
elif q.size == 0:
self.results[q] = Quota.AVAILABILITY_GONE, 0
from ..signals import periodic_task
@receiver(signal=periodic_task)

View File

@@ -67,8 +67,6 @@ def generate_seats(event, subevent, plan, mapping):
update(seat, 'sorting_rank', ss.sorting_rank),
update(seat, 'row_label', ss.row_label),
update(seat, 'seat_label', ss.seat_label),
update(seat, 'x', ss.x),
update(seat, 'y', ss.y),
])
if updated:
seat.save()
@@ -84,8 +82,6 @@ def generate_seats(event, subevent, plan, mapping):
sorting_rank=ss.sorting_rank,
row_label=ss.row_label,
seat_label=ss.seat_label,
x=ss.x,
y=ss.y,
product=p,
))

View File

@@ -175,7 +175,7 @@ def get_tickets_for_order(order, base_position=None):
return tickets
@app.task(base=EventTask, acks_late=True)
@app.task(base=EventTask)
def invalidate_cache(event: Event, item: int=None, provider: str=None, order: int=None, **kwargs):
qs = CachedTicket.objects.filter(order_position__order__event=event)
qsc = CachedCombinedTicket.objects.filter(order__event=event)

View File

@@ -9,7 +9,7 @@ from pretix.base.services.tasks import TransactionAwareProfiledEventTask
from pretix.celery_app import app
@app.task(base=TransactionAwareProfiledEventTask, acks_late=True)
@app.task(base=TransactionAwareProfiledEventTask)
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int) -> None:
vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id'))
user = User.objects.get(pk=user)

View File

@@ -880,24 +880,7 @@ DEFAULTS = {
},
'event_list_type': {
'default': 'list',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('list', _('List')),
('week', _('Week calendar')),
('calendar', _('Month calendar')),
)
),
'form_kwargs': dict(
label=_('Default overview style'),
choices=(
('list', _('List')),
('week', _('Week calendar')),
('calendar', _('Month calendar')),
)
),
'type': str
},
'last_order_modification_date': {
'default': None,
@@ -1648,18 +1631,10 @@ Your {event} team"""))
'default': settings.ENTROPY['giftcard_secret'],
'type': int
},
'seating_minimal_distance': {
'default': '0',
'type': float
},
'seating_allow_blocked_seats_for_channel': {
'default': [],
'type': list
},
'seating_distance_within_row': {
'default': 'False',
'type': bool
}
}
PERSON_NAME_TITLE_GROUPS = OrderedDict([
('english_common', (_('Most common English titles'), (
@@ -1844,7 +1819,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
'AU': (['State', 'Territory'], 'short'),
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'MY': (['State'], 'long'),
'MX': (['State', 'Federal District'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),

View File

@@ -34,7 +34,7 @@ def shred_constraints(event: Event):
max_fromto=Greatest(Max('date_to'), Max('date_from'))
)
max_date = max_date['max_fromto'] or max_date['max_to'] or max_date['max_from']
if max_date is not None and max_date > now() - timedelta(days=60):
if max_date > now() - timedelta(days=60):
return _('Your event needs to be over for at least 60 days to use this feature.')
else:
if (event.date_to or event.date_from) > now() - timedelta(days=60):

View File

@@ -25,10 +25,6 @@
padding: 0;
}
table.layout > tr > td.logo {
padding: 20px 0 0 0;
}
table.layout > tr > td.header {
padding: 0 20px;
text-align: center;
@@ -175,7 +171,7 @@
{% if event.settings.logo_image %}
<!--[if !mso]><!-- -->
<tr>
<td style="line-height: 0;" align="center" class="logo">
<td style="line-height: 0" align="center">
{% if event.settings.logo_image|thumb:'5000x120'|first == '/' %}
<img src="{{ site_url }}{{ event.settings.logo_image|thumb:'5000x120' }}" alt="{{ event.name }}"
style="height: auto; max-width: 100%;" />

View File

@@ -65,9 +65,9 @@ ALLOWED_ATTRIBUTES = {
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
URL_RE = build_url_re(tlds=sorted(tld_set, key=len, reverse=True))
URL_RE = build_url_re(tlds=tld_set)
EMAIL_RE = build_email_re(tlds=sorted(tld_set, key=len, reverse=True))
EMAIL_RE = build_email_re(tlds=tld_set)
def safelink_callback(attrs, new=False):

View File

@@ -5,7 +5,6 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files import File
from django.core.files.uploadedfile import UploadedFile
from django.forms.utils import from_current_timezone
from django.utils.translation import gettext_lazy as _
@@ -95,30 +94,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
return ctx
class SizeFileField(forms.FileField):
def __init__(self, *args, **kwargs):
self.max_size = kwargs.pop("max_size", None)
super().__init__(*args, **kwargs)
@staticmethod
def _sizeof_fmt(num, suffix='B'):
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs)
if isinstance(data, UploadedFile) and self.max_size and data.size > self.max_size:
raise forms.ValidationError(_("Please do not upload files larger than {size}!").format(
size=SizeFileField._sizeof_fmt(self.max_size)
))
return data
class ExtFileField(SizeFileField):
class ExtFileField(forms.FileField):
widget = ClearableBasenameFileInput
def __init__(self, *args, **kwargs):

View File

@@ -35,10 +35,11 @@ class CheckinListForm(forms.ModelForm):
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']
@@ -51,43 +52,13 @@ class CheckinListForm(forms.ModelForm):
'limit_products',
'subevent',
'include_pending',
'auto_checkin_sales_channels',
'allow_multiple_entries',
'allow_entry_after_exit',
'rules',
]
widgets = {
'limit_products': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '<[name$=all_products]'
}),
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
}
field_classes = {
'limit_products': SafeModelMultipleChoiceField,
'subevent': SafeModelChoiceField,
}
class SimpleCheckinListForm(forms.ModelForm):
def __init__(self, **kwargs):
self.event = kwargs.pop('event')
kwargs.pop('locales', None)
super().__init__(**kwargs)
self.fields['limit_products'].queryset = self.event.items.all()
class Meta:
model = CheckinList
localized_fields = '__all__'
fields = [
'name',
'all_products',
'limit_products',
'include_pending',
'auto_checkin_sales_channels'
]
widgets = {
'limit_products': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '<[name$=all_products]'
}),
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple()
}
field_classes = {
'limit_products': SafeModelMultipleChoiceField,

View File

@@ -409,7 +409,6 @@ class EventSettingsForm(SettingsForm):
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('If you provide a logo image, we will by default not show your event name and date '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
@@ -429,7 +428,6 @@ class EventSettingsForm(SettingsForm):
label=_('Social media image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
@@ -512,7 +510,6 @@ class EventSettingsForm(SettingsForm):
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'event_list_type',
'frontpage_text',
'attendee_names_asked',
'attendee_names_required',
@@ -560,7 +557,6 @@ class EventSettingsForm(SettingsForm):
]
if not self.event.has_subevents:
del self.fields['frontpage_subevent_ordering']
del self.fields['event_list_type']
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
]
@@ -714,7 +710,6 @@ class InvoiceSettingsForm(SettingsForm):
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
)

View File

@@ -111,13 +111,6 @@ class OrderFilterForm(FilterForm):
(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)')),
('testmode', _('Test mode')),
('rc', _('Cancellation requested')),
),
required=False,
)
@@ -172,46 +165,6 @@ class OrderFilterForm(FilterForm):
qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
elif s in ('p', 'n', 'e', 'c', 'r'):
qs = qs.filter(status=s)
elif s == 'overpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
)
elif s == 'rc':
qs = qs.filter(
cancellation_requests__isnull=False
)
elif s == 'pendingpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
& Q(require_approval=False)
)
elif s == 'underpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
status=Order.STATUS_PAID,
pending_sum_t__gt=0
)
elif s == 'pa':
qs = qs.filter(
status=Order.STATUS_PENDING,
require_approval=True
)
elif s == 'testmode':
qs = qs.filter(
testmode=True
)
elif s == '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)
)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
@@ -250,6 +203,27 @@ class EventOrderFilterForm(OrderFilterForm):
answer = forms.CharField(
required=False
)
status = forms.ChoiceField(
label=_('Order status'),
choices=(
('', _('All orders')),
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
(Order.STATUS_PENDING, _('Pending')),
('o', _('Pending (overdue)')),
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
(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)')),
('testmode', _('Test mode')),
('rc', _('Cancellation requested')),
),
required=False,
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
@@ -326,6 +300,47 @@ class EventOrderFilterForm(OrderFilterForm):
)
qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True)
if fdata.get('status') == 'overpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
)
elif fdata.get('status') == 'rc':
qs = qs.filter(
cancellation_requests__isnull=False
)
elif fdata.get('status') == 'pendingpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
& Q(require_approval=False)
)
elif fdata.get('status') == 'underpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
status=Order.STATUS_PAID,
pending_sum_t__gt=0
)
elif fdata.get('status') == 'pa':
qs = qs.filter(
status=Order.STATUS_PENDING,
require_approval=True
)
elif fdata.get('status') == 'testmode':
qs = qs.filter(
testmode=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
@@ -757,10 +772,10 @@ class CheckInFilterForm(FilterForm):
'-code': ('-order__code', '-item__name'),
'email': ('order__email', 'item__name'),
'-email': ('-order__email', '-item__name'),
'status': (FixedOrderBy(F('last_entry'), nulls_first=True, descending=True), 'order__code'),
'-status': (FixedOrderBy(F('last_entry'), nulls_last=True), '-order__code'),
'timestamp': (FixedOrderBy(F('last_entry'), nulls_first=True), 'order__code'),
'-timestamp': (FixedOrderBy(F('last_entry'), nulls_last=True, descending=True), '-order__code'),
'status': (FixedOrderBy(F('last_checked_in'), nulls_first=True, descending=True), 'order__code'),
'-status': (FixedOrderBy(F('last_checked_in'), nulls_last=True), '-order__code'),
'timestamp': (FixedOrderBy(F('last_checked_in'), nulls_first=True), 'order__code'),
'-timestamp': (FixedOrderBy(F('last_checked_in'), nulls_last=True, descending=True), '-order__code'),
'item': ('item__name', 'variation__value', 'order__code'),
'-item': ('-item__name', '-variation__value', '-order__code'),
'seat': ('seat__sorting_rank', 'seat__guid'),
@@ -783,7 +798,6 @@ class CheckInFilterForm(FilterForm):
label=_('Check-in status'),
choices=(
('', _('All attendees')),
('2', pgettext_lazy('checkin state', 'Present')),
('1', _('Checked in')),
('0', _('Not checked in')),
),
@@ -824,13 +838,9 @@ class CheckInFilterForm(FilterForm):
if fdata.get('status'):
s = fdata.get('status')
if s == '1':
qs = qs.filter(last_entry__isnull=False)
elif s == '2':
qs = qs.filter(last_entry__isnull=False).filter(
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
)
qs = qs.filter(last_checked_in__isnull=False)
elif s == '0':
qs = qs.filter(last_entry__isnull=True)
qs = qs.filter(last_checked_in__isnull=True)
if fdata.get('ordering'):
ob = self.orders[fdata.get('ordering')]

View File

@@ -4,7 +4,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from pretix.base.forms import SecretKeySettingsField, SettingsForm
from pretix.base.forms import SettingsForm
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import register_global_settings
@@ -37,7 +37,7 @@ class GlobalSettingsForm(SettingsForm):
required=False,
label=_("Global message banner detail text"),
)),
('opencagedata_apikey', SecretKeySettingsField(
('opencagedata_apikey', forms.CharField(
required=False,
label=_("OpenCage API key for geocoding"),
)),

View File

@@ -5,7 +5,6 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.timezone import make_aware, now
from django.utils.translation import (
@@ -122,10 +121,7 @@ class CancelForm(ConfirmPaymentForm):
prs = self.instance.payment_refund_sum
if prs > 0:
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
Decimal('0.00'),
settings.CURRENCY_PLACES.get(self.instance.event.currency, 2)
)
self.fields['cancellation_fee'].initial = Decimal('0.00')
self.fields['cancellation_fee'].max_value = prs
else:
del self.fields['cancellation_fee']

View File

@@ -275,7 +275,6 @@ class OrganizerSettingsForm(SettingsForm):
organizer_logo_image = ExtFileField(
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
max_size=10 * 1024 * 1024,
required=False,
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
@@ -291,8 +290,7 @@ class OrganizerSettingsForm(SettingsForm):
label=_('Default overview style'),
choices=(
('list', _('List')),
('week', _('Week calendar')),
('calendar', _('Month calendar')),
('calendar', _('Calendar'))
)
)
event_list_availability = forms.BooleanField(
@@ -324,7 +322,6 @@ class OrganizerSettingsForm(SettingsForm):
label=_('Favicon'),
ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=1 * 1024 * 1024,
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
'We recommend a size of at least 200x200px to accommodate most devices.')
)

Some files were not shown because too many files have changed in this diff Show More