Compare commits

..

13 Commits

38 changed files with 1021 additions and 942 deletions

View File

@ -8,11 +8,11 @@ repos:
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.19.16
rev: v1.32.1
hooks:
- id: djlint-django

View File

@ -35,6 +35,7 @@ INSTALLED_APPS = [
"recurrence",
"rest_framework",
"rest_framework.authtoken",
"django_q",
"tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig",
@ -114,3 +115,14 @@ REST_FRAMEWORK = {
],
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
}
# Django Q
Q_CLUSTER = {
"name": "cmsmanage",
"orm": "default",
"retry": 360,
"timeout": 300,
"ALT_CLUSTERS": {
"internal": {},
},
}

View File

@ -1,6 +1,11 @@
from django.contrib import admin
from .models import HIDEvent
from .models import Door, HIDEvent
@admin.register(Door)
class DoorAdmin(admin.ModelAdmin):
pass
class IsRedFilter(admin.SimpleListFilter):
@ -23,10 +28,10 @@ class IsRedFilter(admin.SimpleListFilter):
@admin.register(HIDEvent)
class HIDEventAdmin(admin.ModelAdmin):
search_fields = ["forename", "surname", "cardholder_id"]
list_display = ["door_name", "timestamp", "event_type", "description", "is_red"]
list_display = ["timestamp", "door", "event_type", "description", "is_red"]
list_filter = [
"timestamp",
"door_name",
"door",
"event_type",
IsRedFilter,
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
@ -96,4 +95,10 @@ class Migration(migrations.Migration):
"managed": False,
},
),
migrations.AddConstraint(
model_name="hidevent",
constraint=models.UniqueConstraint(
fields=("door_name", "timestamp", "event_type"), name="unique_hidevent"
),
),
]

View File

@ -0,0 +1,74 @@
# Generated by Django 4.2.5 on 2023-09-19 04:20
from django.db import migrations, models
import django.db.models.deletion
def link_events_to_doors(apps, schema_editor):
HIDEvent = apps.get_model("doorcontrol", "HIDEvent")
Door = apps.get_model("doorcontrol", "Door")
for event in HIDEvent.objects.all():
door, created = Door.objects.get_or_create(name=event.door_name)
event.door = door
event.save()
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="hidevent",
options={"ordering": ("-timestamp",)},
),
migrations.CreateModel(
name="Door",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=64, unique=True)),
],
),
# create nullable foreign key to door
migrations.AddField(
model_name="hidevent",
name="door",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.door",
null=True,
),
),
# create new Doors and link them to HID Events
migrations.RunPython(link_events_to_doors, atomic=True),
# make door foreign key not nullable
migrations.AlterField(
model_name="hidevent",
name="door",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="doorcontrol.door"
),
),
# remove old constaint
migrations.RemoveConstraint(model_name="hidevent", name="unique_hidevent"),
# remove old name field
migrations.RemoveField(
model_name="hidevent",
name="door_name",
),
migrations.AddConstraint(
model_name="hidevent",
constraint=models.UniqueConstraint(
fields=("door", "timestamp", "event_type"), name="unique_hidevent"
),
),
]

View File

