doorcontrol: Add report for assigned NFC cards
This commit is contained in:
parent
68a917f3f8
commit
c8b3edcacf
@ -18,12 +18,13 @@ class DoorControlDashboardFragment(dashboard.LinksCardDashboardFragment):
|
|||||||
for name, link in report._report_types()
|
for name, link in report._report_types()
|
||||||
] + [
|
] + [
|
||||||
Link(
|
Link(
|
||||||
"Assign NFC Card ",
|
"Assign NFC Card",
|
||||||
reverse("doorcontrol:assign-nfc-card-user-selector"),
|
reverse("doorcontrol:assign-nfc-card-user-selector"),
|
||||||
permission="doorcontrol.assign_nfc_card",
|
permission="doorcontrol.assign_nfc_card",
|
||||||
)
|
),
|
||||||
|
Link(
|
||||||
|
"Assigned NFC Cards",
|
||||||
|
reverse("doorcontrol:assigned-nfc-cards"),
|
||||||
|
permission="doorcontrol.assign_nfc_card",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
|
||||||
def visible(self) -> bool:
|
|
||||||
return self.request.user.has_perm("doorcontrol.view_hidevent")
|
|
||||||
|
@ -39,3 +39,14 @@ class AttributeScheduleRuleForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = AttributeScheduleRule
|
model = AttributeScheduleRule
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class AssignedNfcCardsReportFilters(forms.Form):
|
||||||
|
has_mw_nfc_card = forms.NullBooleanField(
|
||||||
|
label="Has NFC card in MembershipWorks",
|
||||||
|
widget=forms.Select(choices=[("any", "Any"), (True, "Yes"), (False, "No")]),
|
||||||
|
)
|
||||||
|
has_access_nfc_card = forms.NullBooleanField(
|
||||||
|
label="Has NFC card in UniFi Access",
|
||||||
|
widget=forms.Select(choices=[("any", "Any"), (True, "Yes"), (False, "No")]),
|
||||||
|
)
|
||||||
|
@ -63,3 +63,25 @@ class BusiestTimeOfDayTable(tables.Table):
|
|||||||
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
|
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
|
||||||
events = tables.Column()
|
events = tables.Column()
|
||||||
members = tables.Column()
|
members = tables.Column()
|
||||||
|
|
||||||
|
|
||||||
|
class AssignedNfcCardsTable(tables.Table):
|
||||||
|
member__account_name = tables.Column("Name")
|
||||||
|
member__nfc_card_number = tables.Column("MW NFC Card")
|
||||||
|
access_user__nfc_cards = tables.TemplateColumn(
|
||||||
|
"""
|
||||||
|
{% if value %}
|
||||||
|
<ul>
|
||||||
|
{% for card in value %}
|
||||||
|
<li>{{ card.type }}: {{ card.id }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
???
|
||||||
|
{% endif %}
|
||||||
|
""",
|
||||||
|
empty_values=[[]],
|
||||||
|
verbose_name="UniFi Access Cards",
|
||||||
|
)
|
||||||
|
member__access_wood_shop = tables.BooleanColumn(verbose_name="Access Wood Shop")
|
||||||
|
member__access_metal_shop = tables.BooleanColumn(verbose_name="Access Metal Shop")
|
||||||
|
@ -5,7 +5,7 @@ from django.conf import settings
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from unifi_access import AccessClient
|
from unifi_access import AccessClient
|
||||||
from unifi_access.schemas import AccessPolicy, DoorResource, UserStatus
|
from unifi_access.schemas import AccessPolicy, DoorResource, FullUser, UserStatus
|
||||||
from unifi_access.schemas import User as AccessUser
|
from unifi_access.schemas import User as AccessUser
|
||||||
|
|
||||||
from cmsmanage.django_q2_helper import q_task_group
|
from cmsmanage.django_q2_helper import q_task_group
|
||||||
@ -172,3 +172,11 @@ def update_access():
|
|||||||
)
|
)
|
||||||
|
|
||||||
sync_members(access_client)
|
sync_members(access_client)
|
||||||
|
|
||||||
|
|
||||||
|
@q_task_group("Update Access Users")
|
||||||
|
def update_access_users() -> list[FullUser]:
|
||||||
|
access_client = AccessClient(
|
||||||
|
settings.UNIFI_ACCESS_HOST, settings.UNIFI_ACCESS_API_TOKEN, verify=False
|
||||||
|
)
|
||||||
|
return list(access_client.fetch_all_users__unpaged())
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
{% extends "base.dj.html" %}
|
||||||
|
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% block title %}Assigned NFC Cards{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="vstack align-items-center">
|
||||||
|
<form hx-get="{{ request.path }}{% querystring has_mw_nfc_card=None has_access_nfc_card=None refresh=None %}"
|
||||||
|
hx-include="this"
|
||||||
|
hx-target="body"
|
||||||
|
hx-push-url="true"
|
||||||
|
hx-trigger="change">
|
||||||
|
<div class="row justify-content-center d-print-none">
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="col-12 col-sm-3 mb-2">
|
||||||
|
{{ field.label_tag }} {% render_field field class+="form-select form-select-sm" %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="col-auto">
|
||||||
|
<button class="btn btn-sm btn-secondary"
|
||||||
|
type="button"
|
||||||
|
hx-get="{{ request.path }}{% querystring has_mw_nfc_card=None has_access_nfc_card=None refresh=None %}"
|
||||||
|
hx-push-url="false"
|
||||||
|
hx-indicator="find .htmx-indicator"
|
||||||
|
hx-vals='{"refresh": true}'>
|
||||||
|
<span class="spinner-border spinner-border-sm htmx-indicator"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
Refresh Access Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">{% include "cmsmanage/components/download_table.dj.html" %}</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -15,4 +15,9 @@ urlpatterns = [report._urlpattern() for report in views.REPORTS] + [
|
|||||||
views.AssignNfcCardView.as_view(),
|
views.AssignNfcCardView.as_view(),
|
||||||
name="assign-nfc-card",
|
name="assign-nfc-card",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"assigned-nfc-cards/",
|
||||||
|
views.AssignedNfcCardsReport.as_view(),
|
||||||
|
name="assigned-nfc-cards",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -20,6 +20,7 @@ import django_filters
|
|||||||
import django_q.tasks as q2_tasks
|
import django_q.tasks as q2_tasks
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_filters.views import BaseFilterView
|
from django_filters.views import BaseFilterView
|
||||||
|
from django_q.signing import BadSignature
|
||||||
from django_tables2 import SingleTableMixin
|
from django_tables2 import SingleTableMixin
|
||||||
from django_tables2.export.views import ExportMixin
|
from django_tables2.export.views import ExportMixin
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
@ -34,8 +35,13 @@ from unifi_access.schemas import (
|
|||||||
UserStatus,
|
UserStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from doorcontrol.forms import AssignedNfcCardsReportFilters
|
||||||
|
from doorcontrol.tasks.update_unifi_access import update_access_users
|
||||||
|
from membershipworks.models import Member
|
||||||
|
|
||||||
from .models import Door, HIDEvent
|
from .models import Door, HIDEvent
|
||||||
from .tables import (
|
from .tables import (
|
||||||
|
AssignedNfcCardsTable,
|
||||||
BusiestDayOfWeekTable,
|
BusiestDayOfWeekTable,
|
||||||
BusiestTimeOfDayTable,
|
BusiestTimeOfDayTable,
|
||||||
DeniedAccessTable,
|
DeniedAccessTable,
|
||||||
@ -314,39 +320,36 @@ class BusiestTimeOfDay(BaseAccessReport):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_access_users() -> list[FullUser]:
|
def fetch_access_users_results(force_refresh: bool = False) -> list[FullUser] | None:
|
||||||
access_client = AccessClient(
|
task_group = update_access_users.q_task_group
|
||||||
settings.UNIFI_ACCESS_HOST, settings.UNIFI_ACCESS_API_TOKEN, verify=False
|
|
||||||
|
try:
|
||||||
|
if force_refresh:
|
||||||
|
q2_tasks.delete_group(task_group)
|
||||||
|
refresh_task_id = q2_tasks.async_task(
|
||||||
|
update_access_users, group=task_group, cached=5 * 60
|
||||||
)
|
)
|
||||||
return list(access_client.fetch_all_users__unpaged())
|
return q2_tasks.result(refresh_task_id, wait=-1, cached=True)
|
||||||
|
|
||||||
|
update_users_results = q2_tasks.result_group(task_group, cached=True)
|
||||||
|
if update_users_results and len(update_users_results) > 0:
|
||||||
|
return update_users_results[0]
|
||||||
|
# TODO: this could be better
|
||||||
|
except BadSignature:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("doorcontrol.assign_nfc_card", raise_exception=True)
|
@permission_required("doorcontrol.assign_nfc_card", raise_exception=True)
|
||||||
def assign_nfc_card_user_selector(request: HttpRequest):
|
def assign_nfc_card_user_selector(request: HttpRequest):
|
||||||
template_name = "doorcontrol/assign_nfc_card_user_selector.dj.html"
|
template_name = "doorcontrol/assign_nfc_card_user_selector.dj.html"
|
||||||
task_group = "update_access_users"
|
|
||||||
|
|
||||||
all_users: list[FullUser] | None = None
|
|
||||||
refresh_task_id = None
|
|
||||||
update_users_results = q2_tasks.result_group(task_group, cached=True)
|
|
||||||
if (
|
|
||||||
update_users_results
|
|
||||||
and len(update_users_results) > 0
|
|
||||||
and not request.POST.get("force_refresh")
|
|
||||||
):
|
|
||||||
all_users = update_users_results[0]
|
|
||||||
else:
|
|
||||||
q2_tasks.delete_group(task_group)
|
|
||||||
refresh_task_id = q2_tasks.async_task(
|
|
||||||
update_access_users, group=task_group, cached=5 * 60
|
|
||||||
)
|
|
||||||
|
|
||||||
filtered_users = []
|
filtered_users = []
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if refresh_task_id:
|
all_users = fetch_access_users_results(
|
||||||
all_users = q2_tasks.result(refresh_task_id, wait=-1, cached=True)
|
request.POST.get("force_refresh") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
template_name += "#results"
|
template_name += "#results"
|
||||||
all_filtered_users = (
|
all_filtered_users = (
|
||||||
@ -514,3 +517,56 @@ class AssignNfcCardView(PermissionRequiredMixin, TemplateView):
|
|||||||
).session_id
|
).session_id
|
||||||
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignedNfcCardsReport(
|
||||||
|
ExportMixin, SingleTableMixin, PermissionRequiredMixin, TemplateView
|
||||||
|
):
|
||||||
|
permission_required = "doorcontrol.assign_nfc_card"
|
||||||
|
template_name = "doorcontrol/assigned_nfc_cards_report.dj.html"
|
||||||
|
table_class = AssignedNfcCardsTable
|
||||||
|
export_formats = ("csv", "xlsx", "ods")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
if "form" not in kwargs:
|
||||||
|
kwargs["form"] = AssignedNfcCardsReportFilters(self.request.GET)
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
access_users = fetch_access_users_results(
|
||||||
|
force_refresh=("refresh" in self.request.GET)
|
||||||
|
)
|
||||||
|
if access_users:
|
||||||
|
access_users_by_employee_number = {
|
||||||
|
user.employee_number: user for user in access_users
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
access_users_by_employee_number = {}
|
||||||
|
|
||||||
|
form = AssignedNfcCardsReportFilters(self.request.GET)
|
||||||
|
|
||||||
|
def get_filtered_members():
|
||||||
|
members = Member.objects.with_is_active().filter(is_active=True)
|
||||||
|
if form.is_valid() and form.cleaned_data["has_mw_nfc_card"] is not None:
|
||||||
|
members = members.alias(
|
||||||
|
has_nfc_card_number=(
|
||||||
|
Q(nfc_card_number__isnull=False) & ~Q(nfc_card_number="")
|
||||||
|
)
|
||||||
|
).filter(has_nfc_card_number=form.cleaned_data["has_mw_nfc_card"])
|
||||||
|
|
||||||
|
for member in members.all():
|
||||||
|
access_user = access_users_by_employee_number.get(member.uid, None)
|
||||||
|
if (
|
||||||
|
form.is_valid()
|
||||||
|
and form.cleaned_data["has_access_nfc_card"] is not None
|
||||||
|
and access_user
|
||||||
|
and (
|
||||||
|
bool(access_user.nfc_cards)
|
||||||
|
!= form.cleaned_data["has_access_nfc_card"]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield {"member": member, "access_user": access_user}
|
||||||
|
|
||||||
|
return list(get_filtered_members())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user