Compare commits
4 Commits
eb42e2515b
...
6408748c3f
Author | SHA1 | Date | |
---|---|---|---|
6408748c3f | |||
7f0de3a16f | |||
96bcc80516 | |||
215e2946d1 |
13
cmsmanage/django_q2_helper.py
Normal file
13
cmsmanage/django_q2_helper.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_scheduled(name: str, func, **kwargs):
|
||||||
|
Schedule.objects.update_or_create(
|
||||||
|
name=name,
|
||||||
|
defaults={
|
||||||
|
"func": f"{func.__module__}.{func.__qualname__}",
|
||||||
|
**kwargs,
|
||||||
|
},
|
||||||
|
)
|
@ -30,6 +30,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django_admin_logs",
|
"django_admin_logs",
|
||||||
|
"django_object_actions",
|
||||||
"widget_tweaks",
|
"widget_tweaks",
|
||||||
"markdownx",
|
"markdownx",
|
||||||
"recurrence",
|
"recurrence",
|
||||||
@ -120,9 +121,15 @@ REST_FRAMEWORK = {
|
|||||||
Q_CLUSTER = {
|
Q_CLUSTER = {
|
||||||
"name": "cmsmanage",
|
"name": "cmsmanage",
|
||||||
"orm": "default",
|
"orm": "default",
|
||||||
"retry": 360,
|
"retry": 60 * 6,
|
||||||
"timeout": 300,
|
"timeout": 60 * 5,
|
||||||
|
"catch_up": False,
|
||||||
"ALT_CLUSTERS": {
|
"ALT_CLUSTERS": {
|
||||||
"internal": {},
|
"internal": {
|
||||||
|
"retry": 60 * 60,
|
||||||
|
"timeout": 60 * 59,
|
||||||
|
"ack_failures": True,
|
||||||
|
"max_attempts": 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,6 @@ DEBUG = True
|
|||||||
INTERNAL_IPS = ["127.0.0.1"]
|
INTERNAL_IPS = ["127.0.0.1"]
|
||||||
|
|
||||||
INSTALLED_APPS.append("debug_toolbar")
|
INSTALLED_APPS.append("debug_toolbar")
|
||||||
|
INSTALLED_APPS.append("django_extensions")
|
||||||
|
|
||||||
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from django_object_actions import DjangoObjectActions, action
|
||||||
|
|
||||||
from .models import Door, HIDEvent
|
from .models import Door, HIDEvent
|
||||||
|
from .tasks.scrapehidevents import q_getMessagesAllDoors
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Door)
|
@admin.register(Door)
|
||||||
@ -26,7 +29,7 @@ class IsRedFilter(admin.SimpleListFilter):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(HIDEvent)
|
@admin.register(HIDEvent)
|
||||||
class HIDEventAdmin(admin.ModelAdmin):
|
class HIDEventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
||||||
search_fields = ["forename", "surname", "cardholder_id"]
|
search_fields = ["forename", "surname", "cardholder_id"]
|
||||||
list_display = ["timestamp", "door", "event_type", "description", "is_red"]
|
list_display = ["timestamp", "door", "event_type", "description", "is_red"]
|
||||||
list_filter = [
|
list_filter = [
|
||||||
@ -36,6 +39,7 @@ class HIDEventAdmin(admin.ModelAdmin):
|
|||||||
IsRedFilter,
|
IsRedFilter,
|
||||||
]
|
]
|
||||||
readonly_fields = ["decoded_card_number"]
|
readonly_fields = ["decoded_card_number"]
|
||||||
|
changelist_actions = ("refresh_all_doors",)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super().get_queryset(request).with_is_red().with_decoded_card_number()
|
return super().get_queryset(request).with_is_red().with_decoded_card_number()
|
||||||
@ -52,3 +56,11 @@ class HIDEventAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@action(label="Refresh All Doors")
|
||||||
|
def refresh_all_doors(self, request, obj):
|
||||||
|
q_getMessagesAllDoors()
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Queued refresh, please wait a few seconds/minutes then refresh the page",
|
||||||
|
)
|
||||||
|
@ -5,3 +5,8 @@ class DoorControlConfig(AppConfig):
|
|||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "doorcontrol"
|
name = "doorcontrol"
|
||||||
verbose_name = "Door Control"
|
verbose_name = "Door Control"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from .tasks.scrapehidevents import schedule_tasks
|
||||||
|
|
||||||
|
schedule_tasks()
|
||||||
|
41
doorcontrol/hid/Credential.py
Normal file
41
doorcontrol/hid/Credential.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import bitstring
|
||||||
|
|
||||||
|
# Reference for H10301 card format:
|
||||||
|
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
||||||
|
|
||||||
|
|
||||||
|
class Credential:
|
||||||
|
def __init__(self, code=None, hex=None):
|
||||||
|
if code is None and hex is None:
|
||||||
|
raise TypeError("Must set either code or hex for a Credential")
|
||||||
|
elif code is not None and hex is not None:
|
||||||
|
raise TypeError("Cannot set both code and hex for a Credential")
|
||||||
|
elif code is not None:
|
||||||
|
self.bits = bitstring.pack(
|
||||||
|
"0b000000, 0b0, uint:8=facility, uint:16=number, 0b0",
|
||||||
|
facility=code[0],
|
||||||
|
number=code[1],
|
||||||
|
)
|
||||||
|
self.bits[6] = self.bits[7:19].count(1) % 2 # even parity
|
||||||
|
self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity
|
||||||
|
elif hex is not None:
|
||||||
|
self.bits = bitstring.Bits(hex=hex)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Credential({self.code})"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.bits == other.bits
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.bits.int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code(self):
|
||||||
|
facility = self.bits[7:15].uint
|
||||||
|
code = self.bits[15:31].uint
|
||||||
|
return (facility, code)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hex(self):
|
||||||
|
return self.bits.hex.upper()
|
239
doorcontrol/hid/DoorController.py
Normal file
239
doorcontrol/hid/DoorController.py
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import csv
|
||||||
|
from datetime import datetime
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
from lxml import etree
|
||||||
|
from lxml.builder import ElementMaker
|
||||||
|
|
||||||
|
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
|
||||||
|
E = ElementMaker(
|
||||||
|
namespace="http://www.hidglobal.com/VertX",
|
||||||
|
nsmap={"hid": "http://www.hidglobal.com/VertX"},
|
||||||
|
)
|
||||||
|
E_corp = ElementMaker(
|
||||||
|
namespace="http://www.hidcorp.com/VertX", # stupid
|
||||||
|
nsmap={"hid": "http://www.hidcorp.com/VertX"},
|
||||||
|
)
|
||||||
|
ROOT = E_plain.VertXMessage
|
||||||
|
|
||||||
|
fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDate,Forename,Initial,Surname,Email,Phone,Custom1,Custom2,Schedule1,Schedule2,Schedule3,Schedule4,Schedule5,Schedule6,Schedule7,Schedule8".split(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: where should this live?
|
||||||
|
# it's fine, ssl certs are for losers anyway
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteError(Exception):
|
||||||
|
def __init__(self, r):
|
||||||
|
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
||||||
|
|
||||||
|
|
||||||
|
class DoorController:
|
||||||
|
def __init__(self, ip, username, password):
|
||||||
|
self.ip = ip
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def doImport(self, params=None, files=None):
|
||||||
|
"""Send a request to the door control import script"""
|
||||||
|
r = requests.post(
|
||||||
|
"https://" + self.ip + "/cgi-bin/import.cgi",
|
||||||
|
params=params,
|
||||||
|
files=files,
|
||||||
|
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||||
|
timeout=60,
|
||||||
|
verify=False,
|
||||||
|
) # ignore insecure SSL
|
||||||
|
xml = etree.XML(r.content)
|
||||||
|
if (
|
||||||
|
r.status_code != 200
|
||||||
|
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0
|
||||||
|
):
|
||||||
|
raise RemoteError(r)
|
||||||
|
|
||||||
|
def doCSVImport(self, csv):
|
||||||
|
"""Do the CSV import procedure on a door control"""
|
||||||
|
self.doImport({"task": "importInit"})
|
||||||
|
self.doImport(
|
||||||
|
{"task": "importCardsPeople", "name": "cardspeopleschedule.csv"},
|
||||||
|
{"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, "text/csv")},
|
||||||
|
)
|
||||||
|
self.doImport({"task": "importDone"})
|
||||||
|
|
||||||
|
def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'):
|
||||||
|
if not isinstance(xml, bytes):
|
||||||
|
xml = etree.tostring(xml)
|
||||||
|
r = requests.get(
|
||||||
|
"https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
|
||||||
|
params={"XML": prefix + xml},
|
||||||
|
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
resp_xml = etree.XML(r.content)
|
||||||
|
# probably meed to be more sane about this
|
||||||
|
if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0:
|
||||||
|
raise RemoteError(r)
|
||||||
|
return resp_xml
|
||||||
|
|
||||||
|
def get_scheduleMap(self):
|
||||||
|
schedules = self.doXMLRequest(
|
||||||
|
ROOT(E.Schedules({"action": "LR", "recordOffset": "0", "recordCount": "8"}))
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] for fmt in schedules[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_schedules(self):
|
||||||
|
# TODO: might be able to do in one request
|
||||||
|
schedules = self.doXMLRequest(ROOT(E.Schedules({"action": "LR"})))
|
||||||
|
etree.dump(schedules)
|
||||||
|
|
||||||
|
data = self.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
*[
|
||||||
|
E.Schedules(
|
||||||
|
{"action": "LR", "scheduleID": schedule.attrib["scheduleID"]}
|
||||||
|
)
|
||||||
|
for schedule in schedules[0]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ROOT(E_corp.Schedules({"action": "AD"}, *[s[0] for s in data]))
|
||||||
|
|
||||||
|
def set_schedules(self, schedules):
|
||||||
|
# clear all people
|
||||||
|
outString = StringIO()
|
||||||
|
writer = csv.DictWriter(outString, fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerow({})
|
||||||
|
outString.seek(0)
|
||||||
|
self.doCSVImport(outString)
|
||||||
|
|
||||||
|
# clear all schedules
|
||||||
|
delXML = ROOT(
|
||||||
|
*[
|
||||||
|
E.Schedules({"action": "DD", "scheduleID": str(ii)})
|
||||||
|
for ii in range(1, 8)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.doXMLRequest(delXML)
|
||||||
|
except RemoteError:
|
||||||
|
# don't care about failure to delete, they probably just didn't exist
|
||||||
|
pass
|
||||||
|
|
||||||
|
# load new schedules
|
||||||
|
self.doXMLRequest(schedules)
|
||||||
|
|
||||||
|
def get_cardFormats(self):
|
||||||
|
cardFormats = self.doXMLRequest(
|
||||||
|
ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
fmt[0].attrib["value"]: fmt.attrib["formatID"]
|
||||||
|
for fmt in cardFormats[0].findall("{*}CardFormat[{*}FixedField]")
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_cardFormat(self, formatName, templateID, facilityCode):
|
||||||
|
# TODO: add ability to delete formats
|
||||||
|
# delete example: <hid:CardFormats action="DD" formatID="7-1-244"/>
|
||||||
|
|
||||||
|
el = ROOT(
|
||||||
|
E.CardFormats(
|
||||||
|
{"action": "AD"},
|
||||||
|
E.CardFormat(
|
||||||
|
{"formatName": formatName, "templateID": str(templateID)},
|
||||||
|
E.FixedField({"value": str(facilityCode)}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return self.doXMLRequest(el)
|
||||||
|
|
||||||
|
def get_records(self, req, count, params={}, stopFunction=None):
|
||||||
|
recordCount = 0
|
||||||
|
moreRecords = True
|
||||||
|
|
||||||
|
# note: all the "+/-1" bits are to work around a bug where the
|
||||||
|
# last returned entry is incomplete. There is probably a
|
||||||
|
# better way to do this, but for now I just get the last entry
|
||||||
|
# again in the next request. I suspect this probably ends
|
||||||
|
# poorly if the numbers line up poorly (ie an exact multiple
|
||||||
|
# of the returned record limit)
|
||||||
|
while True:
|
||||||
|
res = self.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
req(
|
||||||
|
{
|
||||||
|
"action": "LR",
|
||||||
|
"recordCount": str(count - recordCount + 1),
|
||||||
|
"recordOffset": str(
|
||||||
|
recordCount - 1 if recordCount > 0 else 0
|
||||||
|
),
|
||||||
|
**params,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recordCount += int(res[0].get("recordCount")) - 1
|
||||||
|
moreRecords = res[0].get("moreRecords") == "true"
|
||||||
|
|
||||||
|
if moreRecords and (stopFunction is None or stopFunction(list(res[0]))):
|
||||||
|
yield list(res[0])[:-1]
|
||||||
|
else:
|
||||||
|
yield list(res[0])
|
||||||
|
break
|
||||||
|
|
||||||
|
def get_cardholders(self):
|
||||||
|
for page in self.get_records(
|
||||||
|
E.Cardholders, 1000, {"responseFormat": "expanded"}
|
||||||
|
):
|
||||||
|
yield from page
|
||||||
|
|
||||||
|
def get_credentials(self):
|
||||||
|
for page in self.get_records(E.Credentials, 1000):
|
||||||
|
yield from page
|
||||||
|
|
||||||
|
def update_credential(self, rawCardNumber: str, cardholderID: str):
|
||||||
|
return self.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
E.Credentials(
|
||||||
|
{
|
||||||
|
"action": "UD",
|
||||||
|
"rawCardNumber": rawCardNumber,
|
||||||
|
"isCard": "true",
|
||||||
|
},
|
||||||
|
E.Credential({"cardholderID": cardholderID}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_events(self, threshold):
|
||||||
|
def event_newer_than_threshold(event):
|
||||||
|
return datetime.fromisoformat(event.attrib["timestamp"]) > threshold
|
||||||
|
|
||||||
|
# These door controllers only store 5000 events max
|
||||||
|
for page in self.get_records(
|
||||||
|
E.EventMessages,
|
||||||
|
5000,
|
||||||
|
stopFunction=lambda events: event_newer_than_threshold(events[-1]),
|
||||||
|
):
|
||||||
|
events = [event for event in page if event_newer_than_threshold(event)]
|
||||||
|
if events:
|
||||||
|
yield events
|
||||||
|
|
||||||
|
def get_lock(self):
|
||||||
|
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||||
|
xml = self.doXMLRequest(el)
|
||||||
|
relayState = xml.find("./{*}Doors/{*}Door").attrib["relayState"]
|
||||||
|
return "unlocked" if relayState == "set" else "locked"
|
||||||
|
|
||||||
|
def set_lock(self, lock=True):
|
||||||
|
el = ROOT(
|
||||||
|
E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"})
|
||||||
|
)
|
||||||
|
return self.doXMLRequest(el)
|
0
doorcontrol/hid/__init__.py
Normal file
0
doorcontrol/hid/__init__.py
Normal file
18
doorcontrol/migrations/0003_door_ip.py
Normal file
18
doorcontrol/migrations/0003_door_ip.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-09-19 15:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("doorcontrol", "0002_door_remove_hidevent_door_name_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="door",
|
||||||
|
name="ip",
|
||||||
|
field=models.GenericIPAddressField(default="", protocol="IPv4"),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -1,10 +1,14 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import ExpressionWrapper, F, Func, Q
|
from django.db.models import ExpressionWrapper, F, Func, Q
|
||||||
from django.db.models.functions import Mod
|
from django.db.models.functions import Mod
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class Door(models.Model):
|
class Door(models.Model):
|
||||||
name = models.CharField(max_length=64, unique=True)
|
name = models.CharField(max_length=64, unique=True)
|
||||||
|
ip = models.GenericIPAddressField(protocol="IPv4")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -122,6 +126,31 @@ class HIDEvent(models.Model):
|
|||||||
max_length=8, blank=True, null=True, db_column="rawCardNumber"
|
max_length=8, blank=True, null=True, db_column="rawCardNumber"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_xml_attributes(cls, door: Door, attrib: dict[str:str]):
|
||||||
|
field_lookup = {
|
||||||
|
field.column: field.attname for field in HIDEvent._meta.get_fields()
|
||||||
|
}
|
||||||
|
|
||||||
|
def attr_to_bool(str):
|
||||||
|
if str is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return str == "true"
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
**{
|
||||||
|
**{field_lookup[k]: v for k, v in attrib.items()},
|
||||||
|
"door": door,
|
||||||
|
# fixups for specific attributes
|
||||||
|
"timestamp": timezone.make_aware(
|
||||||
|
datetime.fromisoformat(attrib["timestamp"])
|
||||||
|
),
|
||||||
|
"io_state": attr_to_bool(attrib.get("ioState")),
|
||||||
|
"command_status": attr_to_bool(attrib.get("commandStatus")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
"""
|
"""
|
||||||
|
0
doorcontrol/tasks/__init__.py
Normal file
0
doorcontrol/tasks/__init__.py
Normal file
54
doorcontrol/tasks/scrapehidevents.py
Normal file
54
doorcontrol/tasks/scrapehidevents.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from django_q.tasks import async_task
|
||||||
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
from cmsmanage.django_q2_helper import ensure_scheduled
|
||||||
|
from doorcontrol.models import Door, HIDEvent
|
||||||
|
from doorcontrol.hid.DoorController import DoorController
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def getMessages(door: Door):
|
||||||
|
last_event = door.hidevent_set.order_by("timestamp").last()
|
||||||
|
if last_event is not None:
|
||||||
|
last_ts = timezone.make_naive(last_event.timestamp)
|
||||||
|
else:
|
||||||
|
last_ts = datetime(2010, 1, 1)
|
||||||
|
|
||||||
|
door_controller = DoorController(
|
||||||
|
door.ip,
|
||||||
|
settings.HID_DOOR_USERNAME,
|
||||||
|
settings.HID_DOOR_PASSWORD,
|
||||||
|
)
|
||||||
|
|
||||||
|
for events_page in door_controller.get_events(last_ts):
|
||||||
|
print(f"Importing {len(events_page)} events for {door.name}")
|
||||||
|
HIDEvent.objects.bulk_create(
|
||||||
|
(HIDEvent.from_xml_attributes(door, event.attrib) for event in events_page),
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def q_getMessagesAllDoors():
|
||||||
|
# TODO: this should probably use async_iter
|
||||||
|
for door in Door.objects.all():
|
||||||
|
async_task(
|
||||||
|
getMessages,
|
||||||
|
door,
|
||||||
|
cluster="internal",
|
||||||
|
group=f"Fetch HID Events - {door.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_tasks():
|
||||||
|
ensure_scheduled(
|
||||||
|
"Fetch HID Events",
|
||||||
|
q_getMessagesAllDoors,
|
||||||
|
schedule_type=Schedule.MINUTES,
|
||||||
|
minutes=15,
|
||||||
|
)
|
287
pdm.lock
287
pdm.lock
@ -2,11 +2,19 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
groups = ["default", "debug", "lint", "server", "typing"]
|
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||||
cross_platform = true
|
strategy = ["cross_platform"]
|
||||||
static_urls = false
|
lock_version = "4.4"
|
||||||
lock_version = "4.3"
|
content_hash = "sha256:4da4f2b2f7ef06d8cf8225cbce58f84db4580b84bec946f166a02905739c9321"
|
||||||
content_hash = "sha256:c3404161808d837738b673f358588ae72e1b51f506b6f4e60d7ec8ef6b8e0e0c"
|
|
||||||
|
[[package]]
|
||||||
|
name = "appnope"
|
||||||
|
version = "0.1.3"
|
||||||
|
summary = "Disable App Nap on macOS >= 10.9"
|
||||||
|
files = [
|
||||||
|
{file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
|
||||||
|
{file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asgiref"
|
name = "asgiref"
|
||||||
@ -18,6 +26,18 @@ files = [
|
|||||||
{file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"},
|
{file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asttokens"
|
||||||
|
version = "2.4.1"
|
||||||
|
summary = "Annotate AST trees with source code positions"
|
||||||
|
dependencies = [
|
||||||
|
"six>=1.12.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
|
||||||
|
{file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beautifulsoup4"
|
name = "beautifulsoup4"
|
||||||
version = "4.11.1"
|
version = "4.11.1"
|
||||||
@ -273,6 +293,16 @@ files = [
|
|||||||
{file = "cssselect2-0.7.0.tar.gz", hash = "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a"},
|
{file = "cssselect2-0.7.0.tar.gz", hash = "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decorator"
|
||||||
|
version = "5.1.1"
|
||||||
|
requires_python = ">=3.5"
|
||||||
|
summary = "Decorators for Humans"
|
||||||
|
files = [
|
||||||
|
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
|
||||||
|
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "4.2.5"
|
version = "4.2.5"
|
||||||
@ -341,6 +371,19 @@ files = [
|
|||||||
{file = "django_debug_toolbar-4.2.0.tar.gz", hash = "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc"},
|
{file = "django_debug_toolbar-4.2.0.tar.gz", hash = "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-extensions"
|
||||||
|
version = "3.2.3"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "Extensions for Django"
|
||||||
|
dependencies = [
|
||||||
|
"Django>=3.2",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
|
||||||
|
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-markdownx"
|
name = "django-markdownx"
|
||||||
version = "4.0.2"
|
version = "4.0.2"
|
||||||
@ -355,6 +398,16 @@ files = [
|
|||||||
{file = "django_markdownx-4.0.2-py2.py3-none-any.whl", hash = "sha256:2fed9b6bbac798a6c24ba30e17ad775fab44f94774c820abd87aabc751f50a7e"},
|
{file = "django_markdownx-4.0.2-py2.py3-none-any.whl", hash = "sha256:2fed9b6bbac798a6c24ba30e17ad775fab44f94774c820abd87aabc751f50a7e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-object-actions"
|
||||||
|
version = "4.2.0"
|
||||||
|
requires_python = ">=3.7,<4.0"
|
||||||
|
summary = "A Django app for adding object tools for models in the admin"
|
||||||
|
files = [
|
||||||
|
{file = "django_object_actions-4.2.0-py3-none-any.whl", hash = "sha256:ae0df9984c68a4f42f219a391b71fa0630fe44a2983b39b8064378ebddcff30c"},
|
||||||
|
{file = "django_object_actions-4.2.0.tar.gz", hash = "sha256:e24befedf01b6fcdccbb03c33c0e2c855fd1a88f352a66dc7e2170ba31e80128"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-picklefield"
|
name = "django-picklefield"
|
||||||
version = "3.1"
|
version = "3.1"
|
||||||
@ -520,6 +573,26 @@ files = [
|
|||||||
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
|
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.1.3"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Backport of PEP 654 (exception groups)"
|
||||||
|
files = [
|
||||||
|
{file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
|
||||||
|
{file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "executing"
|
||||||
|
version = "2.0.1"
|
||||||
|
requires_python = ">=3.5"
|
||||||
|
summary = "Get the currently executing AST node of a frame, and other information"
|
||||||
|
files = [
|
||||||
|
{file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
|
||||||
|
{file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fonttools"
|
name = "fonttools"
|
||||||
version = "4.38.0"
|
version = "4.38.0"
|
||||||
@ -614,6 +687,43 @@ files = [
|
|||||||
{file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
|
{file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipython"
|
||||||
|
version = "8.17.2"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "IPython: Productive Interactive Computing"
|
||||||
|
dependencies = [
|
||||||
|
"appnope; sys_platform == \"darwin\"",
|
||||||
|
"colorama; sys_platform == \"win32\"",
|
||||||
|
"decorator",
|
||||||
|
"exceptiongroup; python_version < \"3.11\"",
|
||||||
|
"jedi>=0.16",
|
||||||
|
"matplotlib-inline",
|
||||||
|
"pexpect>4.3; sys_platform != \"win32\"",
|
||||||
|
"prompt-toolkit!=3.0.37,<3.1.0,>=3.0.30",
|
||||||
|
"pygments>=2.4.0",
|
||||||
|
"stack-data",
|
||||||
|
"traitlets>=5",
|
||||||
|
"typing-extensions; python_version < \"3.10\"",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "ipython-8.17.2-py3-none-any.whl", hash = "sha256:1e4d1d666a023e3c93585ba0d8e962867f7a111af322efff6b9c58062b3e5444"},
|
||||||
|
{file = "ipython-8.17.2.tar.gz", hash = "sha256:126bb57e1895594bb0d91ea3090bbd39384f6fe87c3d57fd558d0670f50339bb"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jedi"
|
||||||
|
version = "0.19.1"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "An autocompletion tool for Python that can be used for text editors."
|
||||||
|
dependencies = [
|
||||||
|
"parso<0.9.0,>=0.8.3",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
|
||||||
|
{file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsbeautifier"
|
name = "jsbeautifier"
|
||||||
version = "1.14.7"
|
version = "1.14.7"
|
||||||
@ -635,6 +745,64 @@ files = [
|
|||||||
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
|
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "4.9.3"
|
||||||
|
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||||
|
summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||||
|
files = [
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"},
|
||||||
|
{file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
|
||||||
|
{file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
|
||||||
|
{file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
|
||||||
|
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
|
||||||
|
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
|
||||||
|
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
|
||||||
|
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
|
||||||
|
{file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"},
|
||||||
|
{file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
|
||||||
|
{file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
|
||||||
|
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
|
||||||
|
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
|
||||||
|
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
|
||||||
|
{file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
|
||||||
|
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
|
||||||
|
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
|
||||||
|
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
|
||||||
|
{file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
|
||||||
|
{file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
|
||||||
|
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
|
||||||
|
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
|
||||||
|
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
|
||||||
|
{file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
|
||||||
|
{file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown"
|
name = "markdown"
|
||||||
version = "3.4.1"
|
version = "3.4.1"
|
||||||
@ -674,6 +842,19 @@ files = [
|
|||||||
{file = "markdownify-0.11.6.tar.gz", hash = "sha256:009b240e0c9f4c8eaf1d085625dcd4011e12f0f8cec55dedf9ea6f7655e49bfe"},
|
{file = "markdownify-0.11.6.tar.gz", hash = "sha256:009b240e0c9f4c8eaf1d085625dcd4011e12f0f8cec55dedf9ea6f7655e49bfe"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matplotlib-inline"
|
||||||
|
version = "0.1.6"
|
||||||
|
requires_python = ">=3.5"
|
||||||
|
summary = "Inline Matplotlib backend for Jupyter"
|
||||||
|
dependencies = [
|
||||||
|
"traitlets",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
|
||||||
|
{file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdformat"
|
name = "mdformat"
|
||||||
version = "0.7.17"
|
version = "0.7.17"
|
||||||
@ -776,6 +957,16 @@ files = [
|
|||||||
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parso"
|
||||||
|
version = "0.8.3"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "A Python Parser"
|
||||||
|
files = [
|
||||||
|
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
|
||||||
|
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -786,6 +977,18 @@ files = [
|
|||||||
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
|
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pexpect"
|
||||||
|
version = "4.8.0"
|
||||||
|
summary = "Pexpect allows easy control of interactive console applications."
|
||||||
|
dependencies = [
|
||||||
|
"ptyprocess>=0.5",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
|
||||||
|
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "9.3.0"
|
version = "9.3.0"
|
||||||
@ -845,6 +1048,37 @@ files = [
|
|||||||
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prompt-toolkit"
|
||||||
|
version = "3.0.39"
|
||||||
|
requires_python = ">=3.7.0"
|
||||||
|
summary = "Library for building powerful interactive command lines in Python"
|
||||||
|
dependencies = [
|
||||||
|
"wcwidth",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"},
|
||||||
|
{file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptyprocess"
|
||||||
|
version = "0.7.0"
|
||||||
|
summary = "Run a subprocess in a pseudo terminal"
|
||||||
|
files = [
|
||||||
|
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||||
|
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pure-eval"
|
||||||
|
version = "0.2.2"
|
||||||
|
summary = "Safely evaluate AST nodes without side effects"
|
||||||
|
files = [
|
||||||
|
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
|
||||||
|
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@ -886,6 +1120,16 @@ files = [
|
|||||||
{file = "pydyf-0.6.0.tar.gz", hash = "sha256:b44a38855d7e47b740b3cd31ab63a2f5b9b2793931d50b0ccaed3bb7b86912fc"},
|
{file = "pydyf-0.6.0.tar.gz", hash = "sha256:b44a38855d7e47b740b3cd31ab63a2f5b9b2793931d50b0ccaed3bb7b86912fc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.16.1"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
files = [
|
||||||
|
{file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
|
||||||
|
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyphen"
|
name = "pyphen"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@ -1080,6 +1324,20 @@ files = [
|
|||||||
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stack-data"
|
||||||
|
version = "0.6.3"
|
||||||
|
summary = "Extract data from python stack frames and tracebacks for informative displays"
|
||||||
|
dependencies = [
|
||||||
|
"asttokens>=2.1.0",
|
||||||
|
"executing>=1.2.0",
|
||||||
|
"pure-eval",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
|
||||||
|
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinycss2"
|
name = "tinycss2"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -1116,6 +1374,16 @@ files = [
|
|||||||
{file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"},
|
{file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "traitlets"
|
||||||
|
version = "5.13.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Traitlets Python configuration system"
|
||||||
|
files = [
|
||||||
|
{file = "traitlets-5.13.0-py3-none-any.whl", hash = "sha256:baf991e61542da48fe8aef8b779a9ea0aa38d8a54166ee250d5af5ecf4486619"},
|
||||||
|
{file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-bleach"
|
name = "types-bleach"
|
||||||
version = "6.0.0.4"
|
version = "6.0.0.4"
|
||||||
@ -1209,6 +1477,15 @@ files = [
|
|||||||
{file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
|
{file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wcwidth"
|
||||||
|
version = "0.2.9"
|
||||||
|
summary = "Measures the displayed width of unicode strings in a terminal"
|
||||||
|
files = [
|
||||||
|
{file = "wcwidth-0.2.9-py2.py3-none-any.whl", hash = "sha256:9a929bd8380f6cd9571a968a9c8f4353ca58d7cd812a4822bba831f8d685b223"},
|
||||||
|
{file = "wcwidth-0.2.9.tar.gz", hash = "sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weasyprint"
|
name = "weasyprint"
|
||||||
version = "59.0"
|
version = "59.0"
|
||||||
|
@ -24,6 +24,8 @@ dependencies = [
|
|||||||
"semver~=3.0",
|
"semver~=3.0",
|
||||||
"djangorestframework~=3.14",
|
"djangorestframework~=3.14",
|
||||||
"django-q2~=1.5",
|
"django-q2~=1.5",
|
||||||
|
"lxml~=4.9",
|
||||||
|
"django-object-actions~=4.2",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|
||||||
@ -83,6 +85,10 @@ typing = [
|
|||||||
debug = [
|
debug = [
|
||||||
"django-debug-toolbar~=4.2",
|
"django-debug-toolbar~=4.2",
|
||||||
]
|
]
|
||||||
|
dev = [
|
||||||
|
"django-extensions~=3.2",
|
||||||
|
"ipython~=8.17",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.pdm.scripts]
|
[tool.pdm.scripts]
|
||||||
start = "./manage.py runserver"
|
start = "./manage.py runserver"
|
||||||
|
@ -22,12 +22,12 @@
|
|||||||
action="{% url 'login' %}">
|
action="{% url 'login' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<div class="form-group row">
|
<div class="form-group row p-2">
|
||||||
<label class="col-sm-4 col-form-label" for="{{ field.auto_id }}">{{ field.label }}</label>
|
<label class="col-sm-4 col-form-label" for="{{ field.auto_id }}">{{ field.label }}</label>
|
||||||
<div class="col-sm-8">{% render_field field class="form-control" %}</div>
|
<div class="col-sm-8">{% render_field field class="form-control" %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<input type="submit" class="btn btn-primary" value="login" />
|
<input type="submit" class="btn btn-primary m-3" value="login" />
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user