move mesh control into mesh module and add permissions
This commit is contained in:
parent
ce8f5f0084
commit
88d6f07eaf
27 changed files with 504 additions and 432 deletions
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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:
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
{% if request.user_permissions.manage_map_updates %}
|
||||
<a href="{% url 'control.map_updates' %}">{% trans 'Map Updates' %}</a> ·
|
||||
{% endif %}
|
||||
<a href="{% url 'control.mesh.nodes' %}">{% trans 'Mesh' %}</a> ·
|
||||
<a href="{% url 'mesh.nodes' %}">{% trans 'Mesh' %}</a> ·
|
||||
<a href="{% url 'control.users.detail' user=request.user.pk %}">{{ request.user.username }}</a>
|
||||
</p>
|
||||
</nav>
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
{% extends 'control/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<div class="columns">
|
||||
<div>
|
||||
<h4>View messages</h4>
|
||||
<a class="button" href="{% url "control.mesh.messages" %}">
|
||||
{% trans 'View received messages' %}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h4>View firmwares</h4>
|
||||
<a class="button" href="{% url "control.mesh.firmwares" %}">
|
||||
{% trans 'View firmwares' %}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Send messages</h4>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<select name="send_msg_type" style="display: inline-block; width: auto;" required>
|
||||
<option value="">select type</option>
|
||||
{% for category, msg_types in send_msg_types %}
|
||||
<optgroup label="{{ category }}">
|
||||
{% for value, label in msg_types %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">{% trans 'Send message' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Logs</h4>
|
||||
<a class="button" href="{% url "control.mesh.log" %}">
|
||||
{% trans 'View log' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Nodes</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Chip' %}</th>
|
||||
<th>{% trans 'Firmware' %}</th>
|
||||
<th>{% trans 'Last msg' %}</th>
|
||||
<th>{% trans 'Last signin' %}</th>
|
||||
<th>{% trans 'Uplink' %}</th>
|
||||
</tr>
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td><a href="{% url "control.mesh.node.detail" pk=node.address %}">{{ node }}</a></td>
|
||||
<td>
|
||||
{{ node.last_messages.CONFIG_BOARD.parsed.board_config.board.pretty_name }}
|
||||
({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} <small>rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }}</small>)
|
||||
</td>
|
||||
<td>
|
||||
{{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.version }}
|
||||
<small>(IDF {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.idf_version }})</small>
|
||||
</td>
|
||||
<td>
|
||||
{% blocktrans trimmed with timesince=node.last_msg|timesince %}
|
||||
{{ timesince }} ago
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td>
|
||||
{% blocktrans trimmed with timesince=node.last_signin|timesince %}
|
||||
{{ timesince }} ago
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
{% comment %}todo: hide uplink if timed out{% endcomment %}
|
||||
{% comment %}todo: more details{% endcomment %}
|
||||
<td>{% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -18,12 +18,12 @@ def mesh_node(context, node: str | MeshNode):
|
|||
if name:
|
||||
return format_html(
|
||||
'<a href="{url}">{bssid}</a> ({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(
|
||||
'<a href="{url}">{bssid}</a>',
|
||||
url=reverse('control.mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid
|
||||
url=reverse('mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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/<int:annoucement>/', 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/<int:pk>/', MeshFirmwareDetailView.as_view(), name='control.mesh.firmwares.detail'),
|
||||
path('mesh/firmwares/builds/<int:pk>/', MeshFirmwareBuildDetailView.as_view(),
|
||||
name='control.mesh.firmwares.build.detail'),
|
||||
path('mesh/<str:pk>/', MeshNodeDetailView.as_view(), name='control.mesh.node.detail'),
|
||||
path('mesh/<str:pk>/edit/', MeshNodeEditView.as_view(), name='control.mesh.node.edit'),
|
||||
path('mesh/message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='control.mesh.sending'),
|
||||
path('mesh/message/<str:recipient>/<str:msg_type>/', MeshMessageSendView.as_view(), name='control.mesh.send'),
|
||||
path('mesh/message/<str:msg_type>/', MeshMessageSendView.as_view(), name='control.mesh.send'),
|
||||
path('', ControlPanelIndexView.as_view(), name='control.index'),
|
||||
]
|
||||
|
|
|
@ -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
|
41
src/c3nav/mesh/templates/mesh/base.html
Normal file
41
src/c3nav/mesh/templates/mesh/base.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{% extends 'site/base.html' %}
|
||||
{% load i18n %}
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'c3nav mesh control' %}{% endblock %}
|
||||
{% block header_title %}<span id="subheader">{% trans 'mesh control' %}</span>{% endblock %}
|
||||
{% block header_title_url %}{% url 'mesh.nodes' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="control"{% block addattributes %}{% endblock %}>
|
||||
{% include 'site/fragment_messages.html' %}
|
||||
{% block back_link %}{% if not request.mobileclient %}<a href="{% url 'site.index' %}" class="float-right">« {% trans 'back to c3nav' %}</a>{% endif %}{% endblock %}
|
||||
<h2>{% block heading %}{% endblock %}</h2>
|
||||
{% block menu %}
|
||||
<nav>
|
||||
<p>
|
||||
<a href="{% url 'mesh.nodes' %}">Nodes</a> ·
|
||||
<a href="{% url 'mesh.messages' %}">Messages</a> ·
|
||||
<a href="{% url 'mesh.firmwares' %}">Firmwares</a> ·
|
||||
<a href="{% url 'mesh.logs' %}">Live logs</a> ·
|
||||
</p>
|
||||
</nav>
|
||||
<hr>
|
||||
{% endblock %}
|
||||
{% block subcontent %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% include 'site/fragment_fakemobileclient.html' %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static 'jquery/jquery.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
if (window.mobileclient) {
|
||||
var $body = $('body');
|
||||
if ($body.is('[data-user-data]')) {
|
||||
mobileclient.setUserData($body.attr('data-user-data'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
|
@ -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 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if node.firmware_desc.build %}
|
||||
<a href="{% url "control.mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
|
||||
<a href="{% url "mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
|
||||
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||
</a><br>
|
||||
Build <a href="{% url "control.mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
|
||||
Build <a href="{% url "mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
|
||||
{% else %}
|
||||
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||
{% endif %}
|
|
@ -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 %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url "control.mesh.firmwares.build.detail" pk=build.pk %}">
|
||||
<a href="{% url "mesh.firmwares.build.detail" pk=build.pk %}">
|
||||
{{ build.variant }}
|
||||
</a>
|
||||
</td>
|
||||
|
@ -82,10 +82,10 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if node.firmware_desc.build %}
|
||||
<a href="{% url "control.mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
|
||||
<a href="{% url "mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
|
||||
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||
</a><br>
|
||||
Build <a href="{% url "control.mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
|
||||
Build <a href="{% url "mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
|
||||
{% else %}
|
||||
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||
{% endif %}
|
|
@ -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 %}
|
||||
<h4>Firmwares</h4>
|
||||
<a class="button" href="{% url "control.mesh.firmwares.current" %}">
|
||||
<a class="button" href="{% url "mesh.firmwares.current" %}">
|
||||
{% trans 'View current firmwares' %}
|
||||
</a>
|
||||
|
||||
|
@ -24,14 +23,14 @@
|
|||
<td>{{ firmware.created }}</td>
|
||||
<td>{{ firmware.uploader }}</td>
|
||||
<td>
|
||||
<a href="{% url "control.mesh.firmwares.detail" pk=firmware.pk %}">
|
||||
<a href="{% url "mesh.firmwares.detail" pk=firmware.pk %}">
|
||||
{{ firmware.project_name }} {{ firmware.version }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ firmware.idf_version }}</td>
|
||||
<td>
|
||||
{% for build in firmware.builds.all %}
|
||||
<a href="{% url "control.mesh.firmwares.build.detail" pk=build.pk %}">
|
||||
<a href="{% url "mesh.firmwares.build.detail" pk=build.pk %}">
|
||||
{{ build.variant }} ({{ build.get_chip_display }})
|
||||
</a><br>
|
||||
{% endfor %}
|
|
@ -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 %}
|
||||
<h4>Current Firmwares</h4>
|
||||
<a class="button" href="{% url "control.mesh.firmwares" %}">
|
||||
<a class="button" href="{% url "mesh.firmwares" %}">
|
||||
{% trans 'View available firmwares' %}
|
||||
</a>
|
||||
|
||||
|
@ -22,7 +21,7 @@
|
|||
<td>{{ firmware.created }}</td>
|
||||
<td>
|
||||
{% if firmware.build %}
|
||||
<a href="{% url "control.mesh.firmwares.detail" pk=firmware.pk %}">{{ firmware.project_name }} {{ firmware.version }}</a>
|
||||
<a href="{% url "mesh.firmwares.detail" pk=firmware.pk %}">{{ firmware.project_name }} {{ firmware.version }}</a>
|
||||
{% else %}
|
||||
{{ firmware.project_name }} {{ firmware.version }}<br>
|
||||
<small>{{ firmware.sha256_hash }}</small>
|
||||
|
@ -30,7 +29,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if firmware.build %}
|
||||
<a href="{% url "control.mesh.firmwares.build.detail" pk=firmware.build.pk %}">
|
||||
<a href="{% url "mesh.firmwares.build.detail" pk=firmware.build.pk %}">
|
||||
{{ firmware.build.variant }} ({{ firmware.chip.pretty_name }})
|
||||
</a>
|
||||
{% else %}
|
|
@ -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]]+")";
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'control/base.html' %}
|
||||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}{% trans 'Mesh logs' %}{% endblock %}
|
||||
|
@ -15,5 +15,5 @@
|
|||
</thead>
|
||||
<tbody id="mesh-logs"></tbody>
|
||||
</table>
|
||||
{% include "control/fragment_mesh_websocket.html" %}
|
||||
{% include "mesh/fragment_mesh_websocket.html" %}
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'control/base.html' %}
|
||||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'control/base.html' %}
|
||||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}
|
||||
|
@ -10,7 +10,7 @@
|
|||
{% block subcontent %}
|
||||
<p><a class="button" href="{{ success_url }}">Go back</a></p>
|
||||
{% if msg_type == "MESH_ROUTE_REQUEST" %}
|
||||
<p>Route to <a href="{% url "control.mesh.node.detail" pk=msg_data.address %}">{{ msg_data.address }} {% if node_name %} ({{ node_name }}){% endif %}</a></p>
|
||||
<p>Route to <a href="{% url "mesh.node.detail" pk=msg_data.address %}">{{ msg_data.address }} {% if node_name %} ({{ node_name }}){% endif %}</a></p>
|
||||
{% endif %}
|
||||
<div class="columns">
|
||||
<div>
|
||||
|
@ -23,7 +23,7 @@
|
|||
{% for address, name in recipients %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if address != "ff:ff:ff:ff:ff:ff" %}<a href="{% url "control.mesh.node.detail" pk=address %}">{% endif %}
|
||||
{% if address != "ff:ff:ff:ff:ff:ff" %}<a href="{% url "mesh.node.detail" pk=address %}">{% endif %}
|
||||
{{ address }}{% if name %} ({{ name }}){% endif %}
|
||||
{% if address != "ff:ff:ff:ff:ff:ff" %}</a>{% endif %}
|
||||
</td>
|
||||
|
@ -69,6 +69,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "control/fragment_mesh_websocket.html" %}
|
||||
{% include "mesh/fragment_mesh_websocket.html" %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,10 +1,30 @@
|
|||
{% extends 'control/base.html' %}
|
||||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n mesh_node %}
|
||||
|
||||
{% block heading %}{% trans 'Mesh messages' %}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<div class="columns">
|
||||
<div>
|
||||
<h4>Send messages</h4>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<select name="send_msg_type" style="display: inline-block; width: auto;" required>
|
||||
<option value="">select type</option>
|
||||
{% for category, msg_types in send_msg_types %}
|
||||
<optgroup label="{{ category }}">
|
||||
{% for value, label in msg_types %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">{% trans 'Send message' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form>
|
||||
<h4>Filter</h4>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
{{ form.message_types }}
|
|
@ -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 @@
|
|||
</p>
|
||||
<p>
|
||||
{% comment %}todo: more details{% endcomment %}
|
||||
<strong>Uplink:</strong> {% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a><br>{% endif %}
|
||||
<strong>Uplink:</strong> {% if node.uplink %}<a href="{% url "mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a><br>{% endif %}
|
||||
|
||||
<strong>Last signin:</strong>
|
||||
{{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }}
|
||||
|
@ -30,13 +30,13 @@
|
|||
<br>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="{% url "control.mesh.node.edit" pk=node.pk %}">
|
||||
<a class="button" href="{% url "mesh.node.edit" pk=node.pk %}">
|
||||
{% trans 'Edit' %}
|
||||
</a>
|
||||
<a class="button" href="{% url "control.mesh.messages" %}?src_nodes={{ node.address }}">
|
||||
<a class="button" href="{% url "mesh.messages" %}?src_nodes={{ node.address }}">
|
||||
{% trans 'View messages' %}
|
||||
</a>
|
||||
<a class="button" href="{% url "control.mesh.send" msg_type="MESH_ROUTE_REQUEST" %}?address={{ node.address }}" >
|
||||
<a class="button" href="{% url "mesh.send" msg_type="MESH_ROUTE_REQUEST" %}?address={{ node.address }}" >
|
||||
{% trans 'Find route' %}
|
||||
</a>
|
||||
</p>
|
||||
|
@ -58,7 +58,7 @@
|
|||
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="{% url "control.mesh.send" recipient=node.address msg_type="CONFIG_BOARD" %}">
|
||||
<a class="button" href="{% url "mesh.send" recipient=node.address msg_type="CONFIG_BOARD" %}">
|
||||
{% trans 'Chaange board settings' %}
|
||||
</a>
|
||||
</p>
|
||||
|
@ -83,7 +83,7 @@
|
|||
<strong>SSL:</strong> {{ node.last_messages.CONFIG_UPLINK.parsed.ssl }}<br>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="{% url "control.mesh.send" recipient=node.address msg_type="CONFIG_UPLINK" %}">
|
||||
<a class="button" href="{% url "mesh.send" recipient=node.address msg_type="CONFIG_UPLINK" %}">
|
||||
{% trans 'Change' %}
|
||||
</a>
|
||||
</p>
|
||||
|
@ -93,7 +93,7 @@
|
|||
<strong>X=</strong>{{ node.last_messages.CONFIG_POSITION.parsed.x_pos }}, <strong>Y=</strong>{{ node.last_messages.CONFIG_POSITION.parsed.y_pos }}, <strong>Z=</strong>{{ node.last_messages.CONFIG_POSITION.parsed.z_pos }}
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="{% url "control.mesh.send" recipient=node.address msg_type="CONFIG_POSITION" %}">
|
||||
<a class="button" href="{% url "mesh.send" recipient=node.address msg_type="CONFIG_POSITION" %}">
|
||||
{% trans 'Change' %}
|
||||
</a>
|
||||
</p>
|
43
src/c3nav/mesh/templates/mesh/nodes.html
Normal file
43
src/c3nav/mesh/templates/mesh/nodes.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}{% trans 'Mesh Nodes' %}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Chip' %}</th>
|
||||
<th>{% trans 'Firmware' %}</th>
|
||||
<th>{% trans 'Last msg' %}</th>
|
||||
<th>{% trans 'Last signin' %}</th>
|
||||
<th>{% trans 'Uplink' %}</th>
|
||||
</tr>
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td><a href="{% url "mesh.node.detail" pk=node.address %}">{{ node }}</a></td>
|
||||
<td>
|
||||
{{ node.last_messages.CONFIG_BOARD.parsed.board_config.board.pretty_name }}
|
||||
({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} <small>rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }}</small>)
|
||||
</td>
|
||||
<td>
|
||||
{{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.version }}
|
||||
<small>(IDF {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.idf_version }})</small>
|
||||
</td>
|
||||
<td>
|
||||
{% blocktrans trimmed with timesince=node.last_msg|timesince %}
|
||||
{{ timesince }} ago
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td>
|
||||
{% blocktrans trimmed with timesince=node.last_signin|timesince %}
|
||||
{{ timesince }} ago
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
{% comment %}todo: hide uplink if timed out{% endcomment %}
|
||||
{% comment %}todo: more details{% endcomment %}
|
||||
<td>{% if node.uplink %}<a href="{% url "mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -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/<int:pk>/', FirmwareDetailView.as_view(), name='mesh.firmwares.detail'),
|
||||
path('firmwares/builds/<int:pk>/', FirmwareBuildDetailView.as_view(), name='mesh.firmwares.build.detail'),
|
||||
path('<str:pk>/', NodeDetailView.as_view(), name='mesh.node.detail'),
|
||||
path('<str:pk>/edit/', NodeEditView.as_view(), name='mesh.node.edit'),
|
||||
path('message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='mesh.sending'),
|
||||
path('message/<str:recipient>/<str:msg_type>/', MeshMessageSendView.as_view(), name='mesh.send'),
|
||||
path('message/<str:msg_type>/', MeshMessageSendView.as_view(), name='mesh.send'),
|
||||
]
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws', MeshConsumer.as_asgi()),
|
||||
|
|
0
src/c3nav/mesh/views/__init__.py
Normal file
0
src/c3nav/mesh/views/__init__.py
Normal file
13
src/c3nav/mesh/views/base.py
Normal file
13
src/c3nav/mesh/views/base.py
Normal file
|
@ -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)
|
106
src/c3nav/mesh/views/firmware.py
Normal file
106
src/c3nav/mesh/views/firmware.py
Normal file
|
@ -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
|
131
src/c3nav/mesh/views/messages.py
Normal file
131
src/c3nav/mesh/views/messages.py
Normal file
|
@ -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,
|
||||
}
|
14
src/c3nav/mesh/views/misc.py
Normal file
14
src/c3nav/mesh/views/misc.py
Normal file
|
@ -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(),
|
||||
}
|
51
src/c3nav/mesh/views/nodes.py
Normal file
51
src/c3nav/mesh/views/nodes.py
Normal file
|
@ -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})
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue