Verified Commit 7e2f1e34 authored by Štěpán Henek's avatar Štěpán Henek 🌩

skeleton created (logic moved from the main foris repo)

parent f4c9c0b4
0.1 (????-??-??)
----------------
* initial version
This diff is collapsed.
Foris subordinates plugin
===================
This is a subordinates plugin for foris
Requirements
============
* foris
* foris-controller-subordinates-module
Installation
============
``python setup.py install``
or
``pip install .``
__import__('pkg_resources').declare_namespace(__name__)
This diff is collapsed.
# Foris - web administration interface for OpenWrt
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import bottle
import typing
from foris import fapi
from foris.form import File, Hidden, Textbox
from foris.state import current_state
from foris.utils.translators import gettext as _
from foris.config_handlers.base import BaseConfigHandler
from foris.config_handlers.wifi import WifiEditForm
class SubordinatesConfigHandler(BaseConfigHandler):
def get_form(self):
form = fapi.ForisForm("suboridinates", self.data)
section = form.add_section(
name="main_section",
title=_(self.userfriendly_title),
)
section.add_field(
File, name="token_file", label=_("Token file"), required=True)
def form_cb(data):
res = current_state.backend.perform(
"subordinates", "add_sub",
{"token": base64.b64encode(data["token_file"].file.read()).decode("utf-8")}
)
return "save_result", res
form.add_callback(form_cb)
return form
def get_controller_id_form(self, data=None):
controller_id_form = fapi.ForisForm("controller_id_form", data)
controller_section = controller_id_form.add_section("controller_section", title=None)
controller_section.add_field(
Hidden, name="controller_id", label="", required=True,
)
return controller_id_form
class SubordinatesEditForm(fapi.ForisAjaxForm):
template_name = "subordinates/_subordinates_edit.html.j2"
def __init__(self, data, controller_id=None):
self.subordinate_controller_id = data["controller_id"]
super().__init__(data, controller_id)
self.title = _("Edit device '%(controller_id)s'") % dict(
controller_id=data["controller_id"])
def convert_data_from_backend_to_form(self, backend_data):
subordinates_list = backend_data["subordinates"]
subordinates_map = {e["controller_id"]: e for e in subordinates_list}
sub_record = subordinates_map.get(self.subordinate_controller_id, None)
if not sub_record:
raise bottle.HTTPError(
404, f"Controller id {self.subordinate_controller_id} not found."
)
return sub_record["options"]
def convert_data_from_form_to_backend(self, data):
controller_id = data.pop("controller_id")
return {
"controller_id": controller_id,
"options": data
}
def make_form(self, data: typing.Optional[dict]):
form_data = self.convert_data_from_backend_to_form(
current_state.backend.perform("subordinates", "list")
)
if data:
form_data.update(data)
sub_form = fapi.ForisForm("update_sub", form_data)
sub_section = sub_form.add_section(
"subordinate_section", title="", description=_(
"You can edit managed devices here. These managed devices are directly connected to this "
"device."
)
)
sub_section.add_field(
Textbox, name="custom_name", label=_("Custom Name"),
hint=_("Nicer name for your device '%(controller_id)s'.")
% dict(controller_id=data["controller_id"])
)
sub_section.add_field(
Hidden, name="controller_id", required=True, title="",
)
def form_cb(data):
msg_data = self.convert_data_from_form_to_backend(data)
res = current_state.backend.perform("subordinates", "update_sub", msg_data)
return "save_result", res
sub_form.add_callback(form_cb)
return sub_form
class SubsubordinatesEditForm(fapi.ForisAjaxForm):
template_name = "subordinates/_subordinates_edit.html.j2"
def __init__(self, data, controller_id=None):
self.subsubordinate_controller_id = data["controller_id"]
super().__init__(data, controller_id)
self.title = _("Edit managed device '%(controller_id)s'") % dict(
controller_id=data["controller_id"])
def convert_data_from_backend_to_form(self, backend_data):
subordinates_list = backend_data["subordinates"]
subsubordinates_map = {
e["controller_id"]: e
for record in subordinates_list
for e in record["subsubordinates"]
}
subsub_record = subsubordinates_map.get(self.subsubordinate_controller_id, None)
if not subsub_record:
raise bottle.HTTPError(
404, f"Controller id {self.subsubordinate_controller_id} not found."
)
return subsub_record["options"]
def convert_data_from_form_to_backend(self, data):
controller_id = data.pop("controller_id")
return {
"controller_id": controller_id,
"options": data
}
def make_form(self, data: typing.Optional[dict]):
form_data = self.convert_data_from_backend_to_form(
current_state.backend.perform("subordinates", "list")
)
if data:
form_data.update(data)
sub_form = fapi.ForisForm("update_subsub", form_data)
sub_section = sub_form.add_section(
"subsubordinate_section", title="", description=_(
"You can edit managed devices here. These devices are not "
"not directly connected to this device but "
"they are connected through another managed device."
)
)
sub_section.add_field(
Hidden, name="controller_id", required=True, title="",
)
sub_section.add_field(
Textbox, name="custom_name", label=_("Custom Name"),
hint=_("Nicer name for your device with serial '%(controller_id)s'.")
% dict(controller_id=self.subsubordinate_controller_id)
)
def form_cb(data):
res = current_state.backend.perform(
"subordinates", "update_subsub",
{
"controller_id": data["controller_id"],
"options": {"custom_name": data["custom_name"]}
}
)
return "save_result", res
sub_form.add_callback(form_cb)
return sub_form
class SubordinatesWifiHandler(BaseConfigHandler):
def get_form(self):
ajax_form = WifiEditForm(self.data)
return ajax_form.foris_form
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>icon / turris / dark</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Assets" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon-/-turris-/-dark" fill="#595959">
<polygon id="Path" points="6.33888735 0 0 11 5.06435376 11 8.87106423 4.39428334 16.4675949 4.39428334 19 0"></polygon>
<polygon id="Path" points="14 11.9925632 20.4310855 23 23 18.6028832 19.1380606 11.9925632 22.9914323 5.39711679 20.4222863 1"></polygon>
<polygon id="Path" points="12.6666667 13 12.6497839 13 0 13 2.53105187 17.3998626 10.1413184 17.3998626 13.9378963 24 19 24 12.6723703 13"></polygon>
</g>
</g>
</svg>
\ No newline at end of file
// TODO separete reasonable part for dynamic js
Foris.messages.subordinatesSubordinatesFailed = (controller_id) => {
return `{% trans controller_id="${controller_id}" %}Connection to '{{ controller_id }}' was interrupted.{% endtrans %}`;
};
<div id="subordinates-edit" {% if hide %}style="display: none"{% endif %}>
<h3>{{ ajax_form.title }}</h3>
{% include 'config/_message.html.j2' %}
<form id="sub-form" class="config-form" action="{{ ajax_form.url }}" method="post" autocomplete="off" novalidate>
<p class="config-description">{{ form.sections[0].description|safe }}</p>
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input type="hidden" name="action" value="{{ form.name }}">
{% for field in form.active_fields %}
{% include '_field.html.j2' %}
{% endfor %}
<div id="{{ 'form-%s-buttons' % form.name }}" class="form-buttons">
<a href="#" class="button grayed">{% trans %}Close{% endtrans %}</a>
<button type="submit" name="apply" class="button">{% trans %}Apply{% endtrans %}</button>
</div>
</form>
</div>
{% macro sub_buttons(controller_id, enabled, form_class) -%}
<form action="{{ url("config_ajax", page_name="subordinates") }}" method="post" class="subordinate-buttons {{ form_class }}">
<input type="hidden" name="controller_id" value="{{ controller_id }}">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
{% if enabled %}
<button name="action" value="disable" type="submit"><i class="fas fa-pause"></i></button>
{% else %}
<button name="action" value="enable" type="submit"><i class="fas fa-play"></i></button>
{% endif %}
<button name="action" value="delete" type="submit"><i class="fas fa-trash-alt"></i> </button>
<button name="action" value="edit" type="submit"><i class="fas fa-edit"></i></button>
</form>
{%- endmacro %}
{% macro convert_state(enabled, sup_enabled) -%}
{% if enabled and sup_enabled %}
<i class="fas fa-question-circle state"></i>
{% else %}
<i class="fas fa-pause-circle state"></i>
{% endif %}
{%- endmacro %}
{%- macro should_be_connected_class(enabled, sup_enabled) %}
{% if enabled and sup_enabled %} sub-connection{% endif %}
{%- endmacro %}
{% if subordinates %}
<table id="subordinates-table">
<thead><th>{% trans %}ID{% endtrans %}</th><th>{% trans %}State{% endtrans %}</th><th></th></thead>
<tbody>
{% for sub in subordinates %}
<tr id="sub-{{ sub.controller_id }}" class="sub-line {{ should_be_connected_class(sub.enabled, True) }}">
<td>{{ sub.options.custom_name or sub.controller_id }}</td>
<td>
{{ convert_state(sub.enabled, True) }}
</td>
<td>{{ sub_buttons(sub.controller_id, sub.enabled, "sub-form") }}</td>
</tr>
{% for subsub in sub.subsubordinates %}
<tr id="sub-{{ subsub.controller_id }}" class="sub-line {{ should_be_connected_class(subsub.enabled, sub.enabled) }}">
<td class="indented-item"><i class="fas fa-level-up-alt rotate-90"></i>
{{ subsub.options.custom_name or subsub.controller_id }}
</td>
<td>
{{ convert_state(subsub.enabled, sub.enabled) }}
</td>
<td>{{ sub_buttons(subsub.controller_id, subsub.enabled, "subsub-form") }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% else %}
<p id="subordinates-table">{% trans %}Currently there are no managed devices.{% endtrans %}</p>
{% endif %}
{% macro make_simple_edit_form(controller_id, form_name) -%}
<form action="{{ url("config_ajax_form", page_name="subordinates", form_name=form_name) }}" method="post" class="subordinate-buttons">
<input type="hidden" name="controller_id" value="{{ controller_id }}">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<button name="action" value="edit" type="submit" style="display: none"><i class="fas fa-edit"></i></button>
</form>
{% endmacro -%}
{% macro convert_state(enabled, sup_enabled) -%}
{% if enabled and sup_enabled %}
<i class="fas fa-question-circle state"></i>
{% else %}
<i class="fas fa-pause-circle state"></i>
{% endif %}
{%- endmacro %}
{%- macro should_be_connected_class(enabled, sup_enabled) %}
{% if enabled and sup_enabled %} sub-connection{% endif %}
{%- endmacro %}
{% if subordinates %}
<table id="subordinates-table">
<thead><th>{% trans %}ID{% endtrans %}</th><th>{% trans %}State{% endtrans %}</th><th>{% trans %}Devices{% endtrans %}</th><th>{% trans %}Channels{% endtrans %}</th><th></th></thead>
<tbody>
{% for sub in subordinates %}
<tr id="sub-{{ sub.controller_id }}" class="sub-line {{ should_be_connected_class(sub.enabled, True) }}">
<td>{{ sub.options.custom_name or sub.controller_id }}</td>
<td>
{{ convert_state(sub.enabled, True) }}
</td>
<td class="wifi-count"><i class="fas fa-spinner rotate"></i></td>
<td class="wifi-channels"><i class="fas fa-spinner rotate"></i></td>
<td>
{{ make_simple_edit_form(sub.controller_id, "wifi") }}
</td>
</tr>
{% for subsub in sub.subsubordinates %}
<tr id="sub-{{ subsub.controller_id }}" class="sub-line {{ should_be_connected_class(subsub.enabled, sub.enabled) }}" >
<td class="indented-item"><i class="fas fa-level-up-alt rotate-90"></i>
{{ subsub.options.custom_name or subsub.controller_id }}
</td>
<td>
{{ convert_state(subsub.enabled, sub.enabled) }}
</td>
<td class="wifi-count"><i class="fas fa-spinner rotate"></i></td>
<td class="wifi-channels"><i class="fas fa-spinner rotate"></i></td>
<td>
{{ make_simple_edit_form(subsub.controller_id, "wifi") }}
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% else %}
<p id="subordinates-table">{% trans %}Currently there are no subordinates.{% endtrans %}</p>
{% endif %}
{% extends 'config/base.html.j2' %}
{% block config_base %}
{% if is_xhr is not defined %}
<div id="page-config" class="config-page">
{% endif %}
<p>
{%- trans %}
On this tab you can set up Wi-Fi of other devices controlled by this device.
{%- endtrans %}
</p>
{% if is_xhr is not defined %}
<h3>{% trans %}Device list{% endtrans %}</h3>
<div id="subordinates-message"></div>
<div id="subordinates-table">
{% trans %}Loading manged devices...{% endtrans %}
</div>
<br />
<div id="subordinates-edit">
</div>
<script>
Foris.queryBackendWifi = async (controller_id) => {
let run = async () => {
try {
resp = await Foris.performBackendQuery(controller_id, "wifi", "get_settings");
$(`#sub-${controller_id}`).find(".wifi-count").text(resp.devices.length);
let channels = [];
for (let device of resp.devices) {
if (device.enabled) {
channels.push(device.channel == 0 ? "*": device.channel);
}
};
$(`#sub-${controller_id}`).find(".wifi-channels").text(channels.join(", "));
} catch(err) {
$(`#sub-${controller_id}`).find(".wifi-count").text("?");
$(`#sub-${controller_id}`).find(".wifi-channels").text("?");
}
};
run(); // call async don't wait for it
};
Foris.loadSubordinatesList = async () => {
let resp = await $.get('{{ url("config_ajax", page_name="subordinates-wifi") }}', {action: "list"});
$("#subordinates-table").replaceWith(resp);
Foris.overrideSubordinatesButtons();
Foris.setSubordinatesTimeouts();
}
Foris.setSubordinatesTimeouts = () => {
for (timeout in Foris.subordinateKeepAliveTimeouts) {
clearTimeout(Foris.subordinateKeepAliveTimeouts[timeout]);
}
Foris.subordinateKeepAliveTimeouts = {};
$(".sub-connection").each((idx, val) => {
let id = $(val).attr("id");
if (id) {
Foris.subordinateSetKeepAliveTimeout(id.replace('sub-', ''));
}
});
};
Foris.overrideSubordinatesEditButton = async () => {
$("#subordinates-edit form").submit(async (e) => {
e.preventDefault();
let form = $(e.currentTarget);
resp = $.ajax({
type: "POST",
url: form.attr('action'),
data: form.serialize()
});
form.find("input, select, button").attr("disabled", "disabled");
let new_form = await resp;
$("#subordinates-edit").replaceWith(new_form);
await Foris.suboridnatesOverrideEditClose();
await Foris.overrideSubordinatesEditButton();
await Foris.loadSubordinatesList();
$(".subordinate-buttons button").hide();
});
};
Foris.overrideSubordinatesButtons = async () => {
$(".subordinate-buttons").submit(async (e) => {
e.preventDefault();
let form = $(e.currentTarget);
let action = $(e.originalEvent.explicitOriginalTarget).val();
let form_data = {};
let serialized = form.serializeArray();
for (record in serialized) {
form_data[serialized[record].name] = serialized[record].value;
}
let controller_id = form_data.controller_id;
let renderResponse = (resp) => {
$("#subordinates-message").replaceWith(resp);
};
let resp = null;
switch (action) {
case "edit":
Foris.subordinateInEditMode = true;
try {
resp = await $.ajax({
type: "POST",
url: '{{ url("config_ajax_form", page_name="subordinates-wifi", form_name="wifi-form") }}',
data: `&csrf_token=${form_data["csrf_token"]}&_controller_id=${form_data["controller_id"]}&_update=1`,
});
} catch(err) {
Foris.subordinateInEditMode = false;
return;
}
await $("#subordinates-edit").promise();
$("#subordinates-edit").replaceWith(resp);
$(".subordinate-buttons button").hide('slow');
await $(".subordinate-buttons button").promise();
$("#subordinates-edit").show('slow');
await $("#subordinates-edit").promise();
await Foris.suboridnatesOverrideEditClose();
Foris.overrideSubordinatesEditButton();
return;
}
$(".subordinate-buttons button").prop('disabled', true);
});
};
Foris.suboridnatesOverrideEditClose = async () => {
$("#subordinates-edit").find("a.button").click(async (e) => {
Foris.subordinateInEditMode = false;
e.preventDefault();
$("#subordinates-edit").hide('slow');
await $("#subordinates-edit").promise();
$("#subordinates-edit").replaceWith("<div id='subordinates-edit'></div>");
if (Foris.subordinatesGetView() == "subordinates") {
$(".subordinate-buttons button").show('slow');
await $(".subordinate-buttons button").promise();
}
});
};
Foris.subordinateInEditMode = false;
Foris.subordinatesAlive = {};
Foris.subordinateKeepAliveTimeouts = {};
Foris.subordinateSetKeepAliveTimeout = (controller_id) => {
let element = $(`#sub-${controller_id}`);
let state_element = element.find(".state");
Foris.subordinateKeepAliveTimeouts[controller_id] = setTimeout(() => {
Foris.subordinatesAlive[controller_id] = false;
state_element.removeClass();
state_element.addClass("state fas fa-exclamation-circle");
delete Foris.subordinateKeepAliveTimeouts[controller_id];
$(`div[data-controller-id=${controller_id}]`).replaceWith(`<div id='subordinates-edit' class='message error' data-controller-id='${controller_id}'>${Foris.messages.subordinatesSubordinatesFailed(controller_id)}</div>`);
Foris.subordinateInEditMode = false;
}, 3000);
};
Foris.updateSubordinateState = async (data) => {
let element = $(`#sub-${data.id}`);
let state_element = element.find(".state");
if (!document.hidden) {
await state_element.animate({opacity: '0.2'}, 50);
}
state_element.removeClass();
switch (data.state) {
case 'started':
state_element.addClass("state fas fa-circle");
break;
case 'running':
if (data.id in Foris.subordinateKeepAliveTimeouts) {
clearTimeout(Foris.subordinateKeepAliveTimeouts[data.id]);
}
state_element.addClass("state fas fa-check-circle");
Foris.subordinateSetKeepAliveTimeout(data.id);
if (!Foris.subordinatesAlive[data.id]) { // query backend if needed
Foris.queryBackendWifi(data.id);
}
Foris.subordinatesAlive[data.id] = true;
if (!Foris.subordinateInEditMode) {
$(`#sub-${data.id}`).find("button[value=edit]").show("slow");
let message = $(`div[data-controller-id=${data.id}].message`);
message.hide("slow");
await message.promise();
message.replaceWith("<div id='subordinates-edit'></div>");
}
break;
case 'exitted':
state_element.addClass("state fas fa-exclamation-circle");
break;
default:
state_element.addClass("state fas fa-question-circle");
break;
}
if (!document.hidden) {
await state_element.animate({opacity: '1.0'}, 50);
}
};
Foris.addWsHanlder("subordinates", async (msg) => {
switch(msg.action) {
case "add_sub":
case "add_subsub":
case "del":
case "set_enabled":
await Foris.loadSubordinatesList();
break;
}
});
Foris.addWsHanlder("remote", async (msg) => {
switch(msg.action) {
case "advertize":
Foris.updateSubordinateState(msg.data);
break;
};
}, '+');
$(document).ready(function() {
Foris.loadSubordinatesList();
Foris.afterAjaxUpdateFunctions.push(Foris.suboridnatesOverrideEditClose);
Foris.afterAjaxUpdateFunctions.push(Foris.overrideSubordinatesEditButton);
});
</script>
<style>
#subordinates-table .sub-line .state {
font-size: 1.5rem;
};
#subordinates-table td {
vertical-align: top;
};
</style>
</div>
{% endif %}
{% endblock %}
#!/usr/bin/env python
import copy
from setuptools import setup
from setuptools.command.build_py import build_py
class BuildCmd(build_py):
def run(self):
# build foris plugin files
from foris_plugins_distutils import build
cmd = build(copy.copy(self.distribution))
cmd.ensure_finalized()
cmd.run()
# build package
build_py.run(self)
setup(
name="Foris Subordinates Plugin",
version="0",
description="Subordinates plugin for foris web interface",
author="CZ.NIC, z. s. p. o.",
author_email="stepan.henek@nic.cz",
url="https://gitlab.labs.nic.cz/turris/foris-subordinates-plugin/",
license="GPL-3.0",
install_requires=[
"foris",
'jinja2',
],
setup_requires=[
'babel',
'libsass',
'foris_plugins_distutils',
],
provides=[
"foris_plugins.subordinates",
],
packages=[
"foris_plugins.subordinates",
],
package_data={
'': [
"templates/**",
"templates/**/*",
"templates/javascript/**",
"templates/javascript/**/*",
"locale/**/LC_MESSAGES/*.mo",
"static/css/*.css",
"static/fonts/*",
"static/img/*",
"static/js/*.js",
"static/js/contrib/*",
],
},
namespace_packages=[
'foris_plugins',
],
cmdclass={
"build_py": BuildCmd, # modify build_py to build the foris files as well
}
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment