diff --git a/src/c3nav/control/migrations/0010_userpermissions_mesh_control.py b/src/c3nav/control/migrations/0010_userpermissions_mesh_control.py new file mode 100644 index 00000000..452a1638 --- /dev/null +++ b/src/c3nav/control/migrations/0010_userpermissions_mesh_control.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-11-09 14:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("control", "0009_django_4_0"), + ] + + operations = [ + migrations.AddField( + model_name="userpermissions", + name="mesh_control", + field=models.BooleanField( + default=False, verbose_name="can access mesh control" + ), + ), + ] diff --git a/src/c3nav/control/models.py b/src/c3nav/control/models.py index 808b08ab..8fb006d3 100644 --- a/src/c3nav/control/models.py +++ b/src/c3nav/control/models.py @@ -35,6 +35,8 @@ class UserPermissions(models.Model): limit_choices_to={'access_restriction': None}, verbose_name=_('can review reports belonging to')) + mesh_control = models.BooleanField(default=False, verbose_name=_('can access mesh control')) + api_secret = models.CharField(null=True, blank=True, max_length=64, verbose_name=_('API secret')) class Meta: diff --git a/src/c3nav/control/templates/control/base.html b/src/c3nav/control/templates/control/base.html index 862ae459..f69b507c 100644 --- a/src/c3nav/control/templates/control/base.html +++ b/src/c3nav/control/templates/control/base.html @@ -24,7 +24,7 @@ {% if request.user_permissions.manage_map_updates %} {% trans 'Map Updates' %} · {% endif %} - {% trans 'Mesh' %} · + {% trans 'Mesh' %} · {{ request.user.username }}

diff --git a/src/c3nav/control/templates/control/mesh_nodes.html b/src/c3nav/control/templates/control/mesh_nodes.html deleted file mode 100644 index b5d8e46a..00000000 --- a/src/c3nav/control/templates/control/mesh_nodes.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends 'control/base.html' %} -{% load i18n %} - -{% block heading %}{% trans 'Mesh' %}{% endblock %} - -{% block subcontent %} -
-
-

View messages

- - {% trans 'View received messages' %} - -
-
-

View firmwares

- - {% trans 'View firmwares' %} - -
-
-

Send messages

-
- {% csrf_token %} - - -
-
-
-

Logs

- - {% trans 'View log' %} - -
-
-

Nodes