@ -3,6 +3,13 @@ from django.db.models import ExpressionWrapper, F, Func, Q
from django.db.models.functions import Mod
class Door(models.Model):
name = models.CharField(max_length=64, unique=True)
def __str__(self):
return self.name
class HIDEventQuerySet(models.QuerySet):
def with_is_red(self):
"""Based on `function isRedEvent` from /html/hid-global.js on a HID EDGE EVO Solo"""
@ -10,19 +17,19 @@ class HIDEventQuerySet(models.QuerySet):
is_red=ExpressionWrapper(
Q(
event_type__in=[
1022,
1023,
2024,
2029,
2036,
2042,
2043,
2046,
4041,
4042,
4043,
4044,
4045,
HIDEvent.EventType.DENIED_ACCESS_CARD_NOT_FOUND,
HIDEvent.EventType.DENIED_ACCESS_ACCESS_PIN_NOT_FOUND,
HIDEvent.EventType.DENIED_ACCESS_SCHEDULE,
HIDEvent.EventType.DENIED_ACCESS_WRONG_PIN,
HIDEvent.EventType.DENIED_ACCESS_CARD_EXPIRED,
HIDEvent.EventType.DENIED_ACCESS_PIN_LOCKOUT,
HIDEvent.EventType.DENIED_ACCESS_UNASSIGNED_CARD,
HIDEvent.EventType.DENIED_ACCESS_PIN_EXPIRED,
HIDEvent.EventType.DOOR_FORCED_ALARM,
HIDEvent.EventType.DOOR_HELD_ALARM,
HIDEvent.EventType.TAMPER_SWITCH_ALARM,
HIDEvent.EventType.AC_FAILURE,
HIDEvent.EventType.BATTERY_FAILURE,
]
),
output_field=models.BooleanField(),
@ -98,7 +105,7 @@ class HIDEvent(models.Model):
DOOR_UNLOCKED = 12032, "Door Unlocked"
DOOR_LOCKED = 12033, "Door Locked"
door_name = models.CharField(max_length=64, db_column="doorName")
door = models.ForeignKey(Door, on_delete=models.CASCADE)
timestamp = models.DateTimeField()
event_type = models.IntegerField(db_column="eventType", choices=EventType.choices)
reader_address = models.IntegerField(db_column="readerAddress")
@ -156,7 +163,7 @@ class HIDEvent(models.Model):
return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
def __str__(self):
return f"{self.door_name} {self.timestamp} - {self.description}"
return f"{self.door.name} {self.timestamp} - {self.description}"
def decoded_card_number(self) -> str:
"""Requires annotations from `with_decoded_card_number`"""
@ -173,9 +180,8 @@ class HIDEvent(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(
fields=["door_name", "timestamp", "event_type"], name="unique_hidevent"
fields=["door", "timestamp", "event_type"], name="unique_hidevent"
)
]
managed = False
db_table = "hidevent"
ordering = ("-timestamp",)

View File

@ -13,8 +13,8 @@ class DoorControlRouter:
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if db == self.db:
return False
if app_label == self.app_label:
return db == self.db
return None
def allow_relation(self, obj1, obj2, **hints):

View File

@ -2,6 +2,7 @@
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
{% block content %}
<div class="d-flex flex-column align-items-center">
<ul class="nav nav-tabs">
{% for report_name, report_url in report_types %}
<li class="nav-item">
@ -10,10 +11,8 @@
</li>
{% endfor %}
</ul>
<form method="get" class="form-floating">
<div class="row align-items-center row-cols-md-auto g-2 mb-2 mt-2">
<div class="col-12">
<div class="form-floating">
<form method="get" class="form-floating m-2 d-flex align-items-center">
<div class="form-floating mx-2">
<input type="date"
class="form-control"
id="startDate"
@ -21,9 +20,7 @@
value="{{ timestamp__gte|date:'Y-m-d' }}">
<label for="startDate">Start Date</label>
</div>
</div>
<div class="col-12">
<div class="form-floating">
<div class="form-floating mx-2">
<input type="date"
class="form-control"
id="endDate"
@ -31,9 +28,7 @@
value="{{ timestamp__lte|date:'Y-m-d' }}">
<label for="endDate">End Date</label>
</div>
</div>
<div class="col-12 col-md-2">
<div class="form-floating">
<div class="form-floating mx-1">
<input type="number"
class="form-control"
id="itemsPerPage"
@ -45,14 +40,10 @@
required>
<label for="itemsPerPage">Items Per Page</label>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="?" class="btn btn-warning">Reset</a>
</div>
</div>
<button type="submit" class="btn btn-primary mx-2">Submit</button>
<a href="?" class="btn btn-warning mx-1">Reset</a>
</form>
<table class="table table-bordered table-striped table-hover mb-2">
<table class="table table-striped table-hover mb-2 w-auto">
<thead>
<tr>
{% for column in object_list.0.keys %}<th>{{ column|title }}</th>{% endfor %}
@ -88,4 +79,5 @@
{% endif %}
</ul>
</nav>
</div>
{% endblock %}

View File

@ -163,7 +163,7 @@ class DeniedAccess(BaseAccessReport):
return [
{
"timestamp": event.timestamp,
"door name": event.door_name,
"door name": event.door.name,
"event type": HIDEvent.EventType(event.event_type).label,
"name": " ".join(
(n for n in [event.forename, event.surname] if n is not None)

View File

@ -5,7 +5,6 @@ import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []

View File

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0001_initial"),
]

View File

@ -24,11 +24,9 @@ class MemberQuerySet(models.QuerySet):
# TODO: maybe rename to reflect EXISTS?
@staticmethod
def has_flag(flag_type: str, flag_name: str):
return Exists(Flag.objects.filter(
type=flag_type,
name=flag_name,
members=OuterRef("pk")
))
return Exists(
Flag.objects.filter(type=flag_type, name=flag_name, members=OuterRef("pk"))
)
# TODO: it should be fairly easy to reduce the number of EXISTS by
# merging the ORed flags

View File

@ -54,6 +54,7 @@ def department_emails(ordered_queryset):
)
for department, certifications in certifications_by_department:
if department.shop_lead_flag is not None:
yield make_department_email(department, list(certifications))

View File

@ -5,7 +5,6 @@ import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [

View File

@ -19,7 +19,6 @@ def migrate_certification_version_forward(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("paperwork", "0001_initial"),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("paperwork", "0002_add_certification_version_model"),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("paperwork", "0003_alter_certificationversion_id"),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("paperwork", "0004_alter_certification_options"),
]

View File

@ -17,7 +17,6 @@ def link_departments(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0001_initial"),
("paperwork", "0005_certificationdefinition_mailing_list"),

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("paperwork", "0006_department_alter_certificationdefinition_department"),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("paperwork", "0007_department_has_mailing_list"),
]

View File

@ -5,7 +5,6 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0002_alter_flag_options"),
("paperwork", "0008_remove_certificationdefinition_mailing_list"),

1431
pdm.lock

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[project]
name = "CMS Management"
name = "CMSManage"
version = "0.1.0"
description = ""
authors = [
@ -8,30 +8,34 @@ authors = [
dependencies = [
"django~=4.2",
"django-admin-logs~=1.0",
"django-auth-ldap~=4.3",
"django-auth-ldap~=4.5",
"django-markdownx~=4.0",
"django-recurrence~=1.11",
"django-widget-tweaks~=1.4",
"django-widget-tweaks~=1.5",
"django-stubs-ext~=4.2",
"markdownify~=0.11",
"mdformat~=0.7",
"mdformat-tables~=0.4",
"mysqlclient~=2.1",
"mysqlclient~=2.2",
"bleach~=6.0",
"django-autocomplete-light~=3.9",
"weasyprint~=59.0",
"requests~=2.31",
"semver~=3.0",
"djangorestframework~=3.14",
"django-q2~=1.5",
]
requires-python = ">=3.9"
[project.optional-dependencies]
server = [
"uvicorn~=0.22",
"setuptools~=68.0",
"uvicorn~=0.23",
"setuptools~=68.2",
]
[tool.black]
line-length = 88
[tool.djlint]
profile="django"
extension = ".dj.html"
@ -64,20 +68,20 @@ name = "pypi"
[tool.pdm.dev-dependencies]
lint = [
"black~=23.3",
"djlint~=1.31",
"black~=23.7",
"djlint~=1.32",
]
typing = [
"mypy~=1.3",
"mypy~=1.4",
"django-stubs~=4.2",
"setuptools~=68.0",
"setuptools~=68.2",
"types-bleach~=6.0",
"types-requests~=2.31",
"types-urllib3~=1.26",
"djangorestframework-stubs[compatible-mypy]~=3.14",
]
debug = [
"django-debug-toolbar~=4.1",
"django-debug-toolbar~=4.2",
]
[tool.pdm.scripts]
@ -85,5 +89,5 @@ start = "./manage.py runserver"
fmt.shell = "black . && djlint --reformat ."
[build-system]
requires = ["pdm-pep517"]
build-backend = "pdm.pep517.api"
requires = ["pdm-backend"]
build-backend = "pdm.backend"

View File

@ -6,7 +6,6 @@ import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rentals", "0001_initial"),
]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rentals", "0002_lockerinfo_notes"),
]

95
static/bootstrap-color-toggle.js vendored Normal file
View File

@ -0,0 +1,95 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Modified by Adam Goldsmith
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
"use strict";
const getStoredTheme = () => localStorage.getItem("theme");
const setStoredTheme = (theme) => localStorage.setItem("theme", theme);
const getPreferredTheme = () => {
const storedTheme = getStoredTheme();
if (storedTheme) {
return storedTheme;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
const setTheme = (theme) => {
if (
theme === "auto" &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
document.documentElement.setAttribute("data-bs-theme", "dark");
} else {
document.documentElement.setAttribute("data-bs-theme", theme);
}
};
setTheme(getPreferredTheme());
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector("#bd-theme");
if (!themeSwitcher) {
return;
}
const themeSwitcherText = document.querySelector("#bd-theme-text");
const activeThemeIcon = document.querySelector(".theme-icon-active");
const btnToActive = document.querySelector(
`[data-bs-theme-value="${theme}"]`,
);
const activeIcon = [
...btnToActive.querySelector(".theme-icon").classList,
].find((c) => c.startsWith("bi-"));
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
element.classList.remove("active");
element.setAttribute("aria-pressed", "false");
});
btnToActive.classList.add("active");
btnToActive.setAttribute("aria-pressed", "true");
[...activeThemeIcon.classList]
.filter((c) => c.startsWith("bi-"))
.forEach((icon) => activeThemeIcon.classList.remove(icon));
activeThemeIcon.classList.add(activeIcon);
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
if (focus) {
themeSwitcher.focus();
}
};
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
const storedTheme = getStoredTheme();
if (storedTheme !== "light" && storedTheme !== "dark") {
setTheme(getPreferredTheme());
}
});
window.addEventListener("DOMContentLoaded", () => {
console.log(getPreferredTheme());
showActiveTheme(getPreferredTheme());
document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => {
toggle.addEventListener("click", () => {
const theme = toggle.getAttribute("data-bs-theme-value");
setStoredTheme(theme);
setTheme(theme);
showActiveTheme(theme, true);
});
});
});
})();

