diff --git a/doorcontrol/dashboard.py b/doorcontrol/dashboard.py
index dc00125..f88d069 100644
--- a/doorcontrol/dashboard.py
+++ b/doorcontrol/dashboard.py
@@ -18,12 +18,13 @@ class DoorControlDashboardFragment(dashboard.LinksCardDashboardFragment):
for name, link in report._report_types()
] + [
Link(
- "Assign NFC Card ",
+ "Assign NFC Card",
reverse("doorcontrol:assign-nfc-card-user-selector"),
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")
diff --git a/doorcontrol/forms.py b/doorcontrol/forms.py
index 719255d..d455dbe 100644
--- a/doorcontrol/forms.py
+++ b/doorcontrol/forms.py
@@ -39,3 +39,14 @@ class AttributeScheduleRuleForm(forms.ModelForm):
class Meta:
model = AttributeScheduleRule
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")]),
+ )
diff --git a/doorcontrol/tables.py b/doorcontrol/tables.py
index 3bfb6d2..f253f6e 100644
--- a/doorcontrol/tables.py
+++ b/doorcontrol/tables.py
@@ -63,3 +63,25 @@ class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = 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 %}
+
+ {% for card in value %}
+ - {{ card.type }}: {{ card.id }}
+ {% endfor %}
+
+ {% 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")
diff --git a/doorcontrol/tasks/update_unifi_access.py b/doorcontrol/tasks/update_unifi_access.py
index 3c4e6eb..904049c 100644
--- a/doorcontrol/tasks/update_unifi_access.py
+++ b/doorcontrol/tasks/update_unifi_access.py
@@ -5,7 +5,7 @@ from django.conf import settings
from django.db.models import Q
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 cmsmanage.django_q2_helper import q_task_group
@@ -172,3 +172,11 @@ def update_access():
)
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())
diff --git a/doorcontrol/templates/doorcontrol/assigned_nfc_cards_report.dj.html b/doorcontrol/templates/doorcontrol/assigned_nfc_cards_report.dj.html
new file mode 100644
index 0000000..253ca0c
--- /dev/null
+++ b/doorcontrol/templates/doorcontrol/assigned_nfc_cards_report.dj.html
@@ -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 %}
+
+
+
+ {% render_table table %}
+
+{% endblock %}
diff --git a/doorcontrol/urls.py b/doorcontrol/urls.py
index 29b6f6d..e584c56 100644
--- a/doorcontrol/urls.py
+++ b/doorcontrol/urls.py
@@ -15,4 +15,9 @@ urlpatterns = [report._urlpattern() for report in views.REPORTS] + [
views.AssignNfcCardView.as_view(),
name="assign-nfc-card",
),
+ path(
+ "assigned-nfc-cards/",
+ views.AssignedNfcCardsReport.as_view(),
+ name="assigned-nfc-cards",
+ ),
]
diff --git a/doorcontrol/views.py b/doorcontrol/views.py
index f677e5c..a563291 100644
--- a/doorcontrol/views.py
+++ b/doorcontrol/views.py
@@ -20,6 +20,7 @@ import django_filters
import django_q.tasks as q2_tasks
import django_tables2 as tables
from django_filters.views import BaseFilterView
+from django_q.signing import BadSignature
from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin
from pydantic import BaseModel, Field, ValidationError
@@ -34,8 +35,13 @@ from unifi_access.schemas import (
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 .tables import (
+ AssignedNfcCardsTable,
BusiestDayOfWeekTable,
BusiestTimeOfDayTable,
DeniedAccessTable,
@@ -314,39 +320,36 @@ class BusiestTimeOfDay(BaseAccessReport):
)
-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())
+def fetch_access_users_results(force_refresh: bool = False) -> list[FullUser] | None:
+ task_group = update_access_users.q_task_group
+
+ 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 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
@permission_required("doorcontrol.assign_nfc_card", raise_exception=True)
def assign_nfc_card_user_selector(request: HttpRequest):
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 = []
if request.method == "POST":
- if refresh_task_id:
- all_users = q2_tasks.result(refresh_task_id, wait=-1, cached=True)
+ all_users = fetch_access_users_results(
+ request.POST.get("force_refresh") == "true"
+ )
template_name += "#results"
all_filtered_users = (
@@ -514,3 +517,56 @@ class AssignNfcCardView(PermissionRequiredMixin, TemplateView):
).session_id
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())