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 - id: check-added-large-files
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 23.7.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/Riverside-Healthcare/djLint - repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.19.16 rev: v1.32.1
hooks: hooks:
- id: djlint-django - id: djlint-django

View File

@ -35,6 +35,7 @@ INSTALLED_APPS = [
"recurrence", "recurrence",
"rest_framework", "rest_framework",
"rest_framework.authtoken", "rest_framework.authtoken",
"django_q",
"tasks.apps.TasksConfig", "tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig", "rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig", "membershipworks.apps.MembershipworksConfig",
@ -114,3 +115,14 @@ REST_FRAMEWORK = {
], ],
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", "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 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): class IsRedFilter(admin.SimpleListFilter):
@ -23,10 +28,10 @@ class IsRedFilter(admin.SimpleListFilter):
@admin.register(HIDEvent) @admin.register(HIDEvent)
class HIDEventAdmin(admin.ModelAdmin): class HIDEventAdmin(admin.ModelAdmin):
search_fields = ["forename", "surname", "cardholder_id"] 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 = [ list_filter = [
"timestamp", "timestamp",
"door_name", "door",
"event_type", "event_type",
IsRedFilter, IsRedFilter,
] ]

View File

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

View File

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

View File

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

View File

@ -163,7 +163,7 @@ class DeniedAccess(BaseAccessReport):
return [ return [
{ {
"timestamp": event.timestamp, "timestamp": event.timestamp,
"door name": event.door_name, "door name": event.door.name,
"event type": HIDEvent.EventType(event.event_type).label, "event type": HIDEvent.EventType(event.event_type).label,
"name": " ".join( "name": " ".join(
(n for n in [event.forename, event.surname] if n is not None) (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): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("rentals", "0002_lockerinfo_notes"), ("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): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

View File

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

View File

@ -8,6 +8,8 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no"> content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
<!-- Bootstrap Icons CSS -->
<link href="{% static 'bootstrap-icons.min.css' %}" rel="stylesheet">
<!-- Tabulator CSS --> <!-- Tabulator CSS -->
<link href="{% static 'tabulator_bootstrap5.min.css' %}" rel="stylesheet"> <link href="{% static 'tabulator_bootstrap5.min.css' %}" rel="stylesheet">
<title> <title>
@ -15,7 +17,8 @@
</title> </title>
</head> </head>
<body> <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"> <div class="container-fluid">
<a class="navbar-brand" href="{% url 'dashboard:dashboard' %}">Claremont MakerSpace</a> <a class="navbar-brand" href="{% url 'dashboard:dashboard' %}">Claremont MakerSpace</a>
<button class="navbar-toggler" <button class="navbar-toggler"
@ -30,6 +33,35 @@
<div id="user-nav" class="collapse navbar-collapse justify-content-end"> <div id="user-nav" class="collapse navbar-collapse justify-content-end">
<div class="navbar-nav"> <div class="navbar-nav">
{% block nav_extra %}{% endblock %} {% 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 %} {% if user.is_staff %}
<a class="nav-item nav-link" <a class="nav-item nav-link"
href="{% block admin_link %}{% url 'admin:index' %}{% endblock %}">Admin</a> href="{% block admin_link %}{% url 'admin:index' %}{% endblock %}">Admin</a>
@ -66,10 +98,10 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
{% block footer %}{% endblock %} {% block footer %}{% endblock %}
</body>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="{% static 'bootstrap.bundle.min.js' %}"></script> <script src="{% static 'bootstrap.bundle.min.js' %}"></script>
<!-- Tabulator JS --> <!-- Tabulator JS -->
<script src="{% static 'tabulator.min.js' %}"></script> <script src="{% static 'tabulator.min.js' %}"></script>
{% block script %}{% endblock %} {% block script %}{% endblock %}
</body>
</html> </html>