5
static/bootstrap-icons.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,6 @@ import markdownx.models
class Migration(migrations.Migration):
initial = True
dependencies = [

View File

@ -10,7 +10,6 @@ def slugify_name(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("tasks", "0001_initial"),
]

View File

@ -8,6 +8,8 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
<!-- Bootstrap Icons CSS -->
<link href="{% static 'bootstrap-icons.min.css' %}" rel="stylesheet">
<!-- Tabulator CSS -->
<link href="{% static 'tabulator_bootstrap5.min.css' %}" rel="stylesheet">
<title>
@ -15,7 +17,8 @@
</title>
</head>
<body>
<nav class="navbar navbar-expand-sm navbar-light bg-light">
<script src="{% static 'bootstrap-color-toggle.js' %}"></script>
<nav class="navbar navbar-expand-sm bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'dashboard:dashboard' %}">Claremont MakerSpace</a>
<button class="navbar-toggler"
@ -30,6 +33,35 @@
<div id="user-nav" class="collapse navbar-collapse justify-content-end">
<div class="navbar-nav">
{% block nav_extra %}{% endblock %}
<div class="nav-item dropdown">
<button class="btn btn-link nav-link dropdown-toggle d-flex align-items-center" id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme (auto)">
<i class="bi theme-icon-active bi-circle-half"></i>
<span class="d-none ms-2" id="bd-theme-text">Toggle theme</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text">
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
<i class="bi me-2 opacity-50 theme-icon bi-sun-fill"></i>
Light
<i class="bi ms-auto d-none bi-check2"></i>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
<i class="bi me-2 opacity-50 theme-icon bi-moon-stars-fill"></i>
Dark
<i class="bi ms-auto d-none bi-check2"></i>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
<i class="bi me-2 opacity-50 theme-icon bi-circle-half"></i>
Auto
<i class="bi ms-auto d-none bi-check2"></i>
</button>
</li>
</ul>
</div>
{% if user.is_staff %}
<a class="nav-item nav-link"
href="{% block admin_link %}{% url 'admin:index' %}{% endblock %}">Admin</a>
@ -66,10 +98,10 @@
{% block content %}{% endblock %}
</div>
{% block footer %}{% endblock %}
</body>
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
<!-- Tabulator JS -->
<script src="{% static 'tabulator.min.js' %}"></script>
{% block script %}{% endblock %}
</body>
</html>