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},
|
limit_choices_to={'access_restriction': None},
|
||||||
verbose_name=_('can review reports belonging to'))
|
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'))
|
api_secret = models.CharField(null=True, blank=True, max_length=64, verbose_name=_('API secret'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
{% if request.user_permissions.manage_map_updates %}
|
{% if request.user_permissions.manage_map_updates %}
|
||||||
<a href="{% url 'control.map_updates' %}">{% trans 'Map Updates' %}</a> ·
|
<a href="{% url 'control.map_updates' %}">{% trans 'Map Updates' %}</a> ·
|
||||||
{% endif %}
|
{% 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>
|
<a href="{% url 'control.users.detail' user=request.user.pk %}">{{ request.user.username }}</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</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:
|
if name:
|
||||||
return format_html(
|
return format_html(
|
||||||
'<a href="{url}">{bssid}</a> ({name})',
|
'<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:
|
else:
|
||||||
return format_html(
|
return format_html(
|
||||||
'<a href="{url}">{bssid}</a>',
|
'<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.announcements import announcement_detail, announcement_list
|
||||||
from c3nav.control.views.base import ControlPanelIndexView
|
from c3nav.control.views.base import ControlPanelIndexView
|
||||||
from c3nav.control.views.mapupdates import map_updates
|
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
|
from c3nav.control.views.users import UserListView, user_detail
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -17,18 +14,5 @@ urlpatterns = [
|
||||||
path('announcements/', announcement_list, name='control.announcements'),
|
path('announcements/', announcement_list, name='control.announcements'),
|
||||||
path('announcements/<int:annoucement>/', announcement_detail, name='control.announcements.detail'),
|
path('announcements/<int:annoucement>/', announcement_detail, name='control.announcements.detail'),
|
||||||
path('mapupdates/', map_updates, name='control.map_updates'),
|
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'),
|
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 %}
|
{% load i18n mesh_node %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
||||||
|
@ -62,10 +62,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if node.firmware_desc.build %}
|
{% 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 }}
|
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||||
</a><br>
|
</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 %}
|
{% else %}
|
||||||
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n mesh_node %}
|
{% load i18n mesh_node %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
{% for build in builds %}
|
{% for build in builds %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "control.mesh.firmwares.build.detail" pk=build.pk %}">
|
<a href="{% url "mesh.firmwares.build.detail" pk=build.pk %}">
|
||||||
{{ build.variant }}
|
{{ build.variant }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -82,10 +82,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if node.firmware_desc.build %}
|
{% 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 }}
|
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||||
</a><br>
|
</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 %}
|
{% else %}
|
||||||
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -1,11 +1,10 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
{% block heading %}{% trans 'Available firmwares' %}{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
<h4>Firmwares</h4>
|
<a class="button" href="{% url "mesh.firmwares.current" %}">
|
||||||
<a class="button" href="{% url "control.mesh.firmwares.current" %}">
|
|
||||||
{% trans 'View current firmwares' %}
|
{% trans 'View current firmwares' %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -24,14 +23,14 @@
|
||||||
<td>{{ firmware.created }}</td>
|
<td>{{ firmware.created }}</td>
|
||||||
<td>{{ firmware.uploader }}</td>
|
<td>{{ firmware.uploader }}</td>
|
||||||
<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 }}
|
{{ firmware.project_name }} {{ firmware.version }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ firmware.idf_version }}</td>
|
<td>{{ firmware.idf_version }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% for build in firmware.builds.all %}
|
{% 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 }})
|
{{ build.variant }} ({{ build.get_chip_display }})
|
||||||
</a><br>
|
</a><br>
|
||||||
{% endfor %}
|
{% endfor %}
|
|
@ -1,11 +1,10 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n mesh_node %}
|
{% load i18n mesh_node %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh' %}{% endblock %}
|
{% block heading %}{% trans 'Current firmwares' %}{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
<h4>Current Firmwares</h4>
|
<a class="button" href="{% url "mesh.firmwares" %}">
|
||||||
<a class="button" href="{% url "control.mesh.firmwares" %}">
|
|
||||||
{% trans 'View available firmwares' %}
|
{% trans 'View available firmwares' %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@
|
||||||
<td>{{ firmware.created }}</td>
|
<td>{{ firmware.created }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if firmware.build %}
|
{% 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 %}
|
{% else %}
|
||||||
{{ firmware.project_name }} {{ firmware.version }}<br>
|
{{ firmware.project_name }} {{ firmware.version }}<br>
|
||||||
<small>{{ firmware.sha256_hash }}</small>
|
<small>{{ firmware.sha256_hash }}</small>
|
||||||
|
@ -30,7 +29,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if firmware.build %}
|
{% 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 }})
|
{{ firmware.build.variant }} ({{ firmware.chip.pretty_name }})
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
|
@ -38,7 +38,7 @@ function connect() {
|
||||||
if (data.uplink) {
|
if (data.uplink) {
|
||||||
cell.append(document.createElement("br"));
|
cell.append(document.createElement("br"));
|
||||||
link_tag = document.createElement("a");
|
link_tag = document.createElement("a");
|
||||||
link_tag.href = "/control/mesh/" + data.uplink;
|
link_tag.href = "/mesh/" + data.uplink;
|
||||||
link_tag.innerText = data.uplink;
|
link_tag.innerText = data.uplink;
|
||||||
if (node_names[data.uplink]) {
|
if (node_names[data.uplink]) {
|
||||||
link_tag.innerText += " ("+node_names[data.uplink]+")";
|
link_tag.innerText += " ("+node_names[data.uplink]+")";
|
||||||
|
@ -49,7 +49,7 @@ function connect() {
|
||||||
|
|
||||||
cell = document.createElement("td");
|
cell = document.createElement("td");
|
||||||
link_tag = document.createElement("a");
|
link_tag = document.createElement("a");
|
||||||
link_tag.href = "/control/mesh/" + data.node;
|
link_tag.href = "/mesh/" + data.node;
|
||||||
link_tag.innerText = data.node;
|
link_tag.innerText = data.node;
|
||||||
if (node_names[data.node]) {
|
if (node_names[data.node]) {
|
||||||
link_tag.innerText += " ("+node_names[data.node]+")";
|
link_tag.innerText += " ("+node_names[data.node]+")";
|
||||||
|
@ -76,7 +76,7 @@ function connect() {
|
||||||
line.appendChild(text)
|
line.appendChild(text)
|
||||||
|
|
||||||
link_tag = document.createElement("a");
|
link_tag = document.createElement("a");
|
||||||
link_tag.href = "/control/mesh/" + data.uplink;
|
link_tag.href = "/mesh/" + data.uplink;
|
||||||
link_tag.innerText = data.uplink;
|
link_tag.innerText = data.uplink;
|
||||||
if (node_names[data.uplink]) {
|
if (node_names[data.uplink]) {
|
||||||
link_tag.innerText += "("+node_names[data.uplink]+")";
|
link_tag.innerText += "("+node_names[data.uplink]+")";
|
||||||
|
@ -96,7 +96,7 @@ function connect() {
|
||||||
|
|
||||||
cell = document.createElement("td");
|
cell = document.createElement("td");
|
||||||
link_tag = document.createElement("a");
|
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;
|
link_tag.innerText = data.msg.src;
|
||||||
if (node_names[data.msg.src]) {
|
if (node_names[data.msg.src]) {
|
||||||
link_tag.innerText += " ("+node_names[data.msg.src]+")";
|
link_tag.innerText += " ("+node_names[data.msg.src]+")";
|
||||||
|
@ -109,7 +109,7 @@ function connect() {
|
||||||
} else {
|
} else {
|
||||||
cell = document.createElement("td");
|
cell = document.createElement("td");
|
||||||
link_tag = document.createElement("a");
|
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;
|
link_tag.innerText = data.msg.route;
|
||||||
if (node_names[data.msg.route]) {
|
if (node_names[data.msg.route]) {
|
||||||
link_tag.innerText += " ("+node_names[data.msg.route]+")";
|
link_tag.innerText += " ("+node_names[data.msg.route]+")";
|
||||||
|
@ -125,7 +125,7 @@ function connect() {
|
||||||
|
|
||||||
cell = document.createElement("td");
|
cell = document.createElement("td");
|
||||||
link_tag = document.createElement("a");
|
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];
|
link_tag.innerText = data.msg.trace[i];
|
||||||
if (node_names[data.msg.trace[i]]) {
|
if (node_names[data.msg.trace[i]]) {
|
||||||
link_tag.innerText += " ("+node_names[data.msg.trace[i]]+")";
|
link_tag.innerText += " ("+node_names[data.msg.trace[i]]+")";
|
||||||
|
@ -145,4 +145,4 @@ function connect() {
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
</script>
|
</script>
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh logs' %}{% endblock %}
|
{% block heading %}{% trans 'Mesh logs' %}{% endblock %}
|
||||||
|
@ -15,5 +15,5 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="mesh-logs"></tbody>
|
<tbody id="mesh-logs"></tbody>
|
||||||
</table>
|
</table>
|
||||||
{% include "control/fragment_mesh_websocket.html" %}
|
{% include "mesh/fragment_mesh_websocket.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
<p><a class="button" href="{{ success_url }}">Go back</a></p>
|
<p><a class="button" href="{{ success_url }}">Go back</a></p>
|
||||||
{% if msg_type == "MESH_ROUTE_REQUEST" %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div>
|
<div>
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
{% for address, name in recipients %}
|
{% for address, name in recipients %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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 %}
|
{{ address }}{% if name %} ({{ name }}){% endif %}
|
||||||
{% if address != "ff:ff:ff:ff:ff:ff" %}</a>{% endif %}
|
{% if address != "ff:ff:ff:ff:ff:ff" %}</a>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -69,6 +69,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include "control/fragment_mesh_websocket.html" %}
|
{% include "mesh/fragment_mesh_websocket.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,10 +1,30 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n mesh_node %}
|
{% load i18n mesh_node %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh messages' %}{% endblock %}
|
{% block heading %}{% trans 'Mesh messages' %}{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% 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>
|
<form>
|
||||||
|
<h4>Filter</h4>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ form.message_types }}
|
{{ form.message_types }}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'control/base.html' %}
|
{% extends 'mesh/base.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block heading %}{% trans 'Mesh Node' %} {{ node }}{% endblock %}
|
{% block heading %}{% trans 'Mesh Node' %} {{ node }}{% endblock %}
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% comment %}todo: more details{% endcomment %}
|
{% 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>
|
<strong>Last signin:</strong>
|
||||||
{{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }}
|
{{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }}
|
||||||
|
@ -30,13 +30,13 @@
|
||||||
<br>
|
<br>
|
||||||
</p>
|
</p>
|
||||||
<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' %}
|
{% trans 'Edit' %}
|
||||||
</a>
|
</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' %}
|
{% trans 'View messages' %}
|
||||||
</a>
|
</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' %}
|
{% trans 'Find route' %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<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' %}
|
{% trans 'Chaange board settings' %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
<strong>SSL:</strong> {{ node.last_messages.CONFIG_UPLINK.parsed.ssl }}<br>
|
<strong>SSL:</strong> {{ node.last_messages.CONFIG_UPLINK.parsed.ssl }}<br>
|
||||||
</p>
|
</p>
|
||||||
<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' %}
|
{% trans 'Change' %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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 }}
|
<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>
|
||||||
<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' %}
|
{% trans 'Change' %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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 django.urls import path
|
||||||
|
|
||||||
from c3nav.mesh.consumers import MeshConsumer, MeshUIConsumer
|
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 = [
|
websocket_urlpatterns = [
|
||||||
path('ws', MeshConsumer.as_asgi()),
|
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('map/', include(c3nav.mapdata.urls)),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('control/', include(c3nav.control.urls)),
|
path('control/', include(c3nav.control.urls)),
|
||||||
|
path('mesh/', include(c3nav.mesh.urls)),
|
||||||
path('locales/', include('django.conf.urls.i18n')),
|
path('locales/', include('django.conf.urls.i18n')),
|
||||||
path('', include(c3nav.site.urls)),
|
path('', include(c3nav.site.urls)),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue