diff --git a/django_walletpass/admin.py b/django_walletpass/admin.py
index a720e15..f9a53e9 100644
--- a/django_walletpass/admin.py
+++ b/django_walletpass/admin.py
@@ -1,6 +1,104 @@
from django.contrib import admin
-from django_walletpass.models import Pass, Registration, Log
+from django.template import Context, Template
+from django.urls import reverse
+from django.utils.html import format_html
-admin.site.register(Pass)
-admin.site.register(Registration)
-admin.site.register(Log)
+from django_walletpass.models import Log, Pass, Registration
+
+
+@admin.register(Log)
+class LogAdmin(admin.ModelAdmin):
+ list_display = (
+ "created_at",
+ "status",
+ "task_type",
+ # "pass_type_identifier",
+ # "serial_number",
+ "pass_",
+ # "web_service_url",
+ "device_id",
+ "msg",
+ )
+ list_filter = ("status", "task_type", "pass_type_identifier")
+ search_fields = (
+ "pass_type_identifier",
+ "serial_number",
+ "device_id",
+ "msg",
+ "message",
+ )
+ readonly_fields = ("created_at", "pass_")
+ raw_id_fields = ("pazz",)
+ list_select_related = ("pazz",)
+
+ def pass_(self, obj: Log):
+ if obj.pazz_id:
+ url = reverse(
+ "admin:%s_%s_change"
+ % (obj.pazz._meta.app_label, obj.pazz._meta.model_name),
+ args=[obj.pazz_id],
+ )
+ return format_html(
+ "{title}",
+ url=url,
+ title=obj.serial_number,
+ )
+ return obj.serial_number
+
+ pass_.short_description = "Pass"
+
+
+@admin.register(Pass)
+class PassAdmin(admin.ModelAdmin):
+ list_display = (
+ "serial_number",
+ "updated_at",
+ "pass_type_identifier",
+ "wallet_pass_",
+ )
+ search_fields = (
+ "serial_number",
+ "pass_type_identifier",
+ "authentication_token",
+ "data",
+ )
+ list_filter = ("pass_type_identifier", "updated_at")
+ date_hierarchy = "updated_at"
+ readonly_fields = ("wallet_pass_", "updated_at")
+
+ def wallet_pass_(self, obj: Pass):
+ if obj.data:
+ return format_html(
+ Template(
+ "{% load static %}"
+ ).render(Context({})),
+ url=obj.data.url,
+ title=obj.data.name,
+ )
+ return
+
+ wallet_pass_.short_description = "Pass"
+
+
+@admin.register(Registration)
+class RegistrationAdmin(admin.ModelAdmin):
+ list_display = ("device_library_identifier", "push_token", "pass_")
+ search_fields = ("device_library_identifier", "push_token", "pazz__serial_number")
+ raw_id_fields = ("pazz",)
+ readonly_fields = ("pass_",)
+
+ def pass_(self, obj: Registration):
+ if obj.pazz_id:
+ url = reverse(
+ "admin:%s_%s_change"
+ % (obj.pazz._meta.app_label, obj.pazz._meta.model_name),
+ args=[obj.pazz_id],
+ )
+ return format_html(
+ "{title}",
+ url=url,
+ title=obj.pazz.serial_number,
+ )
+ return
+
+ pass_.short_description = "Pass"
diff --git a/django_walletpass/apps.py b/django_walletpass/apps.py
index a715e0f..10d6127 100644
--- a/django_walletpass/apps.py
+++ b/django_walletpass/apps.py
@@ -3,6 +3,7 @@
class DjangoWalletpassConfig(AppConfig):
name = 'django_walletpass'
+ verbose_name = 'Django walletpass'
def ready(self):
from django_walletpass import signals as _signals # pylint: disable=import-outside-toplevel
diff --git a/django_walletpass/classviews.py b/django_walletpass/classviews.py
index d7813a3..3bffc6b 100644
--- a/django_walletpass/classviews.py
+++ b/django_walletpass/classviews.py
@@ -142,5 +142,6 @@ class LogViewSet(viewsets.ViewSet):
def create(self, request):
json_body = json.loads(request.body)
for message in json_body['logs']:
- Log(message=message).save()
+ log = Log(message=message)
+ Log.parse_log(log, message)
return Response({}, status=status.HTTP_200_OK)
diff --git a/django_walletpass/migrations/0009_auto_20230709_2143.py b/django_walletpass/migrations/0009_auto_20230709_2143.py
new file mode 100644
index 0000000..6d21b0e
--- /dev/null
+++ b/django_walletpass/migrations/0009_auto_20230709_2143.py
@@ -0,0 +1,66 @@
+# Generated by Django 3.2.14 on 2023-07-09 18:43
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import django_walletpass.storage
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_walletpass', '0008_alter_pass_data'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='log',
+ name='created_at',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='device_id',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='msg',
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='pass_type_identifier',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='pazz',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='django_walletpass.pass'),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='serial_number',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='status',
+ field=models.CharField(blank=True, max_length=100, null=True),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='task_type',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name='log',
+ name='web_service_url',
+ field=models.URLField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='pass',
+ name='data',
+ field=models.FileField(storage=django_walletpass.storage.WalletPassStorage(), upload_to='passes'),
+ ),
+ ]
diff --git a/django_walletpass/migrations/0010_alter_registration_push_token.py b/django_walletpass/migrations/0010_alter_registration_push_token.py
new file mode 100644
index 0000000..9862c75
--- /dev/null
+++ b/django_walletpass/migrations/0010_alter_registration_push_token.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.14 on 2023-07-13 10:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_walletpass', '0009_auto_20230709_2143'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='registration',
+ name='push_token',
+ field=models.CharField(max_length=255),
+ ),
+ ]
diff --git a/django_walletpass/models.py b/django_walletpass/models.py
index a96c968..d35a95f 100644
--- a/django_walletpass/models.py
+++ b/django_walletpass/models.py
@@ -1,4 +1,6 @@
+import datetime
import os
+import re
import uuid
import hashlib
import json
@@ -7,6 +9,7 @@
import zipfile
from glob import glob
from django.core.exceptions import ValidationError
+from django.utils import timezone
from django.utils.module_loading import import_string
from django.db import models
from django.utils.translation import gettext_lazy as _
@@ -289,6 +292,9 @@ def get_pass_builder(self):
def __unicode__(self):
return self.serial_number
+ def __str__(self):
+ return self.serial_number
+
class Meta:
verbose_name_plural = "passes"
unique_together = (
@@ -302,7 +308,7 @@ class Registration(models.Model):
Registration of a Pass on a device
"""
device_library_identifier = models.CharField(max_length=150)
- push_token = models.CharField(max_length=150)
+ push_token = models.CharField(max_length=255)
pazz = models.ForeignKey(
Pass,
on_delete=models.CASCADE,
@@ -312,12 +318,83 @@ class Registration(models.Model):
def __unicode__(self):
return self.device_library_identifier
+ def __str__(self):
+ return self.device_library_identifier
+
class Log(models.Model):
"""
Log message sent by a device
"""
+ created_at = models.DateTimeField(default=timezone.now)
+ status = models.CharField(max_length=100, null=True, blank=True)
+ task_type = models.CharField(max_length=255, null=True, blank=True)
+ pass_type_identifier = models.CharField(max_length=255, null=True, blank=True)
+ serial_number = models.CharField(max_length=255, null=True, blank=True)
+ pazz = models.ForeignKey(Pass, null=True, blank=True, on_delete=models.CASCADE, related_name='logs')
+ web_service_url = models.URLField(null=True, blank=True)
+ device_id = models.CharField(max_length=255, null=True, blank=True)
+ msg = models.TextField(null=True, blank=True)
message = models.TextField()
def __unicode__(self):
return self.message
+
+ def __str__(self):
+ return self.created_at.strftime('%d/%m/%y %H:%M:%S')
+
+ @classmethod
+ def parse_log(cls, log, message):
+ pattern_register = r"\[(.*?)\]\s(.*?)\s\(for device (.*?), pass type (.*?), serial number (.*?); with web service url (.*?)\)\s(.*?): (.*$)"
+ pattern_get = r"\[(.*?)\]\s(.*?)\s\(pass type (.*?), serial number (.*?), if-modified-since \(.*?\); with web service url (.*?)\) (.*?): (.*$)"
+ pattern_web_service_error = r"\[(.*?)\]\s(.*?)\sfor (.*?)\s\((.*?)\):\s(.*$)"
+ pattern_get_warning = r"\[(.*?)\]\s(.*?)\s\(pass type (.*?), serial number (.*?), if-modified-since \(.*?\); with web service url (.*?)\) (.*?): (.*\.)\s(.*$)"
+
+ match_register = re.match(pattern_register, message)
+ match_get = re.match(pattern_get, message)
+ match_web_service_error = re.match(pattern_web_service_error, message)
+ match_get_warning = re.match(pattern_get_warning, message)
+
+ if match_register:
+ timestamp_str, task_type, device_id, pass_type_identifier, serial_number, web_service_url, status, msg = match_register.groups()
+ elif match_get:
+ timestamp_str, task_type, pass_type_identifier, serial_number, web_service_url, status, msg = match_get.groups()
+ device_id = None # 'Get pass task' entries don't include device_id
+ elif match_web_service_error:
+ timestamp_str, task_type, pass_type_identifier, web_service_url, msg = match_web_service_error.groups()
+ serial_number = None
+ device_id = None
+ status = "error"
+ elif match_get_warning:
+ timestamp_str, task_type, pass_type_identifier, serial_number, web_service_url, status, msg = match_get_warning.groups()
+ device_id = None
+ status = "warning"
+ else:
+ log.status = 'unknown'
+ log.message = message
+ log.save()
+ return # Log entry didn't match any known pattern
+
+ if 'error' in status:
+ status = 'error'
+ elif 'warning' in status:
+ status = 'warning'
+
+ log.created_at = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S %z")
+ log.status = status
+ log.task_type = task_type
+ log.device_id = device_id
+ log.pass_type_identifier = pass_type_identifier
+ log.serial_number = serial_number
+ log.web_service_url = web_service_url
+ log.msg = msg
+ log.message = message
+
+ if serial_number:
+ try:
+ pazz = Pass.objects.get(serial_number=serial_number)
+ log.pazz = pazz
+ except Pass.DoesNotExist:
+ pass
+
+ log.save()
diff --git a/django_walletpass/services.py b/django_walletpass/services.py
index b3ea237..7c4985e 100644
--- a/django_walletpass/services.py
+++ b/django_walletpass/services.py
@@ -12,7 +12,11 @@
class PushBackend:
def __init__(self):
- self.loop = asyncio.get_event_loop()
+ try:
+ self.loop = asyncio.get_event_loop()
+ except RuntimeError:
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
async def push_notification(self, client, token):
diff --git a/django_walletpass/static/admin/passbook_icon.svg b/django_walletpass/static/admin/passbook_icon.svg
new file mode 100644
index 0000000..555054e
--- /dev/null
+++ b/django_walletpass/static/admin/passbook_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/django_walletpass/tests/main.py b/django_walletpass/tests/main.py
index b774f78..d5e659e 100644
--- a/django_walletpass/tests/main.py
+++ b/django_walletpass/tests/main.py
@@ -1,31 +1,64 @@
from unittest import mock
from dateutil.parser import parse
+from django.contrib import admin
from django.test import TestCase
from django.utils import timezone
+
from django_walletpass import crypto
+from django_walletpass.admin import PassAdmin
from django_walletpass.classviews import FORMAT
from django_walletpass.models import Pass, PassBuilder, Registration
from django_walletpass.settings import dwpconfig as WALLETPASS_CONF
-class ClassViewsTestCase(TestCase):
+class AdminTestCase(TestCase):
+ def test_wallet_pass(self):
+ admin_view = PassAdmin(Pass, admin.site)
+ builder = PassBuilder()
+ builder.pass_data = {
+ "formatVersion": 1,
+ "barcode": {
+ "message": "123456789",
+ "format": "PKBarcodeFormatPDF417",
+ "messageEncoding": "iso-8859-1",
+ },
+ "organizationName": "Organic Produce",
+ "description": "Organic Produce Loyalty Card",
+ "logoText": "Organic Produce",
+ "foregroundColor": "rgb(255, 255, 255)",
+ "backgroundColor": "rgb(55, 117, 50)",
+ }
+
+ builder.build()
+
+ instance = builder.write_to_model()
+ instance.save()
+
+ self.assertEqual(
+ admin_view.wallet_pass_(instance)[-48:],
+ "",
+ )
+
+class ClassViewsTestCase(TestCase):
def test_format_parse(self):
- """ ensure dateutil reads FORMAT properly """
+ """ensure dateutil reads FORMAT properly"""
now = timezone.now()
now_string = now.strftime(FORMAT)
- self.assertEqual(parse(now_string), timezone.make_naive(now).replace(microsecond=0))
+ self.assertEqual(
+ parse(now_string), timezone.make_naive(now).replace(microsecond=0)
+ )
class CryptoTestCase(TestCase):
def test_smime_sign(self):
crypto.pkcs7_sign(
- certcontent=WALLETPASS_CONF['CERT_CONTENT'],
- keycontent=WALLETPASS_CONF['KEY_CONTENT'],
- wwdr_certificate=WALLETPASS_CONF['WWDRCA_PEM_CONTENT'],
- data=b'data to be signed',
- key_password=WALLETPASS_CONF['KEY_PASSWORD'],
+ certcontent=WALLETPASS_CONF["CERT_CONTENT"],
+ keycontent=WALLETPASS_CONF["KEY_CONTENT"],
+ wwdr_certificate=WALLETPASS_CONF["WWDRCA_PEM_CONTENT"],
+ data=b"data to be signed",
+ key_password=WALLETPASS_CONF["KEY_PASSWORD"],
)
@@ -37,7 +70,7 @@ def test_build_pkpass(self):
"barcode": {
"message": "123456789",
"format": "PKBarcodeFormatPDF417",
- "messageEncoding": "iso-8859-1"
+ "messageEncoding": "iso-8859-1",
},
"organizationName": "Organic Produce",
"description": "Organic Produce Loyalty Card",
@@ -57,7 +90,7 @@ def test_build_pkpass(self):
self.assertEqual(builder.manifest_dict, builder2.manifest_dict)
self.assertEqual(builder.pass_data, builder2.pass_data)
- builder2.pass_data.update({"organizationName": 'test'})
+ builder2.pass_data.update({"organizationName": "test"})
builder2.build()
builder2.write_to_model(instance)
instance.save()
diff --git a/django_walletpass/urls.py b/django_walletpass/urls.py
index 53028e4..883bbfa 100644
--- a/django_walletpass/urls.py
+++ b/django_walletpass/urls.py
@@ -1,4 +1,4 @@
-from django.urls import re_path
+from django.urls import re_path, path
from . import classviews
urlpatterns = [