mirror of
https://github.com/pretix/pretix.git
synced 2026-11-11 01:27:08 +00:00
cleanup; ticket cache invalidation
This commit is contained in:
1 parent
664ecacc4b
commit
f54b41bc24
23 files changed
+370
-139
No files matched your search
@@ -37,9 +37,9 @@ from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedFile, CachedTicket, Event, InvoiceAddress,
|
||||
Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.services.tasks import EventTask, ProfiledTask
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareProfiledEventTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.base.signals import register_ticket_outputs, invalidate_ticket_cache
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
|
||||
@@ -250,8 +250,8 @@ def get_tickets_for_order(order, base_position=None):
|
||||
return tickets
|
||||
|
||||
|
||||
@app.task(base=EventTask, acks_late=True)
|
||||
def invalidate_cache(event: Event, item: int=None, provider: str=None, order: int=None, **kwargs):
|
||||
@app.task(base=TransactionAwareProfiledEventTask, acks_late=True)
|
||||
def invalidate_cache(event: Event, item: int | None=None, provider: str | None=None, order: int | None=None, **kwargs):
|
||||
qs = CachedTicket.objects.filter(order_position__order__event=event)
|
||||
qsc = CachedCombinedTicket.objects.filter(order__event=event)
|
||||
|
||||
@@ -271,6 +271,9 @@ def invalidate_cache(event: Event, item: int=None, provider: str=None, order: in
|
||||
for ct in qsc:
|
||||
ct.delete()
|
||||
|
||||
invalidate_ticket_cache.send(event, item=item, provider=provider, order=order)
|
||||
|
||||
|
||||
def get_download_buttons(event, show_disabled):
|
||||
buttons = []
|
||||
|
||||
@@ -293,6 +296,7 @@ def get_download_buttons(event, show_disabled):
|
||||
})
|
||||
return buttons
|
||||
|
||||
|
||||
def get_wallet_buttons(event):
|
||||
# TODO(wallet): implement this
|
||||
from pretix.plugins.wallet.platforms import AVAILABLE_PLATFORMS
|
||||
@@ -301,4 +305,4 @@ def get_wallet_buttons(event):
|
||||
platform = p(event)
|
||||
attr, cont = platform.render_download_button()
|
||||
buttons.append({"identifier": platform.identifier, "attributes": attr, "content": cont})
|
||||
return buttons
|
||||
return buttons
|
||||
@@ -1218,3 +1218,12 @@ This signal is sent out each time the information for a Device is modified.
|
||||
Both the original and updated versions of the Device are included to allow
|
||||
receivers to see what has been updated.
|
||||
"""
|
||||
|
||||
invalidate_ticket_cache = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``item: int | None``, ``provider: str | None``, ``order: int | None``
|
||||
|
||||
This signal is sent out when the ticket cache is invalidated.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -54,7 +54,6 @@ from django.db.models import (
|
||||
Count, Exists, F, IntegerField, Max, OuterRef, Prefetch, ProtectedError, Q,
|
||||
QuerySet, Subquery, Sum,
|
||||
)
|
||||
from uuid import UUID
|
||||
from django.forms import formset_factory
|
||||
from django.http import (
|
||||
FileResponse, Http404, HttpResponseNotAllowed, HttpResponseRedirect,
|
||||
@@ -661,18 +660,7 @@ class OrderDownload(AsyncAction, OrderView):
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_success_url(self, value):
|
||||
if isinstance(value, UUID):
|
||||
cf = CachedFile.objects.filter(pk=value).first()
|
||||
else:
|
||||
cf = None
|
||||
|
||||
if not cf or not cf.allowed_for_session(self.request):
|
||||
return self.get_self_url()
|
||||
|
||||
if cf.type == 'text/uri-list':
|
||||
return cf.file.file.read().decode()
|
||||
else:
|
||||
return reverse('cachedfile.download', kwargs={"id":str(value)})
|
||||
return self.get_self_url()
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_order_url()
|
||||
@@ -711,19 +699,12 @@ class OrderDownload(AsyncAction, OrderView):
|
||||
if 'position' in kwargs and not self.order_position.generate_ticket:
|
||||
return self.error(_('Ticket download is not enabled for this product.'))
|
||||
|
||||
if self.output.is_cacheable and (ct := self.get_last_ct()):
|
||||
if ct := self.get_last_ct():
|
||||
return self.success(ct)
|
||||
|
||||
if not self.output.is_cacheable:
|
||||
cf = CachedFile(web_download=True, date=now(), expires=now() + timedelta(hours=2))
|
||||
cf.bind_to_session(request)
|
||||
cf.save()
|
||||
else:
|
||||
cf = None
|
||||
|
||||
return self.do('orderposition' if 'position' in kwargs else 'order',
|
||||
self.order_position.pk if 'position' in kwargs else self.order.pk,
|
||||
self.output.identifier, cf.pk if cf else None)
|
||||
self.output.identifier)
|
||||
|
||||
def get_success_message(self, value):
|
||||
return ""
|
||||
@@ -753,12 +734,9 @@ class OrderDownload(AsyncAction, OrderView):
|
||||
return redirect(self.get_success_url(value))
|
||||
|
||||
def get_last_ct(self):
|
||||
if self.output.is_cacheable:
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
else:
|
||||
ct = None
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
if not ct or not ct.file:
|
||||
return None
|
||||
return ct
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0312_alter_customer_locale_alter_devicelastseen_device_and_more",
|
||||
"0311_fix_unshredded_invoices",
|
||||
),
|
||||
("wallet", "0003_walletlayoutfilesetting"),
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0312_alter_customer_locale_alter_devicelastseen_device_and_more",
|
||||
"0311_fix_unshredded_invoices",
|
||||
),
|
||||
("wallet", "0005_rename_type_googlewalletinstance_resource_and_more"),
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0312_alter_customer_locale_alter_devicelastseen_device_and_more",
|
||||
"0311_fix_unshredded_invoices",
|
||||
),
|
||||
("wallet", "0009_googlewalletinstance_usages_and_more"),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.2.17 on 2026-11-02 14:57
|
||||
|
||||
import django.db.models.deletion
|
||||
import pretix.plugins.wallet.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0311_fix_unshredded_invoices",
|
||||
),
|
||||
("wallet", "0013_applewalletpass_manifest_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CombinedAppleWalletPass",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("provider", models.CharField(max_length=255)),
|
||||
("type", models.CharField(max_length=255)),
|
||||
("extension", models.CharField(max_length=255)),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"order",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.order",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.17 on 2026-11-02 15:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wallet", "0014_combinedapplewalletpass"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="combinedapplewalletpass",
|
||||
name="extension",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="combinedapplewalletpass",
|
||||
name="provider",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="combinedapplewalletpass",
|
||||
name="type",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.17 on 2026-11-02 16:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0311_fix_unshredded_invoices",
|
||||
),
|
||||
("wallet", "0015_remove_combinedapplewalletpass_extension_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="googlewalletinstance",
|
||||
name="order_positon",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.orderposition",
|
||||
),
|
||||
),
|
||||
]
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.17 on 2026-11-02 16:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wallet", "0016_googlewalletinstance_order_positon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="googlewalletinstance",
|
||||
old_name="order_positon",
|
||||
new_name="order_position",
|
||||
),
|
||||
]
|
||||
@@ -19,17 +19,16 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import tempfile
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q, constraints
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.models import LoggedModel, OrderPosition
|
||||
from pretix.base.models.event import Event
|
||||
from pretix.base.models.orders import Order
|
||||
|
||||
|
||||
class WalletLayout(LoggedModel):
|
||||
@@ -142,8 +141,15 @@ class AppleWalletPass(models.Model):
|
||||
]
|
||||
|
||||
|
||||
class CombinedAppleWalletPass(models.Model):
|
||||
order = models.OneToOneField(Order, on_delete=models.CASCADE)
|
||||
file = models.FileField(null=True, blank=True, max_length=255)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class GoogleWalletInstance(models.Model):
|
||||
event = models.ForeignKey("pretixbase.Event", on_delete=models.CASCADE)
|
||||
order_position = models.ForeignKey(OrderPosition, on_delete=models.CASCADE, null=True)
|
||||
resource = models.CharField()
|
||||
identifier = models.CharField()
|
||||
data = models.JSONField()
|
||||
|
||||
@@ -2,6 +2,7 @@ import hashlib
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
import cryptography
|
||||
@@ -11,13 +12,17 @@ import cryptography.x509
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.base.models.orders import Order
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.plugins.wallet.i18n import (
|
||||
MaybeTranslatedString,
|
||||
)
|
||||
from ..base import (
|
||||
WalletPlatform,
|
||||
)
|
||||
from ...models import CombinedAppleWalletPass
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -94,26 +99,44 @@ class ApplePlatform(WalletPlatform):
|
||||
]
|
||||
)
|
||||
|
||||
# TODO: generate_ops
|
||||
# def generate_order(self, order: Order):
|
||||
# with tempfile.TemporaryFile() as d:
|
||||
# with zipfile.ZipFile(d, "w") as zipf:
|
||||
# for pos in order.positions_with_tickets:
|
||||
# fname, __, content = self.generate(pos)
|
||||
# zipf.writestr(
|
||||
# "{}-{}{}".format(
|
||||
# order.code, pos.positionid, os.path.splitext(fname)[1]
|
||||
# ),
|
||||
# content,
|
||||
# )
|
||||
def generate(self, position, layout=None) -> str:
|
||||
if not layout:
|
||||
layout = self.get_layout(position).pass_layout
|
||||
pass_obj = layout.generate(position)
|
||||
|
||||
# d.seek(0)
|
||||
# return (
|
||||
# "{}-{}.pkpasses".format(order.code, self.identifier),
|
||||
# "application/vnd.apple.pkpasses",
|
||||
# d.read(),
|
||||
# )
|
||||
return eventreverse_absolute(
|
||||
self.event,
|
||||
"plugins:wallet:apple_download",
|
||||
kwargs={"order": position.order.code, "secret": position.order.secret, "position": position.pk},
|
||||
)
|
||||
|
||||
def generate_order(self, order: Order):
|
||||
with tempfile.TemporaryFile() as d:
|
||||
with zipfile.ZipFile(d, "w") as zipf:
|
||||
for pos in order.positions_with_tickets:
|
||||
pass_obj = self.get_layout(pos).pass_layout.generate(pos)
|
||||
zipf.writestr(
|
||||
"{}-{}.pkpass".format(order.code, pos.positionid),
|
||||
pass_obj.content.read(),
|
||||
)
|
||||
|
||||
d.seek(0)
|
||||
data = d.read()
|
||||
|
||||
if hasattr(order, "combinedapplewalletpass"):
|
||||
order.combinedapplewalletpass.delete()
|
||||
|
||||
ct = CombinedAppleWalletPass.objects.create(
|
||||
order=order,
|
||||
file=None,
|
||||
)
|
||||
ct.file.save("{}-{}.pkpasses".format(order.code, self.identifier), ContentFile(data))
|
||||
|
||||
return eventreverse_absolute(
|
||||
self.event,
|
||||
"plugins:wallet:apple_download_combined",
|
||||
kwargs={"order": order.code, "secret": order.secret},
|
||||
)
|
||||
|
||||
class StringResource:
|
||||
entries: dict[str, MaybeTranslatedString]
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.translation import gettext as _
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.helpers import OF_SELF
|
||||
@@ -238,7 +239,7 @@ class AppleWalletStyle(PassStyle):
|
||||
pass_obj.manifest = manifest
|
||||
pass_obj.save()
|
||||
|
||||
return filename, "application/vnd.apple.pkpass", result
|
||||
return pass_obj
|
||||
|
||||
|
||||
class AppleWalletEventTicket(AppleWalletStyle):
|
||||
|
||||
@@ -10,6 +10,7 @@ from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.api.helpers import handle_file_upload
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.base.models.orders import Order
|
||||
from pretix.plugins.wallet.models import WalletPlatformLayout
|
||||
|
||||
from ..i18n import MaybeTranslatedString, localizable_to_dict
|
||||
@@ -66,10 +67,10 @@ class WalletPlatform:
|
||||
layout = self.get_layout(position).pass_layout
|
||||
return layout.generate(position)
|
||||
|
||||
def generate_ops(self, ops: Iterable[OrderPosition]) -> str:
|
||||
def generate_order(self, order: Order) -> str:
|
||||
"""
|
||||
This method should generate the wallet passes for all positions in `ops` and return a
|
||||
url the user should be sent to to retrieve them (e.g. via download or by redirecting
|
||||
This method should generate the wallet passes for all positions in `order` and return
|
||||
a url the user should be sent to to retrieve them (e.g. via download or by redirecting
|
||||
to the wallet provider)
|
||||
|
||||
.. note:: You should cache the generated files where possible and only regenerate
|
||||
|
||||
@@ -14,6 +14,7 @@ from google.auth import crypt, jwt
|
||||
from google.auth.transport.requests import AuthorizedSession
|
||||
from google.oauth2 import service_account
|
||||
|
||||
from pretix.base.models.orders import OrderPosition
|
||||
from pretix.plugins.wallet.i18n import (
|
||||
MaybeTranslatedString, localizable_to_dict,
|
||||
)
|
||||
@@ -156,6 +157,7 @@ class BaseGoogleWalletObject:
|
||||
|
||||
locales: set[str]
|
||||
default_locale: str
|
||||
order_position = None
|
||||
|
||||
def _localized_string(self, string: MaybeTranslatedString):
|
||||
return localized_string(string, self.locales, self.default_locale)
|
||||
@@ -235,7 +237,7 @@ class EventTicketClass(BaseGoogleWalletObject):
|
||||
}
|
||||
|
||||
def security_animation(self, security_animation: AnimationType):
|
||||
self.data["securityAnimation"] = security_animation
|
||||
self.data["securityAnimation"] = {"animationType": security_animation}
|
||||
|
||||
def doors_open(
|
||||
self,
|
||||
@@ -300,10 +302,12 @@ class EventTicketObject(BaseGoogleWalletObject):
|
||||
state: State,
|
||||
locales: set[str],
|
||||
default_locale: str,
|
||||
order_position: OrderPosition
|
||||
):
|
||||
self.id = object_id
|
||||
self.locales = locales
|
||||
self.default_locale = default_locale
|
||||
self.order_position = order_position
|
||||
|
||||
self.data = {
|
||||
"id": object_id,
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.utils.translation import gettext as _
|
||||
from django import forms
|
||||
|
||||
from pretix.base.models import Event, OrderPosition
|
||||
from pretix.base.models.orders import Order
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.plugins.wallet.models import GoogleWalletInstance
|
||||
from ..base import (
|
||||
@@ -165,7 +166,7 @@ class GooglePlatform(WalletPlatform):
|
||||
event=self.event,
|
||||
resource=object.resource,
|
||||
identifier=object.id,
|
||||
defaults={"data": object.data},
|
||||
defaults={"data": object.data, "order_position": object.order_position},
|
||||
)
|
||||
if created or instance.data != object.data:
|
||||
result = self.comms.put_item(object.resource, object.id, object.data)
|
||||
@@ -207,10 +208,10 @@ class GooglePlatform(WalletPlatform):
|
||||
|
||||
return self._get_jwt([ticket_object])
|
||||
|
||||
def generate_ops(self, ops: Iterable[OrderPosition]):
|
||||
def generate_order(self, order: Order):
|
||||
class_objects = []
|
||||
ticket_objects = []
|
||||
for position in ops:
|
||||
for position in order.positions_with_tickets:
|
||||
class_object, ticket_object = self.get_layout(
|
||||
position
|
||||
).pass_layout.generate(position)
|
||||
|
||||
@@ -200,6 +200,7 @@ class GoogleWalletEventTicketStyle(PassStyle):
|
||||
State.ACTIVE,
|
||||
self.event.settings.locales,
|
||||
self.event.settings.locale,
|
||||
order_position=op
|
||||
)
|
||||
fields = self.get_pass_fields(op)
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.models.event import Event
|
||||
from pretix.base.signals import (
|
||||
register_global_settings, register_ticket_outputs,
|
||||
order_canceled,
|
||||
order_changed,
|
||||
order_paid,
|
||||
register_global_settings,
|
||||
register_ticket_outputs,
|
||||
invalidate_ticket_cache,
|
||||
periodic_task,
|
||||
)
|
||||
from pretix.control.signals import item_forms
|
||||
@@ -35,11 +35,19 @@ from .forms import WalletLayoutItemForm
|
||||
from .models import WalletLayoutItem
|
||||
from .ticketoutput import WalletSettingsHolder
|
||||
from .platforms import AVAILABLE_PLATFORMS
|
||||
from .tasks import send_update_apns
|
||||
from .models import AppleWalletPass, CombinedAppleWalletPass
|
||||
from .tasks import get_pass
|
||||
from .models import GoogleWalletInstance
|
||||
from .platforms.google.google_wallet import Resource
|
||||
|
||||
|
||||
|
||||
@receiver(register_ticket_outputs, dispatch_uid="output_wallet")
|
||||
def register_output(sender, **kwargs):
|
||||
return WalletSettingsHolder
|
||||
|
||||
|
||||
for platform in AVAILABLE_PLATFORMS:
|
||||
if hasattr(platform, "get_global_settings"):
|
||||
register_global_settings.connect(
|
||||
@@ -66,7 +74,68 @@ def control_item_forms(sender, request, item, **kwargs):
|
||||
return forms
|
||||
|
||||
|
||||
|
||||
@receiver(periodic_task, dispatch_uid="wallet_periodic")
|
||||
def periodic_tasks(sender, **kwargs):
|
||||
from .tasks import send_update_apns
|
||||
send_update_apns.apply_async()
|
||||
# TODO: move to platform?
|
||||
send_update_apns.apply_async()
|
||||
|
||||
# TODO: move to platform?
|
||||
@receiver(invalidate_ticket_cache, dispatch_uid="invalidate_ticket_cache_wallet_apple")
|
||||
def invalidate_apple(
|
||||
sender: Event, item: int | None, provider: str | None, order: int | None, **kwargs
|
||||
):
|
||||
|
||||
if provider and provider != "wallet":
|
||||
return
|
||||
|
||||
pass_qs = AppleWalletPass.objects.filter(order_position__order__event=sender)
|
||||
passes_qs = CombinedAppleWalletPass.objects.filter(order__event=sender)
|
||||
|
||||
if item:
|
||||
pass_qs = pass_qs.filter(order_position__item_id=item)
|
||||
passes_qs = passes_qs.filter(order__order_position__item_id=item)
|
||||
|
||||
if order:
|
||||
pass_qs = pass_qs.filter(order_position__order_id=order)
|
||||
passes_qs = passes_qs.filter(order_id=order)
|
||||
|
||||
for pass_obj in pass_qs:
|
||||
get_pass.apply_async(
|
||||
kwargs={
|
||||
"model": "orderposition",
|
||||
"pk": pass_obj.order_position_id,
|
||||
"platform_identifier": "apple",
|
||||
}
|
||||
)
|
||||
|
||||
for passes_obj in passes_qs:
|
||||
passes_obj.delete()
|
||||
|
||||
|
||||
|
||||
@receiver(invalidate_ticket_cache, dispatch_uid="invalidate_ticket_cache_wallet_google")
|
||||
def invalidate_google(
|
||||
sender: Event, item: int | None, provider: str | None, order: int | None, **kwargs
|
||||
):
|
||||
|
||||
if provider and provider != "wallet":
|
||||
return
|
||||
|
||||
inst_qs = GoogleWalletInstance.objects.filter(event=sender, resource=Resource.EVENT_TICKET_OBJECT).exclude(usages=0, order_position__isnull=True)
|
||||
|
||||
if item:
|
||||
inst_qs = inst_qs.filter(order_position__item_id=item)
|
||||
|
||||
if order:
|
||||
inst_qs = inst_qs.filter(order_position__order_id=order)
|
||||
|
||||
for inst in inst_qs:
|
||||
if inst.order_position_id:
|
||||
get_pass.apply_async(
|
||||
kwargs={
|
||||
"model": "orderposition",
|
||||
"pk": inst.order_position_id,
|
||||
"platform_identifier": "google",
|
||||
}
|
||||
)
|
||||
@@ -12,6 +12,7 @@ from pretix.plugins.wallet.platforms.base import WalletPlatform
|
||||
from .models import AppleWalletPass, AppleWalletDevice
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
@app.task
|
||||
@scopes_disabled()
|
||||
def send_update_apns():
|
||||
@@ -58,16 +59,20 @@ def send_update_apns():
|
||||
certfile.write(ca_cert)
|
||||
certfile.flush()
|
||||
ctx.load_cert_chain(
|
||||
certfile=certfile.name, password=event.settings.wallet_apple_key_password
|
||||
certfile=certfile.name,
|
||||
password=event.settings.wallet_apple_key_password,
|
||||
)
|
||||
|
||||
client = httpx.Client(verify=ctx, http1=False, http2=True)
|
||||
|
||||
for device in devices_to_push:
|
||||
device.last_push = now()
|
||||
client.post(f"https://api.push.apple.com/3/device/{device.push_token}", json={})
|
||||
client.post(
|
||||
f"https://api.push.apple.com/3/device/{device.push_token}", json={}
|
||||
)
|
||||
device.save()
|
||||
|
||||
|
||||
def get_platform(event, platform) -> WalletPlatform | None:
|
||||
for p in AVAILABLE_PLATFORMS:
|
||||
if p.identifier == platform:
|
||||
@@ -76,11 +81,17 @@ def get_platform(event, platform) -> WalletPlatform | None:
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def get_pass(model: str, pk: int, platform_identifier: str):
|
||||
if model == 'order':
|
||||
order = Order.objects.select_related('event').get(id=pk)
|
||||
if model == "order":
|
||||
order = Order.objects.select_related("event").get(id=pk)
|
||||
platform = get_platform(order.event, platform_identifier)
|
||||
return platform.generate_ops(order.positions_with_tickets)
|
||||
elif model == 'orderposition':
|
||||
position = OrderPosition.objects.select_related('order', 'order__event').get(id=pk)
|
||||
if not platform:
|
||||
return
|
||||
return platform.generate_order(order)
|
||||
elif model == "orderposition":
|
||||
position = OrderPosition.objects.select_related("order", "order__event").get(
|
||||
id=pk
|
||||
)
|
||||
platform = get_platform(position.order.event, platform_identifier)
|
||||
if not platform:
|
||||
return
|
||||
return platform.generate(position)
|
||||
@@ -31,6 +31,8 @@ from .views.apple import (
|
||||
DeviceRegistrationView,
|
||||
GetPassView,
|
||||
LogView,
|
||||
DownloadPassView,
|
||||
DownloadCombinedPassView
|
||||
)
|
||||
from .views.control import (
|
||||
LayoutCreateView,
|
||||
@@ -108,12 +110,21 @@ event_patterns = [
|
||||
CallbackView.as_view(),
|
||||
name="google_webhook",
|
||||
),
|
||||
# TODO(wallet): actually provide these views
|
||||
re_path(
|
||||
r"^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<position>[0-9]+)/(?P<platform>[^/]+)$",
|
||||
PresaleWalletGetPass.as_view(),
|
||||
name="presale_event.order.wallet.get",
|
||||
),
|
||||
re_path(
|
||||
r"^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/wallet/apple/download/(?P<position>[0-9]+)/$",
|
||||
DownloadPassView.as_view(),
|
||||
name="apple_download",
|
||||
),
|
||||
re_path(
|
||||
r"^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/wallet/apple/download/$",
|
||||
DownloadCombinedPassView.as_view(),
|
||||
name="apple_download_combined",
|
||||
),
|
||||
re_path(
|
||||
r"^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<platform>[^/]+)$",
|
||||
PresaleWalletGetPass.as_view(),
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
import logging
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.http import condition
|
||||
from django.http.response import Http404
|
||||
from django.views import View
|
||||
from rest_framework import exceptions, permissions, serializers
|
||||
from rest_framework.authentication import (
|
||||
BaseAuthentication, get_authorization_header,
|
||||
@@ -12,8 +16,11 @@ from rest_framework.authentication import (
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.base.models.orders import OrderPosition
|
||||
from pretix.helpers.http import ChunkBasedFileResponse
|
||||
from pretix.plugins.wallet.models import AppleWalletDevice, AppleWalletPass
|
||||
from pretix.plugins.wallet.models import AppleWalletDevice, AppleWalletPass, CombinedAppleWalletPass
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views.order import OrderDetailMixin
|
||||
|
||||
logger = logging.getLogger("pretix.plugins.wallet.apple")
|
||||
|
||||
@@ -166,5 +173,31 @@ class LogView(APIView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = LogEntrySerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
logger.debug("Received apple wallet log messages: %s", serializer.data["logs"])
|
||||
logger.info("Received apple wallet log messages: %s", serializer.data["logs"])
|
||||
return Response({})
|
||||
|
||||
|
||||
class DownloadPassView(EventViewMixin, OrderDetailMixin, View):
|
||||
@cached_property
|
||||
def order_position(self):
|
||||
try:
|
||||
return self.order.positions.get(pk=self.kwargs.get('position'))
|
||||
except OrderPosition.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.order_position:
|
||||
return Http404()
|
||||
pass_obj = get_object_or_404(AppleWalletPass, order_position=self.order_position)
|
||||
resp = ChunkBasedFileResponse(pass_obj.content.file, content_type="application/vnd.apple.pkpass")
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(pass_obj.serial_number + ".pkpass").encode('ascii', 'ignore')
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
class DownloadCombinedPassView(EventViewMixin, OrderDetailMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
pass_obj = get_object_or_404(CombinedAppleWalletPass, order=self.order)
|
||||
resp = ChunkBasedFileResponse(pass_obj.file.file, content_type="application/vnd.apple.pkpasses")
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.order.code + ".pkpasses").encode('ascii', 'ignore')
|
||||
return resp
|
||||
@@ -1,41 +1,25 @@
|
||||
|
||||
|
||||
from django.http import Http404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import (
|
||||
View,
|
||||
)
|
||||
from pretix.base.models.orders import OrderPosition
|
||||
from pretix.base.services.orders import OrderError
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from django.http import (
|
||||
Http404,
|
||||
)
|
||||
from django.http import Http404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import View
|
||||
|
||||
from pretix.base.models import (
|
||||
OrderPosition,
|
||||
)
|
||||
from pretix.base.services.orders import (
|
||||
OrderError,
|
||||
)
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.base.services.orders import OrderError
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.views import (
|
||||
EventViewMixin,
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views.order import (
|
||||
OrderDetailMixin, OrderPositionDetailMixin,
|
||||
)
|
||||
from pretix.presale.views.order import OrderDetailMixin, OrderPositionDetailMixin
|
||||
|
||||
from .control import OrderDownloadMixin
|
||||
|
||||
|
||||
class PresaleOrderDownloadMixin(OrderDownloadMixin):
|
||||
known_errortypes = ['OrderError']
|
||||
|
||||
@@ -86,6 +70,7 @@ class PresaleWalletGetPass(PresaleOrderDownloadMixin, EventViewMixin, OrderDetai
|
||||
except OrderPosition.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class PresaleWalletGetPassPosition(PresaleOrderDownloadMixin, EventViewMixin, OrderPositionDetailMixin, AsyncAction, View):
|
||||
def get_error_url(self):
|
||||
@@ -105,4 +90,4 @@ class PresaleWalletGetPassPosition(PresaleOrderDownloadMixin, EventViewMixin, Or
|
||||
)
|
||||
)
|
||||
except OrderPosition.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
@@ -43,8 +43,6 @@ import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote, urlencode
|
||||
from datetime import timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -64,14 +62,12 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import ListView, TemplateView, View
|
||||
from pretixeu.views import reverse
|
||||
|
||||
from pretix.base.auth import has_event_access_permission
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Checkin, GiftCard, Invoice, Order, OrderPosition, Quota,
|
||||
TaxRule,
|
||||
)
|
||||
from pretix.base.models.base import CachedFile
|
||||
from pretix.base.models.orders import (
|
||||
CachedCombinedTicket, InvoiceAddress, OrderFee, OrderPayment, OrderRefund,
|
||||
QuestionAnswer,
|
||||
@@ -1208,32 +1204,15 @@ class OrderDownloadMixin:
|
||||
):
|
||||
return self.error(OrderError(_('Please click the link we sent you via email to download your tickets.')))
|
||||
|
||||
if self.output.is_cacheable and (ct := self.get_last_ct()):
|
||||
if ct := self.get_last_ct():
|
||||
return self.success(ct)
|
||||
|
||||
if not self.output.is_cacheable:
|
||||
cf = CachedFile(web_download=True, date=now(), expires=now() + timedelta(hours=2))
|
||||
cf.save()
|
||||
else:
|
||||
cf = None
|
||||
|
||||
return self.do('orderposition' if 'position' in kwargs else 'order',
|
||||
self.order_position.pk if 'position' in kwargs else self.order.pk,
|
||||
self.output.identifier, cf.pk if cf else None)
|
||||
self.output.identifier)
|
||||
|
||||
def get_success_url(self, value):
|
||||
if isinstance(value, UUID):
|
||||
cf = CachedFile.objects.filter(pk=value).first()
|
||||
else:
|
||||
cf = None
|
||||
|
||||
if not cf or not cf.allowed_for_session(self.request):
|
||||
return self.get_self_url()
|
||||
|
||||
if cf.type == 'text/uri-list':
|
||||
return cf.file.file.read().decode()
|
||||
else:
|
||||
return reverse('cachedfile.download', kwargs={"id":str(value)})
|
||||
return self.get_self_url()
|
||||
|
||||
def get_success_message(self, value):
|
||||
return ""
|
||||
@@ -1275,17 +1254,14 @@ class OrderDownloadMixin:
|
||||
return redirect(self.get_success_url(value))
|
||||
|
||||
def get_last_ct(self):
|
||||
if self.output.is_cacheable:
|
||||
if 'position' in self.kwargs:
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
else:
|
||||
ct = CachedCombinedTicket.objects.filter(
|
||||
order=self.order, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
if 'position' in self.kwargs:
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
else:
|
||||
ct = None
|
||||
ct = CachedCombinedTicket.objects.filter(
|
||||
order=self.order, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
if not ct or not ct.file:
|
||||
return None
|
||||
return ct
|
||||
|
||||
Reference in new issue
Block a user