Compare commits
13 Commits
1aa0bd6014
...
eb42e2515b
Author | SHA1 | Date | |
---|---|---|---|
eb42e2515b | |||
ee001b0256 | |||
cb8bf3da4f | |||
05037b74fc | |||
1d9102d372 | |||
33024d7d62 | |||
2dfa0db316 | |||
d835645221 | |||
90808251d5 | |||
feed128a3b | |||
08ff37de7d | |||
28abbf69f8 | |||
4502c1b3ef |
@ -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
|
||||||
|
@ -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": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
]
|
]
|
||||||
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -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",)
|
||||||
|
@ -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):
|
||||||
|
@ -2,90 +2,82 @@
|
|||||||
|
|
||||||
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
|
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul class="nav nav-tabs">
|
<div class="d-flex flex-column align-items-center">
|
||||||
{% for report_name, report_url in report_types %}
|
<ul class="nav nav-tabs">
|
||||||
<li class="nav-item">
|
{% for report_name, report_url in report_types %}
|
||||||
<a class="nav-link{% if report_name == selected_report %} active{% endif %}"
|
<li class="nav-item">
|
||||||
href="{{ report_url }}?{{ query_params }}">{{ report_name }}</a>
|
<a class="nav-link{% if report_name == selected_report %} active{% endif %}"
|
||||||
</li>
|
href="{{ report_url }}?{{ query_params }}">{{ report_name }}</a>
|
||||||
{% 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">
|
|
||||||
<input type="date"
|
|
||||||
class="form-control"
|
|
||||||
id="startDate"
|
|
||||||
name="timestamp__gte"
|
|
||||||
value="{{ timestamp__gte|date:'Y-m-d' }}">
|
|
||||||
<label for="startDate">Start Date</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="date"
|
|
||||||
class="form-control"
|
|
||||||
id="endDate"
|
|
||||||
name="timestamp__lte"
|
|
||||||
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">
|
|
||||||
<input type="number"
|
|
||||||
class="form-control"
|
|
||||||
id="itemsPerPage"
|
|
||||||
name="items_per_page"
|
|
||||||
value="{{ items_per_page }}"
|
|
||||||
min="10"
|
|
||||||
max="200"
|
|
||||||
step="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>
|
|
||||||
</form>
|
|
||||||
<table class="table table-bordered table-striped table-hover mb-2">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{% for column in object_list.0.keys %}<th>{{ column|title }}</th>{% endfor %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for object in object_list %}
|
|
||||||
<tr>
|
|
||||||
{% for field in object.values %}<td>{{ field }}</td>{% endfor %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<div class="text-center mb-2">Showing {{ page_obj.object_list|length }} of {{ paginator.count }} results.</div>
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link"
|
|
||||||
href="?{{ query_params }}&page={{ page_obj.previous_page_number }}">Previous</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% for page_num in paginator_range %}
|
|
||||||
<li class="page-item {% if page_num == page_obj.number %} active {% elif page_num == paginator.ELLIPSIS %} disabled {% endif %}">
|
|
||||||
<a class="page-link" href="?{{ query_params }}&page={{ page_num }}">{{ page_num }}</a>
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link"
|
|
||||||
href="?{{ query_params }}&page={{ page_obj.next_page_number }}">Next</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
<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"
|
||||||
|
name="timestamp__gte"
|
||||||
|
value="{{ timestamp__gte|date:'Y-m-d' }}">
|
||||||
|
<label for="startDate">Start Date</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating mx-2">
|
||||||
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="endDate"
|
||||||
|
name="timestamp__lte"
|
||||||
|
value="{{ timestamp__lte|date:'Y-m-d' }}">
|
||||||
|
<label for="endDate">End Date</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating mx-1">
|
||||||
|
<input type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="itemsPerPage"
|
||||||
|
name="items_per_page"
|
||||||
|
value="{{ items_per_page }}"
|
||||||
|
min="10"
|
||||||
|
max="200"
|
||||||
|
step="10"
|
||||||
|
required>
|
||||||
|
<label for="itemsPerPage">Items Per Page</label>
|
||||||
|
</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-striped table-hover mb-2 w-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for column in object_list.0.keys %}<th>{{ column|title }}</th>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for object in object_list %}
|
||||||
|
<tr>
|
||||||
|
{% for field in object.values %}<td>{{ field }}</td>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<div class="text-center mb-2">Showing {{ page_obj.object_list|length }} of {{ paginator.count }} results.</div>
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="?{{ query_params }}&page={{ page_obj.previous_page_number }}">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for page_num in paginator_range %}
|
||||||
|
<li class="page-item {% if page_num == page_obj.number %} active {% elif page_num == paginator.ELLIPSIS %} disabled {% endif %}">
|
||||||
|
<a class="page-link" href="?{{ query_params }}&page={{ page_num }}">{{ page_num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="?{{ query_params }}&page={{ page_obj.next_page_number }}">Next</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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)
|
||||||
|
@ -5,7 +5,6 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -54,7 +54,8 @@ def department_emails(ordered_queryset):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for department, certifications in certifications_by_department:
|
for department, certifications in certifications_by_department:
|
||||||
yield make_department_email(department, list(certifications))
|
if department.shop_lead_flag is not None:
|
||||||
|
yield make_department_email(department, list(certifications))
|
||||||
|
|
||||||
|
|
||||||
def make_member_email(member, certifications):
|
def make_member_email(member, certifications):
|
||||||
|
@ -5,7 +5,6 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"),
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"),
|
||||||
|
@ -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"
|
||||||
|
@ -6,7 +6,6 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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
95
static/bootstrap-color-toggle.js
vendored
Normal 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
5
static/bootstrap-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
static/bootstrap.bundle.min.js
vendored
6
static/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
7
static/bootstrap.min.css
vendored
7
static/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
BIN
static/fonts/bootstrap-icons.woff
Normal file
BIN
static/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
static/fonts/bootstrap-icons.woff2
Normal file
BIN
static/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
4
static/tabulator.min.js
vendored
4
static/tabulator.min.js
vendored
File diff suppressed because one or more lines are too long
2
static/tabulator_bootstrap5.min.css
vendored
2
static/tabulator_bootstrap5.min.css
vendored
File diff suppressed because one or more lines are too long
@ -7,7 +7,6 @@ import markdownx.models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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 %}
|
||||||
<!-- Bootstrap JS -->
|
|
||||||
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
|
|
||||||
<!-- Tabulator JS -->
|
|
||||||
<script src="{% static 'tabulator.min.js' %}"></script>
|
|
||||||
{% block script %}{% endblock %}
|
|
||||||
</body>
|
</body>
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
|
||||||
|
<!-- Tabulator JS -->
|
||||||
|
<script src="{% static 'tabulator.min.js' %}"></script>
|
||||||
|
{% block script %}{% endblock %}
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user