- - - - - - - - - - {% for node in nodes %} - - - - - - - {% comment %}todo: hide uplink if timed out{% endcomment %} - {% comment %}todo: more details{% endcomment %} - - - {% endfor %} -
{% trans 'Node' %}{% trans 'Chip' %}{% trans 'Firmware' %}{% trans 'Last msg' %}{% trans 'Last signin' %}{% trans 'Uplink' %}
{{ node }} - {{ node.last_messages.CONFIG_BOARD.parsed.board_config.board.pretty_name }} - ({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }}) - - {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.version }} - (IDF {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.idf_version }}) - - {% blocktrans trimmed with timesince=node.last_msg|timesince %} - {{ timesince }} ago - {% endblocktrans %} - - {% blocktrans trimmed with timesince=node.last_signin|timesince %} - {{ timesince }} ago - {% endblocktrans %} - {% if node.uplink %}{{ node.uplink.node }}{% endif %}
-{% endblock %} diff --git a/src/c3nav/control/templatetags/mesh_node.py b/src/c3nav/control/templatetags/mesh_node.py index 97fed52a..baa835a1 100644 --- a/src/c3nav/control/templatetags/mesh_node.py +++ b/src/c3nav/control/templatetags/mesh_node.py @@ -18,12 +18,12 @@ def mesh_node(context, node: str | MeshNode): if name: return format_html( '{bssid} ({name})', - url=reverse('control.mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid, name=name + url=reverse('mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid, name=name ) else: return format_html( '{bssid}', - url=reverse('control.mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid + url=reverse('mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid ) diff --git a/src/c3nav/control/urls.py b/src/c3nav/control/urls.py index ad0d0925..4b828505 100644 --- a/src/c3nav/control/urls.py +++ b/src/c3nav/control/urls.py @@ -4,9 +4,6 @@ from c3nav.control.views.access import grant_access, grant_access_qr from c3nav.control.views.announcements import announcement_detail, announcement_list from c3nav.control.views.base import ControlPanelIndexView from c3nav.control.views.mapupdates import map_updates -from c3nav.control.views.mesh import (MeshFirmwareBuildDetailView, MeshFirmwareDetailView, MeshFirmwaresCurrentListView, - MeshFirmwaresListView, MeshLogView, MeshMessageListView, MeshMessageSendingView, - MeshMessageSendView, MeshNodeDetailView, MeshNodeEditView, MeshNodeListView) from c3nav.control.views.users import UserListView, user_detail urlpatterns = [ @@ -17,18 +14,5 @@ urlpatterns = [ path('announcements/', announcement_list, name='control.announcements'), path('announcements//', announcement_detail, name='control.announcements.detail'), path('mapupdates/', map_updates, name='control.map_updates'), - path('mesh/', MeshNodeListView.as_view(), name='control.mesh.nodes'), - path('mesh/logs/', MeshLogView.as_view(), name='control.mesh.log'), - path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh.messages'), - path('mesh/firmwares/', MeshFirmwaresListView.as_view(), name='control.mesh.firmwares'), - path('mesh/firmwares/current/', MeshFirmwaresCurrentListView.as_view(), name='control.mesh.firmwares.current'), - path('mesh/firmwares//', MeshFirmwareDetailView.as_view(), name='control.mesh.firmwares.detail'), - path('mesh/firmwares/builds//', MeshFirmwareBuildDetailView.as_view(), - name='control.mesh.firmwares.build.detail'), - path('mesh//', MeshNodeDetailView.as_view(), name='control.mesh.node.detail'), - path('mesh//edit/', MeshNodeEditView.as_view(), name='control.mesh.node.edit'), - path('mesh/message/sending//', MeshMessageSendingView.as_view(), name='control.mesh.sending'), - path('mesh/message///', MeshMessageSendView.as_view(), name='control.mesh.send'), - path('mesh/message//', MeshMessageSendView.as_view(), name='control.mesh.send'), path('', ControlPanelIndexView.as_view(), name='control.index'), ] diff --git a/src/c3nav/control/views/mesh.py b/src/c3nav/control/views/mesh.py deleted file mode 100644 index df940432..00000000 --- a/src/c3nav/control/views/mesh.py +++ /dev/null @@ -1,290 +0,0 @@ -from functools import cached_property -from uuid import uuid4 - -from django.contrib import messages -from django.contrib.messages.views import SuccessMessageMixin -from django.db.models import Max -from django.http import Http404 -from django.shortcuts import redirect -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, FormView, ListView, TemplateView, UpdateView - -from c3nav.control.forms import MeshMessageFilterForm -from c3nav.control.views.base import ControlPanelMixin -from c3nav.mesh.forms import MeshMessageForm, MeshNodeForm -from c3nav.mesh.messages import MeshMessage, MeshMessageType -from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode, NodeMessage -from c3nav.mesh.utils import get_node_names, group_msg_type_choices - - -class MeshNodeListView(ControlPanelMixin, ListView): - model = MeshNode - template_name = "control/mesh_nodes.html" - ordering = "address" - context_object_name = "nodes" - - def get_queryset(self): - return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages() - - def get_context_data(self, *args, **kwargs): - return { - **super().get_context_data(*args, **kwargs), - "send_msg_types": group_msg_type_choices({msg_type for msg_type in MeshMessageForm.msg_types.keys()}) - } - - def post(self, request): - return redirect( - reverse("control.mesh.send", kwargs={"msg_type": request.POST.get("send_msg_type", "")}) - ) - - -class MeshNodeDetailView(ControlPanelMixin, DetailView): - model = MeshNode - template_name = "control/mesh_node_detail.html" - pk_url_kwargs = "address" - context_object_name = "node" - - def get_queryset(self): - return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages() - - -class MeshNodeEditView(ControlPanelMixin, SuccessMessageMixin, UpdateView): - model = MeshNode - form_class = MeshNodeForm - template_name = "control/form.html" - success_message = _('Name updated successfully') - - def get_context_data(self, **kwargs): - return { - **super().get_context_data(), - 'title': _('Editing mesh node: %s') % self.get_object(), - } - - def get_success_url(self): - return reverse('control.mesh.node.detail', kwargs={'pk': self.get_object().pk}) - - -class MeshMessageListView(ControlPanelMixin, ListView): - model = NodeMessage - template_name = "control/mesh_messages.html" - ordering = "-datetime" - paginate_by = 20 - context_object_name = "mesh_messages" - - def get_queryset(self): - qs = super().get_queryset() - - self.form = MeshMessageFilterForm(self.request.GET) - if self.form.is_valid(): - if self.form.cleaned_data['message_types']: - qs = qs.filter(message_type__in=self.form.cleaned_data['message_types']) - if self.form.cleaned_data['src_nodes']: - qs = qs.filter(src_node__in=self.form.cleaned_data['src_nodes']) - - return qs - - def get_context_data(self, *args, **kwargs): - ctx = super().get_context_data(*args, **kwargs) - - form_data = self.request.GET.copy() - form_data.pop('page', None) - - ctx.update({ - "node_names": get_node_names(), - 'form': self.form, - 'form_data': form_data.urlencode(), - }) - return ctx - - -class MeshMessageSendView(ControlPanelMixin, FormView): - template_name = "control/mesh_message_send.html" - - @cached_property - def msg_type(self): - return MeshMessageType[self.kwargs['msg_type']] - - def get_form_class(self): - try: - return MeshMessageForm.get_form_for_type(self.msg_type) - except KeyError: - raise Http404('unknown message type') - - def get_form_kwargs(self): - return { - **super().get_form_kwargs(), - 'recipient': self.kwargs.get('recipient', None), - } - - def get_initial(self): - if 'recipient' in self.kwargs and self.msg_type.name.startswith('CONFIG_'): - try: - node = MeshNode.objects.get(address=self.kwargs['recipient']) - except MeshNode.DoesNotExist: - pass - else: - initial = MeshMessage.get_type(self.msg_type).tojson( - node.last_messages[self.msg_type].parsed - ) - while keys := tuple(key for key, value in initial.items() if isinstance(value, dict)): - for key in keys: - subdict = initial.pop(key) - for subkey, value in subdict.items(): - initial[key+"_"+subkey.removeprefix(key).lstrip('_')] = value - return initial - - if 'address' in self.request.GET and self.msg_type == MeshMessageType.MESH_ROUTE_REQUEST: - return {"address": self.request.GET["address"]} - - return {} - - def get_success_url(self): - if 'recipient' in self.kwargs and False: - return reverse('control.mesh.node.detail', kwargs={'pk': self.kwargs['recipient']}) - else: - return self.request.path - - def form_valid(self, form): - if 'noscript' in self.request.POST: - form.send() - messages.success(self.request, _('Message sent successfully(?)')) - super().form_valid(form) - uuid = uuid4() - self.request.session["mesh_msg_%s" % uuid] = { - "success_url": self.get_success_url(), - "recipients": form.get_recipients(), - "msg_data": form.get_msg_data(), - } - return redirect(reverse('control.mesh.sending', kwargs={'uuid': uuid})) - - -class MeshMessageSendingView(ControlPanelMixin, TemplateView): - template_name = "control/mesh_message_sending.html" - - def get_context_data(self, uuid): - try: - data = self.request.session["mesh_msg_%s" % uuid] - except KeyError: - raise Http404 - node_names = get_node_names() - return { - **super().get_context_data(), - "node_names": node_names, - "send_uuid": uuid, - **data, - "node_name": node_names.get(data["msg_data"].get("address"), ""), - "recipients": [(address, node_names[address]) for address in data["recipients"]], - "msg_type": MeshMessageType[data["msg_data"]["msg_type"]].pretty_name, - } - - -class MeshLogView(ControlPanelMixin, TemplateView): - template_name = "control/mesh_logs.html" - - def get_context_data(self, **kwargs): - return { - **super().get_context_data(), - "node_names": get_node_names(), - } - - -class MeshFirmwaresListView(ControlPanelMixin, ListView): - model = FirmwareVersion - template_name = "control/mesh_firmwares.html" - ordering = "-created" - context_object_name = "firmwares" - paginate_by = 20 - - -class MeshFirmwaresCurrentListView(ControlPanelMixin, TemplateView): - template_name = "control/mesh_firmwares_current.html" - - def get_context_data(self, **kwargs): - nodes = list(MeshNode.objects.all().prefetch_firmwares()) - - firmwares = {} - for node in nodes: - firmwares.setdefault(node.firmware_desc.get_lookup(), (node.firmware_desc, []))[1].append(node) - - firmwares = sorted(firmwares.values(), key=lambda k: k[0].created, reverse=True) - - print(firmwares) - - return { - **super().get_context_data(), - "firmwares": firmwares, - } - - -class MeshFirmwareDetailView(ControlPanelMixin, DetailView): - model = FirmwareVersion - template_name = "control/mesh_firmware_detail.html" - context_object_name = "firmware" - - def get_queryset(self): - return super().get_queryset().prefetch_related('builds', 'builds__firmwarebuildboard_set') - - def get_context_data(self, **kwargs): - ctx = super().get_context_data() - - nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages( - MeshMessageType.CONFIG_BOARD, - )) - builds = self.get_object().builds.all() - - build_lookups = set(build.get_firmware_description().get_lookup() for build in builds) - - installed_nodes = [] - compatible_nodes = [] - for node in nodes: - if node.firmware_desc.get_lookup() in build_lookups: - installed_nodes.append(node) - else: - node.compatible_builds = [] - for build in builds: - if node.board in build.boards: - node.compatible_builds.append(build) - if node.compatible_builds: - compatible_nodes.append(node) - - ctx.update({ - 'builds': builds, - 'installed_nodes': installed_nodes, - 'compatible_nodes': compatible_nodes, - }) - return ctx - - -class MeshFirmwareBuildDetailView(ControlPanelMixin, DetailView): - model = FirmwareBuild - template_name = "control/mesh_firmware_build_detail.html" - context_object_name = "build" - - def get_queryset(self): - return super().get_queryset().prefetch_related('firmwarebuildboard_set') - - def get_context_data(self, **kwargs): - ctx = super().get_context_data() - - nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages( - MeshMessageType.CONFIG_BOARD, - )) - - build_lookup = self.get_object().get_firmware_description().get_lookup() - build_boards = self.get_object().boards - - installed_nodes = [] - compatible_nodes = [] - for node in nodes: - if node.firmware_desc.get_lookup() == build_lookup: - installed_nodes.append(node) - else: - if node.board in build_boards: - compatible_nodes.append(node) - - ctx.update({ - 'installed_nodes': installed_nodes, - 'compatible_nodes': compatible_nodes, - }) - return ctx diff --git a/src/c3nav/mesh/templates/mesh/base.html b/src/c3nav/mesh/templates/mesh/base.html new file mode 100644 index 00000000..5a7f04bb --- /dev/null +++ b/src/c3nav/mesh/templates/mesh/base.html @@ -0,0 +1,41 @@ +{% extends 'site/base.html' %} +{% load i18n %} +{% load compress %} +{% load static %} + +{% block title %}{% trans 'c3nav mesh control' %}{% endblock %} +{% block header_title %}{% trans 'mesh control' %}{% endblock %} +{% block header_title_url %}{% url 'mesh.nodes' %}{% endblock %} + +{% block content %} +
+ {% include 'site/fragment_messages.html' %} + {% block back_link %}{% if not request.mobileclient %}« {% trans 'back to c3nav' %}{% endif %}{% endblock %} +

{% block heading %}{% endblock %}

+ {% block menu %} + +
+ {% endblock %} + {% block subcontent %} + {% endblock %} +
+ {% include 'site/fragment_fakemobileclient.html' %} + {% compress js %} + + + {% endcompress %} +{% endblock %} diff --git a/src/c3nav/control/templates/control/mesh_firmware_build_detail.html b/src/c3nav/mesh/templates/mesh/firmware_build_detail.html similarity index 89% rename from src/c3nav/control/templates/control/mesh_firmware_build_detail.html rename to src/c3nav/mesh/templates/mesh/firmware_build_detail.html index 9102464d..8f7a5ebe 100644 --- a/src/c3nav/control/templates/control/mesh_firmware_build_detail.html +++ b/src/c3nav/mesh/templates/mesh/firmware_build_detail.html @@ -1,4 +1,4 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n mesh_node %} {% block heading %}{% trans 'Mesh' %}{% endblock %} @@ -62,10 +62,10 @@ {% if node.firmware_desc.build %} - + {{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
- Build {{ node.firmware_desc.build.variant }} + Build {{ node.firmware_desc.build.variant }} {% else %} {{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }} {% endif %} diff --git a/src/c3nav/control/templates/control/mesh_firmware_detail.html b/src/c3nav/mesh/templates/mesh/firmware_detail.html similarity index 89% rename from src/c3nav/control/templates/control/mesh_firmware_detail.html rename to src/c3nav/mesh/templates/mesh/firmware_detail.html index 4d9ca2c8..1f0551ba 100644 --- a/src/c3nav/control/templates/control/mesh_firmware_detail.html +++ b/src/c3nav/mesh/templates/mesh/firmware_detail.html @@ -1,4 +1,4 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n mesh_node %} {% block heading %}{% trans 'Mesh' %}{% endblock %} @@ -26,7 +26,7 @@ {% for build in builds %} - + {{ build.variant }} @@ -82,10 +82,10 @@ {% if node.firmware_desc.build %} - + {{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
- Build {{ node.firmware_desc.build.variant }} + Build {{ node.firmware_desc.build.variant }} {% else %} {{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }} {% endif %} diff --git a/src/c3nav/control/templates/control/mesh_firmwares.html b/src/c3nav/mesh/templates/mesh/firmwares.html similarity index 75% rename from src/c3nav/control/templates/control/mesh_firmwares.html rename to src/c3nav/mesh/templates/mesh/firmwares.html index 6b31fd95..7ae0e2cf 100644 --- a/src/c3nav/control/templates/control/mesh_firmwares.html +++ b/src/c3nav/mesh/templates/mesh/firmwares.html @@ -1,11 +1,10 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n %} -{% block heading %}{% trans 'Mesh' %}{% endblock %} +{% block heading %}{% trans 'Available firmwares' %}{% endblock %} {% block subcontent %} -

Firmwares

- + {% trans 'View current firmwares' %} @@ -24,14 +23,14 @@ {{ firmware.created }} {{ firmware.uploader }} - + {{ firmware.project_name }} {{ firmware.version }} {{ firmware.idf_version }} {% for build in firmware.builds.all %} - + {{ build.variant }} ({{ build.get_chip_display }})
{% endfor %} diff --git a/src/c3nav/control/templates/control/mesh_firmwares_current.html b/src/c3nav/mesh/templates/mesh/firmwares_current.html similarity index 76% rename from src/c3nav/control/templates/control/mesh_firmwares_current.html rename to src/c3nav/mesh/templates/mesh/firmwares_current.html index 7cff67a7..64b10471 100644 --- a/src/c3nav/control/templates/control/mesh_firmwares_current.html +++ b/src/c3nav/mesh/templates/mesh/firmwares_current.html @@ -1,11 +1,10 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n mesh_node %} -{% block heading %}{% trans 'Mesh' %}{% endblock %} +{% block heading %}{% trans 'Current firmwares' %}{% endblock %} {% block subcontent %} -

Current Firmwares

- + {% trans 'View available firmwares' %} @@ -22,7 +21,7 @@ {{ firmware.created }} {% if firmware.build %} - {{ firmware.project_name }} {{ firmware.version }} + {{ firmware.project_name }} {{ firmware.version }} {% else %} {{ firmware.project_name }} {{ firmware.version }}
{{ firmware.sha256_hash }} @@ -30,7 +29,7 @@ {% if firmware.build %} - + {{ firmware.build.variant }} ({{ firmware.chip.pretty_name }}) {% else %} diff --git a/src/c3nav/control/templates/control/fragment_mesh_websocket.html b/src/c3nav/mesh/templates/mesh/fragment_mesh_websocket.html similarity index 92% rename from src/c3nav/control/templates/control/fragment_mesh_websocket.html rename to src/c3nav/mesh/templates/mesh/fragment_mesh_websocket.html index df7ad577..1bfdb2b8 100644 --- a/src/c3nav/control/templates/control/fragment_mesh_websocket.html +++ b/src/c3nav/mesh/templates/mesh/fragment_mesh_websocket.html @@ -38,7 +38,7 @@ function connect() { if (data.uplink) { cell.append(document.createElement("br")); link_tag = document.createElement("a"); - link_tag.href = "/control/mesh/" + data.uplink; + link_tag.href = "/mesh/" + data.uplink; link_tag.innerText = data.uplink; if (node_names[data.uplink]) { link_tag.innerText += " ("+node_names[data.uplink]+")"; @@ -49,7 +49,7 @@ function connect() { cell = document.createElement("td"); link_tag = document.createElement("a"); - link_tag.href = "/control/mesh/" + data.node; + link_tag.href = "/mesh/" + data.node; link_tag.innerText = data.node; if (node_names[data.node]) { link_tag.innerText += " ("+node_names[data.node]+")"; @@ -76,7 +76,7 @@ function connect() { line.appendChild(text) link_tag = document.createElement("a"); - link_tag.href = "/control/mesh/" + data.uplink; + link_tag.href = "/mesh/" + data.uplink; link_tag.innerText = data.uplink; if (node_names[data.uplink]) { link_tag.innerText += "("+node_names[data.uplink]+")"; @@ -96,7 +96,7 @@ function connect() { cell = document.createElement("td"); link_tag = document.createElement("a"); - link_tag.href = "/control/mesh/" + data.msg.src; + link_tag.href = "/mesh/" + data.msg.src; link_tag.innerText = data.msg.src; if (node_names[data.msg.src]) { link_tag.innerText += " ("+node_names[data.msg.src]+")"; @@ -109,7 +109,7 @@ function connect() { } else { cell = document.createElement("td"); link_tag = document.createElement("a"); - link_tag.href = "/control/mesh/" + data.msg.route; + link_tag.href = "/mesh/" + data.msg.route; link_tag.innerText = data.msg.route; if (node_names[data.msg.route]) { link_tag.innerText += " ("+node_names[data.msg.route]+")"; @@ -125,7 +125,7 @@ function connect() { cell = document.createElement("td"); link_tag = document.createElement("a"); - link_tag.href = "/control/mesh/" + data.msg.trace[i]; + link_tag.href = "/mesh/" + data.msg.trace[i]; link_tag.innerText = data.msg.trace[i]; if (node_names[data.msg.trace[i]]) { link_tag.innerText += " ("+node_names[data.msg.trace[i]]+")"; @@ -145,4 +145,4 @@ function connect() { connect(); - \ No newline at end of file + diff --git a/src/c3nav/control/templates/control/mesh_logs.html b/src/c3nav/mesh/templates/mesh/mesh_logs.html similarity index 83% rename from src/c3nav/control/templates/control/mesh_logs.html rename to src/c3nav/mesh/templates/mesh/mesh_logs.html index 2c6ee496..d1d31f32 100644 --- a/src/c3nav/control/templates/control/mesh_logs.html +++ b/src/c3nav/mesh/templates/mesh/mesh_logs.html @@ -1,4 +1,4 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n %} {% block heading %}{% trans 'Mesh logs' %}{% endblock %} @@ -15,5 +15,5 @@ - {% include "control/fragment_mesh_websocket.html" %} + {% include "mesh/fragment_mesh_websocket.html" %} {% endblock %} diff --git a/src/c3nav/control/templates/control/mesh_message_send.html b/src/c3nav/mesh/templates/mesh/mesh_message_send.html similarity index 98% rename from src/c3nav/control/templates/control/mesh_message_send.html rename to src/c3nav/mesh/templates/mesh/mesh_message_send.html index 194cef43..1d29af89 100644 --- a/src/c3nav/control/templates/control/mesh_message_send.html +++ b/src/c3nav/mesh/templates/mesh/mesh_message_send.html @@ -1,4 +1,4 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n %} {% block heading %} diff --git a/src/c3nav/control/templates/control/mesh_message_sending.html b/src/c3nav/mesh/templates/mesh/mesh_message_sending.html similarity index 87% rename from src/c3nav/control/templates/control/mesh_message_sending.html rename to src/c3nav/mesh/templates/mesh/mesh_message_sending.html index f12a2b6e..174ad84a 100644 --- a/src/c3nav/control/templates/control/mesh_message_sending.html +++ b/src/c3nav/mesh/templates/mesh/mesh_message_sending.html @@ -1,4 +1,4 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n %} {% block heading %} @@ -10,7 +10,7 @@ {% block subcontent %}

Go back

{% if msg_type == "MESH_ROUTE_REQUEST" %} -

Route to {{ msg_data.address }} {% if node_name %} ({{ node_name }}){% endif %}

+

Route to {{ msg_data.address }} {% if node_name %} ({{ node_name }}){% endif %}

{% endif %}
@@ -23,7 +23,7 @@ {% for address, name in recipients %} - {% if address != "ff:ff:ff:ff:ff:ff" %}{% endif %} + {% if address != "ff:ff:ff:ff:ff:ff" %}{% endif %} {{ address }}{% if name %} ({{ name }}){% endif %} {% if address != "ff:ff:ff:ff:ff:ff" %}{% endif %} @@ -69,6 +69,6 @@
{% endif %}
- {% include "control/fragment_mesh_websocket.html" %} + {% include "mesh/fragment_mesh_websocket.html" %} {% endblock %} diff --git a/src/c3nav/control/templates/control/mesh_messages.html b/src/c3nav/mesh/templates/mesh/mesh_messages.html similarity index 87% rename from src/c3nav/control/templates/control/mesh_messages.html rename to src/c3nav/mesh/templates/mesh/mesh_messages.html index 50f79254..a3c5e5ca 100644 --- a/src/c3nav/control/templates/control/mesh_messages.html +++ b/src/c3nav/mesh/templates/mesh/mesh_messages.html @@ -1,10 +1,30 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n mesh_node %} {% block heading %}{% trans 'Mesh messages' %}{% endblock %} {% block subcontent %} +
+
+

Send messages

+
+ {% csrf_token %} + + +
+
+
+

Filter

{{ form.message_types }} diff --git a/src/c3nav/control/templates/control/mesh_node_detail.html b/src/c3nav/mesh/templates/mesh/node_detail.html similarity index 83% rename from src/c3nav/control/templates/control/mesh_node_detail.html rename to src/c3nav/mesh/templates/mesh/node_detail.html index ca3f6cc4..35428c82 100644 --- a/src/c3nav/control/templates/control/mesh_node_detail.html +++ b/src/c3nav/mesh/templates/mesh/node_detail.html @@ -1,4 +1,4 @@ -{% extends 'control/base.html' %} +{% extends 'mesh/base.html' %} {% load i18n %} {% block heading %}{% trans 'Mesh Node' %} {{ node }}{% endblock %} @@ -13,7 +13,7 @@

{% comment %}todo: more details{% endcomment %} - Uplink: {% if node.uplink %}{{ node.uplink.node }}
{% endif %} + Uplink: {% if node.uplink %}{{ node.uplink.node }}
{% endif %} Last signin: {{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }} @@ -30,13 +30,13 @@

- + {% trans 'Edit' %} - + {% trans 'View messages' %} - + {% trans 'Find route' %}

@@ -58,7 +58,7 @@

- + {% trans 'Chaange board settings' %}

@@ -83,7 +83,7 @@ SSL: {{ node.last_messages.CONFIG_UPLINK.parsed.ssl }}

- + {% trans 'Change' %}

@@ -93,7 +93,7 @@ X={{ node.last_messages.CONFIG_POSITION.parsed.x_pos }}, Y={{ node.last_messages.CONFIG_POSITION.parsed.y_pos }}, Z={{ node.last_messages.CONFIG_POSITION.parsed.z_pos }}

- + {% trans 'Change' %}

diff --git a/src/c3nav/mesh/templates/mesh/nodes.html b/src/c3nav/mesh/templates/mesh/nodes.html new file mode 100644 index 00000000..72c6fa4b --- /dev/null +++ b/src/c3nav/mesh/templates/mesh/nodes.html @@ -0,0 +1,43 @@ +{% extends 'mesh/base.html' %} +{% load i18n %} + +{% block heading %}{% trans 'Mesh Nodes' %}{% endblock %} + +{% block subcontent %} + + + + + + + + + + {% for node in nodes %} + + + + + + + {% comment %}todo: hide uplink if timed out{% endcomment %} + {% comment %}todo: more details{% endcomment %} + + + {% endfor %} +
{% trans 'Node' %}{% trans 'Chip' %}{% trans 'Firmware' %}{% trans 'Last msg' %}{% trans 'Last signin' %}{% trans 'Uplink' %}
{{ node }} + {{ node.last_messages.CONFIG_BOARD.parsed.board_config.board.pretty_name }} + ({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }}) + + {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.version }} + (IDF {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.idf_version }}) + + {% blocktrans trimmed with timesince=node.last_msg|timesince %} + {{ timesince }} ago + {% endblocktrans %} + + {% blocktrans trimmed with timesince=node.last_signin|timesince %} + {{ timesince }} ago + {% endblocktrans %} + {% if node.uplink %}{{ node.uplink.node }}{% endif %}
+{% endblock %} diff --git a/src/c3nav/mesh/urls.py b/src/c3nav/mesh/urls.py index 2b899351..c14b8727 100644 --- a/src/c3nav/mesh/urls.py +++ b/src/c3nav/mesh/urls.py @@ -1,6 +1,26 @@ from django.urls import path from c3nav.mesh.consumers import MeshConsumer, MeshUIConsumer +from c3nav.mesh.views.firmware import (FirmwareBuildDetailView, FirmwareDetailView, FirmwaresCurrentListView, + FirmwaresListView) +from c3nav.mesh.views.messages import MeshMessageListView, MeshMessageSendingView, MeshMessageSendView +from c3nav.mesh.views.misc import MeshLogView +from c3nav.mesh.views.nodes import NodeDetailView, NodeEditView, NodeListView + +urlpatterns = [ + path('', NodeListView.as_view(), name='mesh.nodes'), + path('logs/', MeshLogView.as_view(), name='mesh.logs'), + path('messages/', MeshMessageListView.as_view(), name='mesh.messages'), + path('firmwares/', FirmwaresListView.as_view(), name='mesh.firmwares'), + path('firmwares/current/', FirmwaresCurrentListView.as_view(), name='mesh.firmwares.current'), + path('firmwares//', FirmwareDetailView.as_view(), name='mesh.firmwares.detail'), + path('firmwares/builds//', FirmwareBuildDetailView.as_view(), name='mesh.firmwares.build.detail'), + path('/', NodeDetailView.as_view(), name='mesh.node.detail'), + path('/edit/', NodeEditView.as_view(), name='mesh.node.edit'), + path('message/sending//', MeshMessageSendingView.as_view(), name='mesh.sending'), + path('message///', MeshMessageSendView.as_view(), name='mesh.send'), + path('message//', MeshMessageSendView.as_view(), name='mesh.send'), +] websocket_urlpatterns = [ path('ws', MeshConsumer.as_asgi()), diff --git a/src/c3nav/mesh/views/__init__.py b/src/c3nav/mesh/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/c3nav/mesh/views/base.py b/src/c3nav/mesh/views/base.py new file mode 100644 index 00000000..01a5a731 --- /dev/null +++ b/src/c3nav/mesh/views/base.py @@ -0,0 +1,13 @@ +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin + + +class MeshControlMixin(UserPassesTestMixin, LoginRequiredMixin): + login_url = 'site.login' + user_permission = None + + def test_func(self): + if not self.request.user_permissions.mesh_control: + return False + if not self.user_permission: + return True + return getattr(self.request.user_permissions, self.user_permission) diff --git a/src/c3nav/mesh/views/firmware.py b/src/c3nav/mesh/views/firmware.py new file mode 100644 index 00000000..06e4b368 --- /dev/null +++ b/src/c3nav/mesh/views/firmware.py @@ -0,0 +1,106 @@ +from django.views.generic import DetailView, ListView, TemplateView + +from c3nav.mesh.messages import MeshMessageType +from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode +from c3nav.mesh.views.base import MeshControlMixin + + +class FirmwaresListView(MeshControlMixin, ListView): + model = FirmwareVersion + template_name = "mesh/firmwares.html" + ordering = "-created" + context_object_name = "firmwares" + paginate_by = 20 + + +class FirmwaresCurrentListView(MeshControlMixin, TemplateView): + template_name = "mesh/firmwares_current.html" + + def get_context_data(self, **kwargs): + nodes = list(MeshNode.objects.all().prefetch_firmwares()) + + firmwares = {} + for node in nodes: + firmwares.setdefault(node.firmware_desc.get_lookup(), (node.firmware_desc, []))[1].append(node) + + firmwares = sorted(firmwares.values(), key=lambda k: k[0].created, reverse=True) + + print(firmwares) + + return { + **super().get_context_data(), + "firmwares": firmwares, + } + + +class FirmwareDetailView(MeshControlMixin, DetailView): + model = FirmwareVersion + template_name = "mesh/firmware_detail.html" + context_object_name = "firmware" + + def get_queryset(self): + return super().get_queryset().prefetch_related('builds', 'builds__firmwarebuildboard_set') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + + nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages( + MeshMessageType.CONFIG_BOARD, + )) + builds = self.get_object().builds.all() + + build_lookups = set(build.get_firmware_description().get_lookup() for build in builds) + + installed_nodes = [] + compatible_nodes = [] + for node in nodes: + if node.firmware_desc.get_lookup() in build_lookups: + installed_nodes.append(node) + else: + node.compatible_builds = [] + for build in builds: + if node.board in build.boards: + node.compatible_builds.append(build) + if node.compatible_builds: + compatible_nodes.append(node) + + ctx.update({ + 'builds': builds, + 'installed_nodes': installed_nodes, + 'compatible_nodes': compatible_nodes, + }) + return ctx + + +class FirmwareBuildDetailView(MeshControlMixin, DetailView): + model = FirmwareBuild + template_name = "mesh/firmware_build_detail.html" + context_object_name = "build" + + def get_queryset(self): + return super().get_queryset().prefetch_related('firmwarebuildboard_set') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + + nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages( + MeshMessageType.CONFIG_BOARD, + )) + + build_lookup = self.get_object().get_firmware_description().get_lookup() + build_boards = self.get_object().boards + + installed_nodes = [] + compatible_nodes = [] + for node in nodes: + if node.firmware_desc.get_lookup() == build_lookup: + installed_nodes.append(node) + else: + if node.board in build_boards: + compatible_nodes.append(node) + + ctx.update({ + 'installed_nodes': installed_nodes, + 'compatible_nodes': compatible_nodes, + }) + return ctx diff --git a/src/c3nav/mesh/views/messages.py b/src/c3nav/mesh/views/messages.py new file mode 100644 index 00000000..70c61ed8 --- /dev/null +++ b/src/c3nav/mesh/views/messages.py @@ -0,0 +1,131 @@ +from functools import cached_property +from uuid import uuid4 + +from django.contrib import messages +from django.http import Http404 +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.views.generic import FormView, ListView, TemplateView + +from c3nav.control.forms import MeshMessageFilterForm +from c3nav.mesh.forms import MeshMessageForm +from c3nav.mesh.messages import MeshMessage, MeshMessageType +from c3nav.mesh.models import MeshNode, NodeMessage +from c3nav.mesh.utils import get_node_names, group_msg_type_choices +from c3nav.mesh.views.base import MeshControlMixin + + +class MeshMessageListView(MeshControlMixin, ListView): + model = NodeMessage + template_name = "mesh/mesh_messages.html" + ordering = "-datetime" + paginate_by = 20 + context_object_name = "mesh_messages" + + def get_queryset(self): + qs = super().get_queryset() + + self.form = MeshMessageFilterForm(self.request.GET) + if self.form.is_valid(): + if self.form.cleaned_data['message_types']: + qs = qs.filter(message_type__in=self.form.cleaned_data['message_types']) + if self.form.cleaned_data['src_nodes']: + qs = qs.filter(src_node__in=self.form.cleaned_data['src_nodes']) + + return qs + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + + form_data = self.request.GET.copy() + form_data.pop('page', None) + + ctx.update({ + "node_names": get_node_names(), + "send_msg_types": group_msg_type_choices({msg_type for msg_type in MeshMessageForm.msg_types.keys()}), + 'form': self.form, + 'form_data': form_data.urlencode(), + }) + return ctx + + +class MeshMessageSendView(MeshControlMixin, FormView): + template_name = "mesh/mesh_message_send.html" + + @cached_property + def msg_type(self): + return MeshMessageType[self.kwargs['msg_type']] + + def get_form_class(self): + try: + return MeshMessageForm.get_form_for_type(self.msg_type) + except KeyError: + raise Http404('unknown message type') + + def get_form_kwargs(self): + return { + **super().get_form_kwargs(), + 'recipient': self.kwargs.get('recipient', None), + } + + def get_initial(self): + if 'recipient' in self.kwargs and self.msg_type.name.startswith('CONFIG_'): + try: + node = MeshNode.objects.get(address=self.kwargs['recipient']) + except MeshNode.DoesNotExist: + pass + else: + initial = MeshMessage.get_type(self.msg_type).tojson( + node.last_messages[self.msg_type].parsed + ) + while keys := tuple(key for key, value in initial.items() if isinstance(value, dict)): + for key in keys: + subdict = initial.pop(key) + for subkey, value in subdict.items(): + initial[key+"_"+subkey.removeprefix(key).lstrip('_')] = value + return initial + + if 'address' in self.request.GET and self.msg_type == MeshMessageType.MESH_ROUTE_REQUEST: + return {"address": self.request.GET["address"]} + + return {} + + def get_success_url(self): + if 'recipient' in self.kwargs and False: + return reverse('mesh.node.detail', kwargs={'pk': self.kwargs['recipient']}) + else: + return self.request.path + + def form_valid(self, form): + if 'noscript' in self.request.POST: + form.send() + messages.success(self.request, _('Message sent successfully(?)')) + super().form_valid(form) + uuid = uuid4() + self.request.session["mesh_msg_%s" % uuid] = { + "success_url": self.get_success_url(), + "recipients": form.get_recipients(), + "msg_data": form.get_msg_data(), + } + return redirect(reverse('mesh.sending', kwargs={'uuid': uuid})) + + +class MeshMessageSendingView(MeshControlMixin, TemplateView): + template_name = "mesh/mesh_message_sending.html" + + def get_context_data(self, uuid): + try: + data = self.request.session["mesh_msg_%s" % uuid] + except KeyError: + raise Http404 + node_names = get_node_names() + return { + **super().get_context_data(), + "node_names": node_names, + "send_uuid": uuid, + **data, + "node_name": node_names.get(data["msg_data"].get("address"), ""), + "recipients": [(address, node_names[address]) for address in data["recipients"]], + "msg_type": MeshMessageType[data["msg_data"]["msg_type"]].pretty_name, + } diff --git a/src/c3nav/mesh/views/misc.py b/src/c3nav/mesh/views/misc.py new file mode 100644 index 00000000..7bb28f47 --- /dev/null +++ b/src/c3nav/mesh/views/misc.py @@ -0,0 +1,14 @@ +from django.views.generic import TemplateView + +from c3nav.mesh.utils import get_node_names +from c3nav.mesh.views.base import MeshControlMixin + + +class MeshLogView(MeshControlMixin, TemplateView): + template_name = "mesh/mesh_logs.html" + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(), + "node_names": get_node_names(), + } diff --git a/src/c3nav/mesh/views/nodes.py b/src/c3nav/mesh/views/nodes.py new file mode 100644 index 00000000..04958d7f --- /dev/null +++ b/src/c3nav/mesh/views/nodes.py @@ -0,0 +1,51 @@ +from django.contrib.messages.views import SuccessMessageMixin +from django.db.models import Max +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.views.generic import DetailView, ListView, UpdateView + +from c3nav.mesh.forms import MeshNodeForm +from c3nav.mesh.models import MeshNode +from c3nav.mesh.views.base import MeshControlMixin + + +class NodeListView(MeshControlMixin, ListView): + model = MeshNode + template_name = "mesh/nodes.html" + ordering = "address" + context_object_name = "nodes" + + def get_queryset(self): + return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages() + + def post(self, request): + return redirect( + reverse("control.mesh.send", kwargs={"msg_type": request.POST.get("send_msg_type", "")}) + ) + + +class NodeDetailView(MeshControlMixin, DetailView): + model = MeshNode + template_name = "mesh/node_detail.html" + pk_url_kwargs = "address" + context_object_name = "node" + + def get_queryset(self): + return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages() + + +class NodeEditView(MeshControlMixin, SuccessMessageMixin, UpdateView): + model = MeshNode + form_class = MeshNodeForm + template_name = "control/form.html" + success_message = _('Name updated successfully') + + def get_context_data(self, **kwargs): + return { + **super().get_context_data(), + 'title': _('Editing mesh node: %s') % self.get_object(), + } + + def get_success_url(self): + return reverse('mesh.node.detail', kwargs={'pk': self.get_object().pk}) diff --git a/src/c3nav/urls.py b/src/c3nav/urls.py index 75482dfa..4f79e131 100644 --- a/src/c3nav/urls.py +++ b/src/c3nav/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ path('map/', include(c3nav.mapdata.urls)), path('admin/', admin.site.urls), path('control/', include(c3nav.control.urls)), + path('mesh/', include(c3nav.mesh.urls)), path('locales/', include('django.conf.urls.i18n')), path('', include(c3nav.site.urls)), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)