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 %} + + {% 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 %} +
+
+
+ {% for field in form %} +
+ {{ field.label_tag }} {% render_field field class+="form-select form-select-sm" %} +
+ {% endfor %} +
+ +
+ +
{% include "cmsmanage/components/download_table.dj.html" %}
+
+
+ + {% 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())