OTA create view
This commit is contained in:
parent
73876f1b75
commit
58df04b4af
10 changed files with 368 additions and 169 deletions
|
@ -0,0 +1,69 @@
|
|||
# Generated by Django 4.2.1 on 2023-11-10 14:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("mesh", "0009_meshuplink"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OTAUpdate",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="creation"),
|
||||
),
|
||||
(
|
||||
"build",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="mesh.firmwarebuild",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="OTAUpdateRecipient",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"node",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="ota_updates",
|
||||
to="mesh.meshnode",
|
||||
verbose_name="node",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="recipients",
|
||||
to="mesh.otaupdate",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -41,17 +41,26 @@ class FirmwareDescription:
|
|||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HardwareDescription:
|
||||
chip: ChipType
|
||||
board: BoardType
|
||||
|
||||
|
||||
class MeshNodeQuerySet(models.QuerySet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._prefetch_last_messages = set()
|
||||
self._prefetch_last_messages_done = False
|
||||
self._prefetch_firmwares = False
|
||||
self._prefetch_ota = False
|
||||
self._prefetch_ota_done = False
|
||||
|
||||
def _clone(self):
|
||||
clone = super()._clone()
|
||||
clone._prefetch_last_messages = self._prefetch_last_messages
|
||||
clone._prefetch_firmwares = self._prefetch_firmwares
|
||||
clone._prefetch_ota = self._prefetch_ota
|
||||
return clone
|
||||
|
||||
def prefetch_last_messages(self, *types: MeshMessageType):
|
||||
|
@ -67,8 +76,14 @@ class MeshNodeQuerySet(models.QuerySet):
|
|||
clone._prefetch_firmwares = True
|
||||
return clone
|
||||
|
||||
def prefetch_ota(self):
|
||||
clone = self._chain()
|
||||
clone._prefetch_pta = True
|
||||
return clone
|
||||
|
||||
def _fetch_all(self):
|
||||
super()._fetch_all()
|
||||
nodes = None
|
||||
if self._prefetch_last_messages and not self._prefetch_last_messages_done:
|
||||
nodes: dict[str, MeshNode] = {node.pk: node for node in self._result_cache}
|
||||
try:
|
||||
|
@ -125,6 +140,22 @@ class MeshNodeQuerySet(models.QuerySet):
|
|||
for node in nodes_to_complete:
|
||||
node.firmware_desc.created = created_lookup[node.firmware_desc.sha256_hash]
|
||||
|
||||
if self._prefetch_ota and not self._prefetch_ota_done:
|
||||
if nodes is None:
|
||||
nodes: dict[str, MeshNode] = {node.pk: node for node in self._result_cache}
|
||||
try:
|
||||
for ota in OTAUpdateRecipient.objects.order_by('node', '-update__created').filter(
|
||||
src_node__in=nodes.keys(),
|
||||
).select_related("update", "update__build").distinct('node'):
|
||||
# noinspection PyUnresolvedReferences
|
||||
nodes[ota.node_id]._current_ota = ota
|
||||
for node in nodes.values():
|
||||
if not hasattr(node, "_current_ota"):
|
||||
node._current_ota = None
|
||||
self._prefetch_ota_done = True
|
||||
except NotSupportedError:
|
||||
pass
|
||||
|
||||
|
||||
class LastMessagesByTypeLookup(UserDict):
|
||||
def __init__(self, node):
|
||||
|
@ -183,8 +214,18 @@ class MeshNode(models.Model):
|
|||
def last_messages(self) -> Mapping[Any, "NodeMessage"]:
|
||||
return LastMessagesByTypeLookup(self)
|
||||
|
||||
@cached_property
|
||||
def current_ota(self) -> Optional["OTAUpdateRecipient"]:
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return self._current_ota
|
||||
except AttributeError:
|
||||
return self.ota_updates.order_by('-update__created').first()
|
||||
|
||||
def get_firmware_description(self) -> FirmwareDescription:
|
||||
# noinspection PyTypeChecker
|
||||
firmware_msg: ConfigFirmwareMessage = self.last_messages[MeshMessageType.CONFIG_FIRMWARE].parsed
|
||||
# noinspection PyTypeChecker
|
||||
hardware_msg: ConfigHardwareMessage = self.last_messages[MeshMessageType.CONFIG_HARDWARE].parsed
|
||||
return FirmwareDescription(
|
||||
chip=hardware_msg.chip,
|
||||
|
@ -194,6 +235,13 @@ class MeshNode(models.Model):
|
|||
sha256_hash=firmware_msg.app_desc.app_elf_sha256,
|
||||
)
|
||||
|
||||
def get_hardware_description(self) -> HardwareDescription:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return HardwareDescription(
|
||||
chip=self.last_messages[MeshMessageType.CONFIG_HARDWARE].parsed.chip,
|
||||
board=self.last_messages[MeshMessageType.CONFIG_BOARD].parsed.board_config.board,
|
||||
)
|
||||
|
||||
# overriden by prefetch_firmwares()
|
||||
firmware_desc = None
|
||||
|
||||
|
@ -203,6 +251,7 @@ class MeshNode(models.Model):
|
|||
|
||||
@cached_property
|
||||
def board(self) -> ChipType:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return self.last_messages[MeshMessageType.CONFIG_BOARD].parsed.board_config.board
|
||||
|
||||
def get_uplink(self) -> Optional["MeshUplink"]:
|
||||
|
@ -233,7 +282,7 @@ class MeshUplink(models.Model):
|
|||
|
||||
name = models.CharField(_('channel name'), max_length=128)
|
||||
started = models.DateTimeField(_('started'), auto_now_add=True)
|
||||
node = models.ForeignKey('MeshNode', models.PROTECT, related_name='uplink_sessions',
|
||||
node = models.ForeignKey(MeshNode, models.PROTECT, related_name='uplink_sessions',
|
||||
verbose_name=_('node'))
|
||||
last_ping = models.DateTimeField(_('last ping from consumer'))
|
||||
end_reason = models.CharField(_('end reason'), choices=EndReason.choices, null=True, max_length=16)
|
||||
|
@ -246,9 +295,9 @@ class MeshUplink(models.Model):
|
|||
|
||||
class NodeMessage(models.Model):
|
||||
MESSAGE_TYPES = [(msgtype.name, msgtype.pretty_name) for msgtype in MeshMessageType]
|
||||
src_node = models.ForeignKey('MeshNode', models.PROTECT,
|
||||
related_name='received_messages', verbose_name=_('node'))
|
||||
uplink = models.ForeignKey('MeshUplink', models.PROTECT, related_name='relayed_messages',
|
||||
src_node = models.ForeignKey(MeshNode, models.PROTECT, related_name='received_messages',
|
||||
verbose_name=_('node'))
|
||||
uplink = models.ForeignKey(MeshUplink, models.PROTECT, related_name='relayed_messages',
|
||||
verbose_name=_('uplink'))
|
||||
datetime = models.DateTimeField(_('datetime'), db_index=True, auto_now_add=True)
|
||||
message_type = models.CharField(_('message type'), max_length=24, db_index=True, choices=MESSAGE_TYPES)
|
||||
|
@ -308,6 +357,10 @@ class FirmwareBuild(models.Model):
|
|||
def boards(self):
|
||||
return {BoardType[board.board] for board in self.firmwarebuildboard_set.all()}
|
||||
|
||||
@property
|
||||
def chip_type(self) -> ChipType:
|
||||
return ChipType(self.chip)
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
'chip': ChipType(self.chip).name,
|
||||
|
@ -318,7 +371,7 @@ class FirmwareBuild(models.Model):
|
|||
|
||||
def get_firmware_description(self) -> FirmwareDescription:
|
||||
return FirmwareDescription(
|
||||
chip=ChipType(self.chip),
|
||||
chip=self.chip_type,
|
||||
project_name=self.version.project_name,
|
||||
version=self.version.version,
|
||||
idf_version=self.version.idf_version,
|
||||
|
@ -327,6 +380,15 @@ class FirmwareBuild(models.Model):
|
|||
build=self,
|
||||
)
|
||||
|
||||
def get_hardware_descriptions(self) -> list[HardwareDescription]:
|
||||
return [
|
||||
HardwareDescription(
|
||||
chip=self.chip_type,
|
||||
board=board,
|
||||
)
|
||||
for board in self.boards
|
||||
]
|
||||
|
||||
|
||||
class FirmwareBuildBoard(models.Model):
|
||||
BOARDS = [(boardtype.name, boardtype.pretty_name) for boardtype in BoardType]
|
||||
|
@ -337,3 +399,14 @@ class FirmwareBuildBoard(models.Model):
|
|||
unique_together = [
|
||||
('build', 'board'),
|
||||
]
|
||||
|
||||
|
||||
class OTAUpdate(models.Model):
|
||||
build = models.ForeignKey(FirmwareBuild, on_delete=models.CASCADE)
|
||||
created = models.DateTimeField(_('creation'), auto_now_add=True)
|
||||
|
||||
|
||||
class OTAUpdateRecipient(models.Model):
|
||||
update = models.ForeignKey(OTAUpdate, on_delete=models.CASCADE, related_name='recipients')
|
||||
node = models.ForeignKey(MeshNode, models.PROTECT, related_name='ota_updates',
|
||||
verbose_name=_('node'))
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div>
|
||||
<p>
|
||||
<strong>Project name:</strong> {{ build.version.project_name }}<br>
|
||||
<strong>Version:</strong> {{ build.version.version }}<br>
|
||||
<strong>Version:</strong> <a href="{% url 'mesh.firmwares.detail' pk=build.version.pk %}">{{ build.version.version }}</a><br>
|
||||
<strong>IDF Version:</strong> {{ build.version.idf_version }}<br>
|
||||
<strong>Uploader:</strong> {{ build.version.uploader }}<br>
|
||||
<strong>Created:</strong> {{ build.version.created }}<br>
|
||||
|
@ -27,52 +27,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Installed nodes</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Hardware' %}</th>
|
||||
</tr>
|
||||
{% for node in installed_nodes %}
|
||||
<tr>
|
||||
<td>
|
||||
{% mesh_node node %}
|
||||
</td>
|
||||
<td>
|
||||
{{ node.board.pretty_name }} ({{ node.chip.pretty_name }})
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h4>Compatible nodes</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Hardware' %}</th>
|
||||
<th>{% trans 'Current Firmware' %}</th>
|
||||
</tr>
|
||||
{% for node in compatible_nodes %}
|
||||
<tr>
|
||||
<td>
|
||||
{% mesh_node node %}
|
||||
</td>
|
||||
<td>
|
||||
{{ node.board.pretty_name }} <small>({{ node.chip.pretty_name }})</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if node.firmware_desc.build %}
|
||||
<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 "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 %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% include "mesh/fragment_ota_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -42,62 +42,7 @@
|
|||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Installed nodes</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Hardware' %}</th>
|
||||
<th>{% trans 'Build' %}</th>
|
||||
</tr>
|
||||
{% for node in installed_nodes %}
|
||||
<tr>
|
||||
<td>
|
||||
{% mesh_node node %}
|
||||
</td>
|
||||
<td>
|
||||
{{ node.board.pretty_name }} ({{ node.chip.pretty_name }})
|
||||
</td>
|
||||
<td>
|
||||
{{ node.firmware_desc.build.variant }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h4>Compatible nodes</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Hardware' %}</th>
|
||||
<th>{% trans 'Current Firmware' %}</th>
|
||||
<th>{% trans 'Compatible Builds' %}</th>
|
||||
</tr>
|
||||
{% for node in compatible_nodes %}
|
||||
<tr>
|
||||
<td>
|
||||
{% mesh_node node %}
|
||||
</td>
|
||||
<td>
|
||||
{{ node.board.pretty_name }} <small>({{ node.chip.pretty_name }})</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if node.firmware_desc.build %}
|
||||
<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 "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 %}
|
||||
</td>
|
||||
<td>
|
||||
{% for build in node.compatible_builds %}
|
||||
{{ build.variant }}<br>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% include "mesh/fragment_ota_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
12
src/c3nav/mesh/templates/mesh/fragment_firmware_cell.html
Normal file
12
src/c3nav/mesh/templates/mesh/fragment_firmware_cell.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% if firmware_desc.build %}
|
||||
<a href="{% url "mesh.firmwares.detail" pk=firmware_desc.build.version.pk %}">
|
||||
<small>{{ firmware_desc.project_name }}</small>
|
||||
{{ firmware_desc.version }}
|
||||
</a><br>
|
||||
Variant: <a href="{% url "mesh.firmwares.build.detail" pk=firmware_desc.build.pk %}">
|
||||
{{ firmware_desc.build.variant }}
|
||||
</a>
|
||||
{% else %}
|
||||
<small>{{ firmware_desc.project_name }}</small> {{ firmware_desc.version }}<br>
|
||||
<small>{{ firmware_desc.sha256_hash }}</small>
|
||||
{% endif %}
|
74
src/c3nav/mesh/templates/mesh/fragment_ota_form.html
Normal file
74
src/c3nav/mesh/templates/mesh/fragment_ota_form.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
{% load i18n mesh_node %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for group in form.groups %}
|
||||
<h4>{{ group.hardware.board.pretty_name }} ({{ group.hardware.chip.pretty_name }})</h4>
|
||||
<div style="text-align: right;margin-top: -35px;">
|
||||
Set
|
||||
<select style="width: auto;">
|
||||
<option value="all">all</option>
|
||||
<option value="older">older</option>
|
||||
<option value="newer">newer</option>
|
||||
<option value="different">different</option>
|
||||
</select>
|
||||
<select style="width: auto;">
|
||||
<option value="no-ota">with no OTA</option>
|
||||
<option value="all">regardless of OTA</option>
|
||||
</select>
|
||||
to
|
||||
{% if group.builds|length == 1 %}
|
||||
<select style="width: auto;">
|
||||
<option value="yes">yes</option>
|
||||
<option value="no">no</option>
|
||||
</select>
|
||||
{% else %}
|
||||
<select style="width: auto;">
|
||||
<option value="">---</option>
|
||||
{% for build in group.builds %}
|
||||
<option value="{{ build.pk }}">{{ build.variant }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
|
||||
<button type="button">{% trans 'Set' %}</button>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Current Firmware' %}</th>
|
||||
<th>{% trans 'Current OTA' %}</th>
|
||||
<th>
|
||||
{% if group.builds|length == 1 %}
|
||||
Install {{ group.builds.0.variant }}
|
||||
{% else %}
|
||||
{% trans 'Build to install' %}
|
||||
{% endif %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for node, field in group.fields.values %}
|
||||
<tr>
|
||||
<td>
|
||||
{% mesh_node node %}
|
||||
</td>
|
||||
<td>
|
||||
{% include "mesh/fragment_firmware_cell.html" with firmware_desc=node.firmware_desc %}
|
||||
</td>
|
||||
<td>
|
||||
{% if node.current_ota %}
|
||||
<a>#{{ node.current_ota.update.pk }} <small>({{ node.current_ota.update.created }})</small></a><br>
|
||||
{% include "mesh/fragment_firmware_cell.html" with firmware_desc=node.current_ota.update.build.get_firmware_description %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ field }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
<div style="text-align: right">
|
||||
<button type="submit">{% trans 'Start OTA' %}</button>
|
||||
</div>
|
||||
</form>
|
|
@ -21,15 +21,7 @@
|
|||
<small>({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }})</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if node.firmware_desc.build %}
|
||||
<a href="{% url "mesh.firmwares.detail" pk=node.firmware_desc.build.firmware.pk %}">{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}</a><br>
|
||||
<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 }}<br>
|
||||
<small>{{ node.firmware_desc.sha256_hash }}</small>
|
||||
{% endif %}
|
||||
{% include "mesh/fragment_firmware_cell.html" with firmware_desc=node.firmware_desc %}
|
||||
</td>
|
||||
<td>
|
||||
{% blocktrans trimmed with timesince=node.last_messages.any.datetime|timesince %}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from c3nav.mesh.messages import MeshMessageType
|
||||
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode
|
||||
from c3nav.mesh.views.base import MeshControlMixin
|
||||
from c3nav.site.forms import OTACreateForm
|
||||
|
||||
|
||||
class FirmwaresListView(MeshControlMixin, ListView):
|
||||
|
@ -33,7 +35,22 @@ class FirmwaresCurrentListView(MeshControlMixin, TemplateView):
|
|||
}
|
||||
|
||||
|
||||
class FirmwareDetailView(MeshControlMixin, DetailView):
|
||||
class OTACreateMixin(SuccessMessageMixin, FormMixin):
|
||||
form_class = OTACreateForm
|
||||
success_message = 'OTA have been created'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if not form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
form.save()
|
||||
return self.form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.path
|
||||
|
||||
|
||||
class FirmwareDetailView(OTACreateMixin, MeshControlMixin, DetailView):
|
||||
model = FirmwareVersion
|
||||
template_name = "mesh/firmware_detail.html"
|
||||
context_object_name = "firmware"
|
||||
|
@ -41,38 +58,21 @@ class FirmwareDetailView(MeshControlMixin, DetailView):
|
|||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related('builds', 'builds__firmwarebuildboard_set')
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
'builds': self.get_object().builds.all(),
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
nodes: list[MeshNode] = 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,
|
||||
'builds': self.get_object().builds.all(),
|
||||
})
|
||||
return ctx
|
||||
|
||||
|
||||
class FirmwareBuildDetailView(MeshControlMixin, DetailView):
|
||||
class FirmwareBuildDetailView(OTACreateMixin, MeshControlMixin, DetailView):
|
||||
model = FirmwareBuild
|
||||
template_name = "mesh/firmware_build_detail.html"
|
||||
context_object_name = "build"
|
||||
|
@ -80,27 +80,8 @@ class FirmwareBuildDetailView(MeshControlMixin, DetailView):
|
|||
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
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
'builds': [self.get_object()],
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import replace as dataclass_replace
|
||||
from functools import cached_property
|
||||
from operator import attrgetter
|
||||
from typing import Any, Sequence
|
||||
|
||||
from django.db import transaction
|
||||
from django.forms import Form, ModelChoiceField, ModelForm
|
||||
from django.forms import BooleanField, ChoiceField, Form, ModelChoiceField, ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from c3nav.mapdata.forms import I18nModelFormMixin
|
||||
from c3nav.mapdata.models.locations import Position
|
||||
from c3nav.mapdata.models.report import Report, ReportUpdate
|
||||
from c3nav.mesh.messages import MeshMessageType
|
||||
from c3nav.mesh.models import FirmwareBuild, HardwareDescription, MeshNode, OTAUpdate
|
||||
|
||||
|
||||
class ReportIssueForm(I18nModelFormMixin, ModelForm):
|
||||
|
@ -65,3 +71,92 @@ class PositionSetForm(Form):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.fields['position'].queryset = Position.objects.filter(owner=request.user)
|
||||
self.fields['position'].label_from_instance = attrgetter('name')
|
||||
|
||||
|
||||
@dataclass
|
||||
class OTAFormGroup:
|
||||
hardware: HardwareDescription
|
||||
builds: Sequence[FirmwareBuild]
|
||||
fields: dict[str, tuple[MeshNode, Any]]
|
||||
|
||||
@cached_property
|
||||
def builds_by_id(self) -> dict[int, FirmwareBuild]:
|
||||
return {build.pk: build for build in self.builds}
|
||||
|
||||
|
||||
class OTACreateForm(Form):
|
||||
def __init__(self, builds: Sequence[FirmwareBuild], *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
nodes: Sequence[MeshNode] = MeshNode.objects.prefetch_last_messages(
|
||||
MeshMessageType.CONFIG_BOARD
|
||||
).prefetch_firmwares().prefetch_ota()
|
||||
|
||||
builds_by_hardware = {}
|
||||
for build in builds:
|
||||
for hardware_desc in build.get_hardware_descriptions():
|
||||
builds_by_hardware.setdefault(hardware_desc, []).append(build)
|
||||
|
||||
nodes_by_hardware = {}
|
||||
for node in nodes:
|
||||
nodes_by_hardware.setdefault(node.get_hardware_description(), []).append(node)
|
||||
|
||||
self._groups: list[OTAFormGroup] = []
|
||||
for hardware, hw_nodes in sorted(nodes_by_hardware.items(), key=lambda k: len(k[1]), reverse=True):
|
||||
try:
|
||||
hw_builds = builds_by_hardware[hardware]
|
||||
except KeyError:
|
||||
continue
|
||||
choices = [
|
||||
('', '---'),
|
||||
*((build.pk, build.variant) for build in hw_builds)
|
||||
]
|
||||
|
||||
group = OTAFormGroup(
|
||||
hardware=hardware,
|
||||
builds=hw_builds,
|
||||
fields={
|
||||
f'build_{node.pk}': (node, (
|
||||
ChoiceField(choices=choices, required=False)
|
||||
if len(hw_builds) > 1
|
||||
else BooleanField(required=False)
|
||||
)) for node in hw_nodes
|
||||
}
|
||||
)
|
||||
for name, (node, hw_field) in group.fields.items():
|
||||
self.fields[name] = hw_field
|
||||
self._groups.append(group)
|
||||
|
||||
@property
|
||||
def groups(self) -> list[OTAFormGroup]:
|
||||
return [
|
||||
dataclass_replace(group, fields={
|
||||
name: (node, self[name])
|
||||
for name, (node, hw_field) in group.fields.items()
|
||||
})
|
||||
for group in self._groups
|
||||
]
|
||||
|
||||
@property
|
||||
def selected_builds(self):
|
||||
build_nodes = {}
|
||||
for group in self._groups:
|
||||
for name, (node, hw_field) in group.fields.items():
|
||||
value = self.cleaned_data.get(name, None)
|
||||
if not value:
|
||||
continue
|
||||
if len(group.builds) == 1:
|
||||
build_nodes.setdefault(group.builds[0], []).append(node)
|
||||
else:
|
||||
build_nodes.setdefault(group.builds[0], []).append(group.builds_by_id[int(value)])
|
||||
return build_nodes
|
||||
|
||||
def save(self) -> list[OTAUpdate]:
|
||||
updates = []
|
||||
with transaction.atomic():
|
||||
for build, nodes in self.selected_builds.items():
|
||||
update = OTAUpdate.objects.create(build=build)
|
||||
for node in nodes:
|
||||
update.recipients.create(node=node)
|
||||
updates.append(update)
|
||||
return updates
|
||||
|
|
|
@ -1365,3 +1365,6 @@ button + button {
|
|||
padding: 2px;
|
||||
}
|
||||
}
|
||||
table td select:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue